packages/requirements/tests/Unit/Source/JsonSourceTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Source;
6
7use JsonException;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\DataProvider;
10use PHPUnit\Framework\Attributes\Small;
11use PHPUnit\Framework\Attributes\UsesClass;
12use PHPUnit\Framework\TestCase;
13use Requirements\Model\Source;
14use Requirements\Source\JsonPath;
15use Requirements\Source\JsonSource;
16use Requirements\Source\LocalFile;
17use Requirements\Source\ResourceLoader;
18use Requirements\Source\ResourceLocation;
19use Requirements\Source\Unit;
20use RuntimeException;
21use Tests\Fake\ProjectDirectory;
22use Tests\Fake\SourceDocuments;
23
24#[CoversClass(JsonSource::class)]
25#[UsesClass(JsonPath::class)]
26#[UsesClass(LocalFile::class)]
27#[UsesClass(ResourceLoader::class)]
28#[UsesClass(ResourceLocation::class)]
29#[UsesClass(Source::class)]
30#[UsesClass(Unit::class)]
31#[Small]
32final class JsonSourceTest extends TestCase
33{
34 /**
35 * @throws JsonException
36 */
37 #[DataProvider('providerSelectStableDistinctUnits')]
38 public function testSelectStableDistinctUnits(string $selector, int $count): void
39 {
40 $project = new ProjectDirectory();
41 $project->put('source.json', SourceDocuments::JSON);
42 $source = new Source('test', 'source.json', 'json', $selector);
43 $extension = new JsonSource();
44 $units = $extension->select($source, $selector, $project->directory, false);
45
46 self::assertCount($count, $units);
47 self::assertCount($count, array_unique(array_map(static fn (Unit $unit): string => $unit->location, $units)));
48 self::assertSame($units[0]->location, $extension->select($source, $selector, $project->directory, false)[0]->location);
49 }
50
51 /**
52 * @return array<string, array{string, int}>
53 */
54 public static function providerSelectStableDistinctUnits(): array
55 {
56 return [
57 'wildcard' => ['$.rules[*].text', 3],
58 'recursive' => ['$..text', 3],
59 'quoted keys' => ["$['rules'][1]['text']", 1],
60 ];
61 }
62
63 /**
64 * @param list<Unit> $expected
65 * @throws JsonException
66 */
67 #[DataProvider('providerSelectUnits')]
68 public function testSelectLocatesUnitsByJsonPointer(string $document, string $selector, array $expected): void
69 {
70 $project = new ProjectDirectory();
71 $project->put('source.json', $document);
72
73 self::assertEquals($expected, (new JsonSource())->select(new Source('test', 'source.json', 'json', $selector), $selector, $project->directory, false));
74 }
75
76 /**
77 * @return array<string, array{string, string, list<Unit>}>
78 */
79 public static function providerSelectUnits(): array
80 {
81 return [
82 'strings' => [SourceDocuments::JSON, '$.rules[*].text', [
83 new Unit('json:/rules/0/text', 'First rule.'),
84 new Unit('json:/rules/1/text', 'First rule.'),
85 new Unit('json:/rules/2/text', 'Third rule.'),
86 ]],
87 'each path once' => [SourceDocuments::JSON, '$..*..text', [
88 new Unit('json:/rules/0/text', 'First rule.'),
89 new Unit('json:/rules/1/text', 'First rule.'),
90 new Unit('json:/rules/2/text', 'Third rule.'),
91 ]],
92 'normalized string' => ['{"text":" First\n\trule. "}', '$.text', [new Unit('json:/text', 'First rule.')]],
93 'number' => ['{"n":1.5}', '$.n', [new Unit('json:/n', '1.5')]],
94 'boolean' => ['{"b":true}', '$.b', [new Unit('json:/b', 'true')]],
95 'null' => ['{"z":null}', '$.z', [new Unit('json:/z', 'null')]],
96 'object' => ['{"o":{"url":"a/b","name":"日本"}}', '$.o', [new Unit('json:/o', '{"url":"a/b","name":"日本"}')]],
97 'empty object' => ['{"o":{}}', '$.o', [new Unit('json:/o', '{}')]],
98 'array' => ['{"a":[1,"x"]}', '$.a', [new Unit('json:/a', '[1,"x"]')]],
99 'nothing selected' => [SourceDocuments::JSON, '$.missing', []],
100 ];
101 }
102
103 /**
104 * @throws JsonException
105 */
106 public function testSelectRejectsUnsupportedJsonPathInsteadOfSilentlySelecting(): void
107 {
108 $project = new ProjectDirectory();
109 $project->put('source.json', SourceDocuments::JSON);
110 $source = new Source('test', 'source.json', 'json', '$.rules[?(@.text)]');
111
112 $this->expectException(RuntimeException::class);
113 (new JsonSource())->select($source, $source->selector, $project->directory, false);
114 }
115
116 /**
117 * @throws JsonException
118 */
119 public function testSelectRejectsInvalidJson(): void
120 {
121 $project = new ProjectDirectory();
122 $project->put('source.json', '{"rules":');
123
124 $this->expectException(JsonException::class);
125 (new JsonSource())->select(new Source('test', 'source.json', 'json', '$'), '$', $project->directory, false);
126 }
127
128 /**
129 * @throws JsonException
130 */
131 public function testSelectAcceptsTheNestingLimit(): void
132 {
133 $project = new ProjectDirectory();
134 $project->put('deep.json', str_repeat('[', 511) . str_repeat(']', 511));
135
136 self::assertCount(1, (new JsonSource())->select(new Source('test', 'deep.json', 'json', '$[0]'), '$[0]', $project->directory, false));
137 }
138
139 /**
140 * @throws JsonException
141 */
142 public function testSelectRejectsDocumentsNestedBeyondTheLimit(): void
143 {
144 $project = new ProjectDirectory();
145 $project->put('deep.json', str_repeat('[', 512) . str_repeat(']', 512));
146
147 $this->expectException(JsonException::class);
148 (new JsonSource())->select(new Source('test', 'deep.json', 'json', '$[0]'), '$[0]', $project->directory, false);
149 }
150
151 /**
152 * @throws JsonException
153 */
154 public function testSelectReadsThroughTheGivenLoader(): void
155 {
156 $project = new ProjectDirectory();
157 $project->put('source.json', SourceDocuments::JSON);
158 $loader = new ResourceLoader();
159 $loader->fetch($project->path('source.json'));
160 $project->put('source.json', '{"other":"Changed."}');
161
162 self::assertEquals([new Unit('json:/other', 'Outside scope.')], (new JsonSource($loader))->select(new Source('test', 'source.json', 'json', '$.other'), '$.other', $project->directory, false));
163 }
164}
165