packages/sql-catalog/tests/Unit/Reporter/Html/CatalogStatisticsTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Reporter\Html;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use SqlCatalog\Core\Catalog\CallSite;
11use SqlCatalog\Core\Catalog\Catalog;
12use SqlCatalog\Core\Catalog\CatalogEntry;
13use SqlCatalog\Core\Catalog\Finding;
14use SqlCatalog\Core\Catalog\FindingRule;
15use SqlCatalog\Core\Catalog\Resolution;
16use SqlCatalog\Core\Catalog\Severity;
17use SqlCatalog\Core\Sql\StatementKind;
18use SqlCatalog\Core\Text\Origin;
19use SqlCatalog\Core\Text\TextHole;
20use SqlCatalog\Core\Text\TextPattern;
21use SqlCatalog\Core\Type\TypeShape;
22use SqlCatalog\Reporter\Html\CatalogStatistics;
23
24#[CoversClass(CatalogStatistics::class)]
25#[UsesClass(CallSite::class)]
26#[UsesClass(Catalog::class)]
27#[UsesClass(CatalogEntry::class)]
28#[UsesClass(Finding::class)]
29#[UsesClass(FindingRule::class)]
30#[UsesClass(Resolution::class)]
31#[UsesClass(Severity::class)]
32#[UsesClass(Origin::class)]
33#[UsesClass(TextHole::class)]
34#[UsesClass(TextPattern::class)]
35#[UsesClass(TypeShape::class)]
36#[UsesClass(StatementKind::class)]
37#[UsesClass(\SqlCatalog\Core\Text\LiteralText::class)]
38final class CatalogStatisticsTest extends TestCase
39{
40 public function testStatementsCountsWhatTheCatalogHolds(): void
41 {
42 $catalog = new Catalog([
43 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), ['t'], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
44 ]);
45
46 self::assertSame(1, (new CatalogStatistics($catalog))->statements());
47 }
48
49 public function testResolvedCountsTheStatementsWithNoGaps(): void
50 {
51 $catalog = new Catalog([
52 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
53 new CatalogEntry('b', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::External, TypeShape::unknown())), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
54 ]);
55
56 self::assertSame(1, (new CatalogStatistics($catalog))->resolved());
57 }
58
59 public function testOpenCountsTheStatementsWhoseSearchDidNotClose(): void
60 {
61 $catalog = new Catalog([
62 new CatalogEntry('a', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::External, TypeShape::unknown())), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
63 new CatalogEntry('b', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::Budget, TypeShape::unknown())), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
64 ]);
65
66 self::assertSame(1, (new CatalogStatistics($catalog))->open());
67 }
68
69 public function testFindingsCountsEveryFindingOfEveryStatement(): void
70 {
71 $catalog = new Catalog([
72 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
73 Finding::of(FindingRule::DynamicSql, 'one'),
74 Finding::of(FindingRule::ExternalInput, 'two'),
75 ]),
76 ]);
77
78 self::assertSame(2, (new CatalogStatistics($catalog))->findings());
79 }
80
81 public function testAtLeastCountsTheStatementsWorthLookingAt(): void
82 {
83 $catalog = new Catalog([
84 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
85 Finding::of(FindingRule::ExternalInput, 'one'),
86 ]),
87 new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), [
88 Finding::of(FindingRule::AnalysisIncomplete, 'two'),
89 ]),
90 ]);
91
92 self::assertSame(1, (new CatalogStatistics($catalog))->atLeast(Severity::High));
93 }
94
95 public function testByResolutionAccountsForEveryStatementUnderEveryResolution(): void
96 {
97 $catalog = new Catalog([
98 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
99 ]);
100
101 self::assertSame(
102 ['resolved' => 1, 'external-input' => 0, 'incomplete-model' => 0, 'incomplete' => 0, 'not-analyzed' => 0],
103 (new CatalogStatistics($catalog))->byResolution(),
104 );
105 }
106
107 public function testByKindCountsTheKindsMostFirst(): void
108 {
109 $catalog = new Catalog([
110 new CatalogEntry('a', StatementKind::Insert, TextPattern::fromText('INSERT INTO t VALUES (1)'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
111 new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
112 new CatalogEntry('c', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 3, 'f', 'pdo.query'), []),
113 ]);
114
115 self::assertSame(['select' => 2, 'insert' => 1], (new CatalogStatistics($catalog))->byKind());
116 }
117
118 public function testByRuleCountsTheFindingsMostFirst(): void
119 {
120 $catalog = new Catalog([
121 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
122 Finding::of(FindingRule::DynamicSql, 'one'),
123 Finding::of(FindingRule::ExternalInput, 'two'),
124 ]),
125 new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), [
126 Finding::of(FindingRule::DynamicSql, 'three'),
127 ]),
128 ]);
129
130 self::assertSame(['dynamic-sql' => 2, 'external-input' => 1], (new CatalogStatistics($catalog))->byRule());
131 }
132
133 public function testTablesSayHowEachOneIsUsed(): void
134 {
135 $catalog = new Catalog([
136 new CatalogEntry('a', StatementKind::Select, TextPattern::fromText('SELECT 1'), ['posts'], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
137 new CatalogEntry('b', StatementKind::Insert, TextPattern::fromText('INSERT'), ['posts'], [], new CallSite('a.php', 2, 'f', 'pdo.query'), []),
138 new CatalogEntry('c', StatementKind::Select, TextPattern::fromText('SELECT 2'), ['users'], [], new CallSite('a.php', 3, 'f', 'pdo.query'), []),
139 ]);
140
141 self::assertSame(
142 [
143 ['name' => 'posts', 'reads' => 1, 'writes' => 1, 'statements' => 2],
144 ['name' => 'users', 'reads' => 1, 'writes' => 0, 'statements' => 1],
145 ],
146 (new CatalogStatistics($catalog))->tables(),
147 );
148 }
149
150 public function testFilesSayWhatWasFoundInEachOfThem(): void
151 {
152 $catalog = new Catalog([
153 new CatalogEntry('a', StatementKind::Select, TextPattern::fromHole(new TextHole(Origin::Budget, TypeShape::unknown())), [], [], new CallSite('b.php', 1, 'f', 'pdo.query'), [
154 Finding::of(FindingRule::AnalysisIncomplete, 'one'),
155 ]),
156 new CatalogEntry('b', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), []),
157 ]);
158
159 self::assertSame(
160 [
161 'a.php' => ['statements' => 1, 'open' => 0, 'findings' => 0],
162 'b.php' => ['statements' => 1, 'open' => 1, 'findings' => 1],
163 ],
164 (new CatalogStatistics($catalog))->files(),
165 );
166 }
167
168 public function testFlaggedListsTheReportedStatementsWorstFirst(): void
169 {
170 $catalog = new Catalog([
171 new CatalogEntry('low', StatementKind::Select, TextPattern::fromText('SELECT 1'), [], [], new CallSite('a.php', 1, 'f', 'pdo.query'), [
172 Finding::of(FindingRule::AnalysisIncomplete, 'one'),
173 ]),
174 new CatalogEntry('high', StatementKind::Select, TextPattern::fromText('SELECT 2'), [], [], new CallSite('a.php', 2, 'f', 'pdo.query'), [
175 Finding::of(FindingRule::ExternalInput, 'two'),
176 ]),
177 new CatalogEntry('none', StatementKind::Select, TextPattern::fromText('SELECT 3'), [], [], new CallSite('a.php', 3, 'f', 'pdo.query'), []),
178 ]);
179
180 self::assertSame(['high', 'low'], array_column((new CatalogStatistics($catalog))->flagged(), 'id'));
181 }
182}
183