packages/requirements/tests/Unit/Config/LinkValidatorTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Config;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Small;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use Requirements\Config\LinkValidator;
12use Requirements\Input\InvalidInputException;
13use Requirements\Model\Item;
14use Requirements\Model\TestReference;
15use Requirements\Test\RunnerConfig;
16
17#[CoversClass(LinkValidator::class)]
18#[UsesClass(Item::class)]
19#[UsesClass(RunnerConfig::class)]
20#[UsesClass(TestReference::class)]
21#[Small]
22final class LinkValidatorTest extends TestCase
23{
24    public function testValidateAcceptsResolvedLinks(): void
25    {
26        $items = [
27            'REQ-001' => new Item('REQ-001', 'requirement', 'Names start with a letter.', 'supported', null, [], [], [], [], [], '', 'sourced', '', 'a.yaml', []),
28            'SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [new TestReference('unit', 'Test::testPass')], ['REQ-001'], ['SPEC-002'], [], '', 'sourced', '', 'b.yaml', []),
29            'SPEC-002' => new Item('SPEC-002', 'specification', 'The parser shall accept digits.', 'supported', null, [], [], [], ['SPEC-001', 'REQ-001'], [], '', 'original', 'Design choice.', 'b.yaml', []),
30        ];
31        (new LinkValidator())->validate($items, ['unit' => new RunnerConfig('phpunit', ['phpunit'], '/project')]);
32        $this->addToAssertionCount(1);
33    }
34
35    public function testValidateAcceptsNoItems(): void
36    {
37        (new LinkValidator())->validate([], []);
38        $this->addToAssertionCount(1);
39    }
40
41    public function testValidateRejectsMissingRequirement(): void
42    {
43        $items = ['SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [], ['MISSING'], [], [], '', 'sourced', '', 'b.yaml', [])];
44        $this->expectException(InvalidInputException::class);
45        $this->expectExceptionMessage("SPEC-001: missing or self reference 'MISSING'.");
46        (new LinkValidator())->validate($items, []);
47    }
48
49    public function testValidateRejectsMissingRelatedItem(): void
50    {
51        $items = ['SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [], [], ['SPEC-404'], [], '', 'sourced', '', 'b.yaml', [])];
52        $this->expectException(InvalidInputException::class);
53        $this->expectExceptionMessage("SPEC-001: missing or self reference 'SPEC-404'.");
54        (new LinkValidator())->validate($items, []);
55    }
56
57    public function testValidateRejectsSelfRelation(): void
58    {
59        $items = ['SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [], [], ['SPEC-001'], [], '', 'sourced', '', 'b.yaml', [])];
60        $this->expectException(InvalidInputException::class);
61        $this->expectExceptionMessage("SPEC-001: missing or self reference 'SPEC-001'.");
62        (new LinkValidator())->validate($items, []);
63    }
64
65    public function testValidateRejectsSelfRequirement(): void
66    {
67        $items = ['REQ-001' => new Item('REQ-001', 'requirement', 'Names start with a letter.', 'supported', null, [], [], ['REQ-001'], [], [], '', 'sourced', '', 'a.yaml', [])];
68        $this->expectException(InvalidInputException::class);
69        $this->expectExceptionMessage("REQ-001: missing or self reference 'REQ-001'.");
70        (new LinkValidator())->validate($items, []);
71    }
72
73    public function testValidateRejectsRequirementThatIsSpecification(): void
74    {
75        $items = [
76            'SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [], ['SPEC-002'], [], [], '', 'sourced', '', 'b.yaml', []),
77            'SPEC-002' => new Item('SPEC-002', 'specification', 'The parser shall accept digits.', 'supported', null, [], [], [], [], [], '', 'sourced', '', 'b.yaml', []),
78        ];
79        $this->expectException(InvalidInputException::class);
80        $this->expectExceptionMessage("SPEC-001: 'SPEC-002' must be a sourced requirement.");
81        (new LinkValidator())->validate($items, []);
82    }
83
84    public function testValidateRejectsRequirementThatIsNotSourced(): void
85    {
86        $items = [
87            'SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [], ['REQ-001'], [], [], '', 'sourced', '', 'b.yaml', []),
88            'REQ-001' => new Item('REQ-001', 'requirement', 'Names start with a letter.', 'supported', null, [], [], [], [], [], '', 'original', 'Local rule.', 'a.yaml', []),
89        ];
90        $this->expectException(InvalidInputException::class);
91        $this->expectExceptionMessage("SPEC-001: 'REQ-001' must be a sourced requirement.");
92        (new LinkValidator())->validate($items, []);
93    }
94
95    public function testValidateRejectsUnknownRunner(): void
96    {
97        $items = ['SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [new TestReference('unit', 'Test::testPass'), new TestReference('missing', 'Test::testPass')], [], [], [], '', 'sourced', '', 'b.yaml', [])];
98        $this->expectException(InvalidInputException::class);
99        $this->expectExceptionMessage("SPEC-001: unknown runner 'missing'.");
100        (new LinkValidator())->validate($items, ['unit' => new RunnerConfig('phpunit', ['phpunit'], '/project')]);
101    }
102
103    public function testValidateRejectsRunnerNamedByExtension(): void
104    {
105        $items = ['SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [new TestReference('phpunit', 'Test::testPass')], [], [], [], '', 'sourced', '', 'b.yaml', [])];
106        $this->expectException(InvalidInputException::class);
107        $this->expectExceptionMessage("SPEC-001: unknown runner 'phpunit'.");
108        (new LinkValidator())->validate($items, ['unit' => new RunnerConfig('phpunit', ['phpunit'], '/project')]);
109    }
110
111    public function testValidateChecksEveryItem(): void
112    {
113        $items = [
114            'SPEC-001' => new Item('SPEC-001', 'specification', 'The parser shall require a leading letter.', 'supported', null, [], [], [], [], [], '', 'sourced', '', 'b.yaml', []),
115            'SPEC-002' => new Item('SPEC-002', 'specification', 'The parser shall accept digits.', 'supported', null, [], [], [], ['SPEC-003'], [], '', 'sourced', '', 'b.yaml', []),
116        ];
117        $this->expectException(InvalidInputException::class);
118        $this->expectExceptionMessage("SPEC-002: missing or self reference 'SPEC-003'.");
119        (new LinkValidator())->validate($items, []);
120    }
121}
122