Skip to content

Commit 9723f3f

Browse files
committed
Add PHPUnit tests for AbstractPage circular parent validation and ManifestDepthGroupTrait
AbstractPageTest covers: - validateNoCircularParent: no parent, self-reference, indirect cycle, valid chain, new page without ID, parentPageData field name - getParentPageRoute: with route, parent without route, no parent ManifestDepthGroupTraitTest covers: - flat resource → single depth group - parentPage and parentPageData boundaries produce two groups, root first - two-level nesting produces three groups - well-known and resource_metadatas IRIs filtered out - duplicate IRIs within a group deduplicated - arrays of sub-resources are walked - parent IRI does not appear in child group
1 parent c0fb4da commit 9723f3f

2 files changed

Lines changed: 342 additions & 0 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Silverback\ApiComponentsBundle\Tests\Entity\Core;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Ramsey\Uuid\Uuid;
16+
use Ramsey\Uuid\UuidInterface;
17+
use Silverback\ApiComponentsBundle\Entity\Core\AbstractPageData;
18+
use Silverback\ApiComponentsBundle\Entity\Core\Page;
19+
use Silverback\ApiComponentsBundle\Entity\Core\Route;
20+
use Symfony\Component\Validator\Context\ExecutionContextInterface;
21+
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
22+
23+
/**
24+
* Concrete Page subclass that exposes an ID setter for tests.
25+
* AbstractPage::$id is protected (from IdTrait) so we set it from within the hierarchy.
26+
*/
27+
class TestPage extends Page
28+
{
29+
public function withId(UuidInterface $id): static
30+
{
31+
$this->id = $id;
32+
33+
return $this;
34+
}
35+
}
36+
37+
/**
38+
* Concrete AbstractPageData subclass for testing the parentPageData violation field name.
39+
*/
40+
class TestPageData extends AbstractPageData
41+
{
42+
public Page $page;
43+
44+
public function withId(UuidInterface $id): static
45+
{
46+
$this->id = $id;
47+
48+
return $this;
49+
}
50+
}
51+
52+
class AbstractPageTest extends TestCase
53+
{
54+
private function makePage(?string $uuid = null): TestPage
55+
{
56+
$page = new TestPage();
57+
if ($uuid) {
58+
$page->withId(Uuid::fromString($uuid));
59+
}
60+
61+
return $page;
62+
}
63+
64+
private function makeContext(int $expectedViolations = 0, ?string $expectedField = null): ExecutionContextInterface
65+
{
66+
$context = $this->createMock(ExecutionContextInterface::class);
67+
68+
if (0 === $expectedViolations) {
69+
$context->expects($this->never())->method('buildViolation');
70+
71+
return $context;
72+
}
73+
74+
$builder = $this->createMock(ConstraintViolationBuilderInterface::class);
75+
$builder->expects($this->exactly($expectedViolations))->method('atPath')->with($expectedField)->willReturn($builder);
76+
$builder->expects($this->exactly($expectedViolations))->method('addViolation');
77+
$context->expects($this->exactly($expectedViolations))->method('buildViolation')->willReturn($builder);
78+
79+
return $context;
80+
}
81+
82+
public function test_no_parent_produces_no_violation(): void
83+
{
84+
$page = $this->makePage('11111111-1111-1111-1111-111111111111');
85+
$page->validateNoCircularParent($this->makeContext(0));
86+
}
87+
88+
public function test_self_reference_via_parent_page_triggers_violation(): void
89+
{
90+
$page = $this->makePage('11111111-1111-1111-1111-111111111111');
91+
$page->setParentPage($page);
92+
93+
$page->validateNoCircularParent($this->makeContext(1, 'parentPage'));
94+
}
95+
96+
public function test_indirect_cycle_via_parent_page_triggers_violation(): void
97+
{
98+
$a = $this->makePage('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
99+
$b = $this->makePage('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
100+
101+
$b->setParentPage($a);
102+
$a->setParentPage($b);
103+
104+
$a->validateNoCircularParent($this->makeContext(1, 'parentPage'));
105+
}
106+
107+
public function test_valid_chain_produces_no_violation(): void
108+
{
109+
$a = $this->makePage('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
110+
$b = $this->makePage('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
111+
$c = $this->makePage('cccccccc-cccc-cccc-cccc-cccccccccccc');
112+
113+
$c->setParentPage($b);
114+
$b->setParentPage($a);
115+
116+
$c->validateNoCircularParent($this->makeContext(0));
117+
}
118+
119+
public function test_new_page_without_id_with_parent_produces_no_violation(): void
120+
{
121+
$parent = $this->makePage('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
122+
$newPage = $this->makePage(); // no ID — not yet persisted
123+
124+
$newPage->setParentPage($parent);
125+
126+
$newPage->validateNoCircularParent($this->makeContext(0));
127+
}
128+
129+
public function test_cycle_via_parent_page_data_reports_correct_field(): void
130+
{
131+
$pageData = new TestPageData();
132+
$pageData->withId(Uuid::fromString('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'));
133+
$pageData->setParentPageData($pageData);
134+
135+
$pageData->validateNoCircularParent($this->makeContext(1, 'parentPageData'));
136+
}
137+
138+
public function test_get_parent_page_route_returns_parent_route(): void
139+
{
140+
$route = new Route();
141+
$parent = $this->makePage('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
142+
$parent->setRoute($route);
143+
144+
$child = $this->makePage('cccccccc-cccc-cccc-cccc-cccccccccccc');
145+
$child->setParentPage($parent);
146+
147+
$this->assertSame($route, $child->getParentPageRoute());
148+
}
149+
150+
public function test_get_parent_page_route_returns_null_when_parent_has_no_route(): void
151+
{
152+
$parent = $this->makePage('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb');
153+
$child = $this->makePage('cccccccc-cccc-cccc-cccc-cccccccccccc');
154+
$child->setParentPage($parent);
155+
156+
$this->assertNull($child->getParentPageRoute());
157+
}
158+
159+
public function test_get_parent_page_route_returns_null_when_no_parent(): void
160+
{
161+
$page = $this->makePage('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
162+
163+
$this->assertNull($page->getParentPageRoute());
164+
}
165+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Silverback\ApiComponentsBundle\Tests\Serializer\Normalizer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Silverback\ApiComponentsBundle\Serializer\Normalizer\Trait\ManifestDepthGroupTrait;
16+
17+
class ConcreteManifestDepthGroup
18+
{
19+
use ManifestDepthGroupTrait;
20+
21+
public function groups(array $resource): array
22+
{
23+
return $this->buildDepthGroups($resource);
24+
}
25+
}
26+
27+
class ManifestDepthGroupTraitTest extends TestCase
28+
{
29+
private ConcreteManifestDepthGroup $subject;
30+
31+
protected function setUp(): void
32+
{
33+
$this->subject = new ConcreteManifestDepthGroup();
34+
}
35+
36+
public function test_flat_resource_returns_single_depth_group(): void
37+
{
38+
$resource = [
39+
'@id' => '/_/routes/home',
40+
'page' => ['@id' => '/_/pages/abc'],
41+
];
42+
43+
$this->assertSame(
44+
[['/_/routes/home', '/_/pages/abc']],
45+
$this->subject->groups($resource)
46+
);
47+
}
48+
49+
public function test_resource_with_parent_page_returns_two_groups_root_first(): void
50+
{
51+
$resource = [
52+
'@id' => '/_/abstract_page_data/child-uuid',
53+
'page' => ['@id' => '/_/pages/child-page-uuid'],
54+
'parentPage' => [
55+
'@id' => '/_/pages/parent-uuid',
56+
'route' => ['@id' => '/_/routes/conference'],
57+
],
58+
];
59+
60+
$groups = $this->subject->groups($resource);
61+
62+
$this->assertCount(2, $groups);
63+
$this->assertContains('/_/pages/parent-uuid', $groups[0]);
64+
$this->assertContains('/_/routes/conference', $groups[0]);
65+
$this->assertContains('/_/abstract_page_data/child-uuid', $groups[1]);
66+
$this->assertContains('/_/pages/child-page-uuid', $groups[1]);
67+
$this->assertNotContains('/_/pages/parent-uuid', $groups[1]);
68+
}
69+
70+
public function test_resource_with_parent_page_data_returns_two_groups_root_first(): void
71+
{
72+
$resource = [
73+
'@id' => '/_/abstract_page_data/child-uuid',
74+
'parentPageData' => [
75+
'@id' => '/_/abstract_page_data/parent-uuid',
76+
'route' => ['@id' => '/_/routes/conference'],
77+
],
78+
];
79+
80+
$groups = $this->subject->groups($resource);
81+
82+
$this->assertCount(2, $groups);
83+
$this->assertContains('/_/abstract_page_data/parent-uuid', $groups[0]);
84+
$this->assertContains('/_/abstract_page_data/child-uuid', $groups[1]);
85+
}
86+
87+
public function test_two_level_nesting_returns_three_groups(): void
88+
{
89+
$resource = [
90+
'@id' => '/_/abstract_page_data/child-uuid',
91+
'parentPageData' => [
92+
'@id' => '/_/abstract_page_data/parent-uuid',
93+
'parentPage' => [
94+
'@id' => '/_/pages/grandparent-uuid',
95+
],
96+
],
97+
];
98+
99+
$groups = $this->subject->groups($resource);
100+
101+
$this->assertCount(3, $groups);
102+
$this->assertContains('/_/pages/grandparent-uuid', $groups[0]);
103+
$this->assertContains('/_/abstract_page_data/parent-uuid', $groups[1]);
104+
$this->assertContains('/_/abstract_page_data/child-uuid', $groups[2]);
105+
}
106+
107+
public function test_well_known_iris_are_filtered_out(): void
108+
{
109+
$resource = [
110+
'@id' => '/_/routes/home',
111+
'_metadata' => ['@id' => '/.well-known/genid/abc123'],
112+
];
113+
114+
$groups = $this->subject->groups($resource);
115+
116+
$this->assertSame([['/_/routes/home']], $groups);
117+
}
118+
119+
public function test_resource_metadata_collection_iri_is_filtered_out(): void
120+
{
121+
$resource = [
122+
'@id' => '/_/routes/home',
123+
'something' => ['@id' => '/_/resource_metadatas'],
124+
];
125+
126+
$groups = $this->subject->groups($resource);
127+
128+
$this->assertSame([['/_/routes/home']], $groups);
129+
}
130+
131+
public function test_duplicate_iris_within_depth_group_are_deduplicated(): void
132+
{
133+
$resource = [
134+
'@id' => '/_/routes/home',
135+
'items' => [
136+
['@id' => '/_/pages/abc'],
137+
['@id' => '/_/pages/abc'],
138+
],
139+
];
140+
141+
$groups = $this->subject->groups($resource);
142+
143+
$this->assertSame([['/_/routes/home', '/_/pages/abc']], $groups);
144+
}
145+
146+
public function test_nested_arrays_of_sub_resources_are_walked(): void
147+
{
148+
$resource = [
149+
'@id' => '/_/routes/home',
150+
'componentGroups' => [
151+
['@id' => '/_/component_groups/cg1'],
152+
['@id' => '/_/component_groups/cg2'],
153+
],
154+
];
155+
156+
$groups = $this->subject->groups($resource);
157+
158+
$this->assertSame([[
159+
'/_/routes/home',
160+
'/_/component_groups/cg1',
161+
'/_/component_groups/cg2',
162+
]], $groups);
163+
}
164+
165+
public function test_parent_iri_does_not_appear_in_child_group(): void
166+
{
167+
$resource = [
168+
'@id' => '/_/abstract_page_data/child-uuid',
169+
'parentPage' => ['@id' => '/_/pages/parent-uuid'],
170+
];
171+
172+
$groups = $this->subject->groups($resource);
173+
174+
$this->assertNotContains('/_/pages/parent-uuid', $groups[1] ?? []);
175+
$this->assertContains('/_/pages/parent-uuid', $groups[0]);
176+
}
177+
}

0 commit comments

Comments
 (0)