-
Notifications
You must be signed in to change notification settings - Fork 0
test: enforce exhaustive scenario contracts #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
eba8b34
test: add missing negative rule contracts
jordyloeuille c6e578d
test: include rule coverage contracts in smoke matrix
jordyloeuille d031103
test: enforce public rule scenario coverage
jordyloeuille 708c822
test: lock finite operation catalog contracts
jordyloeuille 0451256
test: add catalog scanner helper
jordyloeuille f72d6f1
test: exhaust finite cache operation catalogs
jordyloeuille f950db0
test: exhaust finite Redis operation catalogs
jordyloeuille 244cc0d
test: exhaust finite database mutation catalogs
jordyloeuille d6cb8e9
test: cover every analyzer integrity diagnostic
jordyloeuille 035b8df
test: add negative cache mutation control
jordyloeuille 3f08f84
docs: define scenario completeness contract
jordyloeuille 5ce38b2
docs: record exhaustive scenario contracts
jordyloeuille 75c278e
test: skip TG903 permission case when privileges bypass chmod
jordyloeuille File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Codegenie\TransactionGuard\Analysis\AnalysisConfig; | ||
| use Codegenie\TransactionGuard\Analysis\ClassMetadataIndex; | ||
| use Codegenie\TransactionGuard\Analysis\SourceScanner; | ||
| use Codegenie\TransactionGuard\Tests\Support\CatalogScanner; | ||
| use Codegenie\TransactionGuard\TransactionGuard; | ||
|
|
||
| it('reports TG900 when a requested source file cannot be read', function (): void { | ||
| $missing = sys_get_temp_dir().DIRECTORY_SEPARATOR.'transaction-guard-missing-'.bin2hex(random_bytes(4)).'.php'; | ||
| $scanner = new SourceScanner(ClassMetadataIndex::fromFiles([])); | ||
| $findings = $scanner->scan($missing); | ||
|
|
||
| expect($findings)->toHaveCount(1) | ||
| ->and($findings[0]->rule)->toBe('TG900'); | ||
| }); | ||
|
|
||
| it('reports TG901 for invalid PHP syntax', function (): void { | ||
| $findings = CatalogScanner::scan('<?php function broken( {'); | ||
|
|
||
| expect($findings)->toHaveCount(1) | ||
| ->and($findings[0]->rule)->toBe('TG901'); | ||
| }); | ||
|
|
||
| it('reports TG902 when a valid analyzer regex fails at runtime', function (): void { | ||
| $originalLimit = ini_get('pcre.backtrack_limit'); | ||
| ini_set('pcre.backtrack_limit', '1'); | ||
| $tail = str_repeat('a', 128).'Y'; | ||
| $source = "<?php\nuse Illuminate\\Support\\Facades\\DB;\nDB::transaction(function () { CustomGateway::noop(); });\n/* {$tail} */\n"; | ||
|
|
||
| try { | ||
| $findings = CatalogScanner::scan($source, new AnalysisConfig( | ||
| customSideEffectPatterns: ['/(a+)+X/'], | ||
| )); | ||
| } finally { | ||
| if (is_string($originalLimit)) { | ||
| ini_set('pcre.backtrack_limit', $originalLimit); | ||
| } | ||
| } | ||
|
|
||
| $rules = array_map(static fn ($finding): string => $finding->rule, $findings); | ||
| expect($rules)->toContain('TG902'); | ||
| }); | ||
|
|
||
| it('reports TG903 when a requested source subtree cannot be traversed', function (): void { | ||
| if (DIRECTORY_SEPARATOR === '\\') { | ||
| $this->markTestSkipped('POSIX permission semantics are required for deterministic unreadable-directory coverage.'); | ||
| } | ||
|
|
||
| $root = sys_get_temp_dir().DIRECTORY_SEPARATOR.'transaction-guard-unreadable-'.bin2hex(random_bytes(4)); | ||
| $blocked = $root.DIRECTORY_SEPARATOR.'blocked'; | ||
| $hidden = $blocked.DIRECTORY_SEPARATOR.'Hidden.php'; | ||
| mkdir($blocked, 0777, true); | ||
| file_put_contents($hidden, '<?php'); | ||
| chmod($blocked, 0000); | ||
|
|
||
| if (is_readable($blocked)) { | ||
| @chmod($blocked, 0777); | ||
| @unlink($hidden); | ||
| @rmdir($blocked); | ||
| @rmdir($root); | ||
| $this->markTestSkipped('The current process can bypass directory permission restrictions.'); | ||
| } | ||
|
|
||
| try { | ||
| $result = (new TransactionGuard(new AnalysisConfig))->analyze([$root]); | ||
| $rules = array_map(static fn ($finding): string => $finding->rule, $result->diagnostics); | ||
|
|
||
| expect($rules)->toContain('TG903'); | ||
| } finally { | ||
| @chmod($blocked, 0777); | ||
| @unlink($hidden); | ||
| @rmdir($blocked); | ||
| @rmdir($root); | ||
| } | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Codegenie\TransactionGuard\Analysis\OperationCatalog; | ||
| use Codegenie\TransactionGuard\Tests\Support\CatalogScanner; | ||
|
|
||
| it('detects every catalogued cache mutation', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "Cache::{$method}('key', 'value');", | ||
| OperationCatalog::CACHE_MUTATIONS, | ||
| ); | ||
| $source = "<?php\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\DB;\nDB::transaction(function () {\n".implode("\n", $statements)."\n});\n"; | ||
| $snippets = array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter(CatalogScanner::scan($source), static fn ($finding): bool => $finding->rule === 'TG008')), | ||
| ); | ||
| $joined = implode("\n", $snippets); | ||
|
|
||
| foreach (OperationCatalog::CACHE_MUTATIONS as $method) { | ||
| expect($joined, $method)->toContain("Cache::{$method}("); | ||
| } | ||
| }); | ||
|
|
||
| it('detects every catalogued cache lock terminal', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "Cache::lock('key')->{$method}('value');", | ||
| OperationCatalog::CACHE_LOCK_TERMINALS, | ||
| ); | ||
| $source = "<?php\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\DB;\nDB::transaction(function () {\n".implode("\n", $statements)."\n});\n"; | ||
| $joined = implode("\n", array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter(CatalogScanner::scan($source), static fn ($finding): bool => $finding->rule === 'TG008')), | ||
| )); | ||
|
|
||
| foreach (OperationCatalog::CACHE_LOCK_TERMINALS as $method) { | ||
| expect($joined, $method)->toContain("->{$method}("); | ||
| } | ||
| }); | ||
|
|
||
| it('detects every catalogued rate limiter mutation', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "RateLimiter::{$method}('key', 'value');", | ||
| OperationCatalog::RATE_LIMITER_MUTATIONS, | ||
| ); | ||
| $source = "<?php\nuse Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Facades\\RateLimiter;\nDB::transaction(function () {\n".implode("\n", $statements)."\n});\n"; | ||
| $joined = implode("\n", array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter(CatalogScanner::scan($source), static fn ($finding): bool => $finding->rule === 'TG008')), | ||
| )); | ||
|
|
||
| foreach (OperationCatalog::RATE_LIMITER_MUTATIONS as $method) { | ||
| expect($joined, $method)->toContain("RateLimiter::{$method}("); | ||
| } | ||
| }); | ||
|
|
||
| it('keeps representative cache reads clean', function (): void { | ||
| $source = <<<'PHP' | ||
| <?php | ||
| use Illuminate\Support\Facades\Cache; | ||
| use Illuminate\Support\Facades\DB; | ||
| DB::transaction(function () { | ||
| Cache::get('key'); | ||
| Cache::has('key'); | ||
| Cache::many(['a', 'b']); | ||
| }); | ||
| PHP; | ||
|
|
||
| $rules = array_map(static fn ($finding): string => $finding->rule, CatalogScanner::scan($source)); | ||
|
|
||
| expect($rules)->not->toContain('TG008'); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Codegenie\TransactionGuard\Analysis\AnalysisConfig; | ||
| use Codegenie\TransactionGuard\Analysis\OperationCatalog; | ||
| use Codegenie\TransactionGuard\Tests\Support\CatalogScanner; | ||
|
|
||
| it('detects every catalogued cross-connection query mutation', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "DB::connection('pgsql')->table('audit')->{$method}(['value' => 1]);", | ||
| OperationCatalog::QUERY_MUTATIONS, | ||
| ); | ||
| $source = "<?php\nuse Illuminate\\Support\\Facades\\DB;\nDB::connection('mysql')->transaction(function () {\n".implode("\n", $statements)."\n});\n"; | ||
| $joined = implode("\n", array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter( | ||
| CatalogScanner::scan($source, new AnalysisConfig(defaultDatabaseConnection: 'mysql')), | ||
| static fn ($finding): bool => $finding->rule === 'TG021', | ||
| )), | ||
| )); | ||
|
|
||
| foreach (OperationCatalog::QUERY_MUTATIONS as $method) { | ||
| expect($joined, $method)->toContain("->{$method}("); | ||
| } | ||
| }); | ||
|
|
||
| it('detects every catalogued cross-connection Eloquent static mutation', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "Audit::{$method}(['value' => 1]);", | ||
| OperationCatalog::ELOQUENT_STATIC_MUTATIONS, | ||
| ); | ||
| $source = "<?php\nnamespace App\\Models;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Facades\\DB;\nclass Audit extends Model { protected \$connection = 'pgsql'; }\nDB::connection('mysql')->transaction(function () {\n".implode("\n", $statements)."\n});\n"; | ||
| $joined = implode("\n", array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter( | ||
| CatalogScanner::scan($source, new AnalysisConfig(defaultDatabaseConnection: 'mysql')), | ||
| static fn ($finding): bool => $finding->rule === 'TG021', | ||
| )), | ||
| )); | ||
|
|
||
| foreach (OperationCatalog::ELOQUENT_STATIC_MUTATIONS as $method) { | ||
| expect($joined, $method)->toContain("Audit::{$method}("); | ||
| } | ||
| }); | ||
|
|
||
| it('detects every catalogued cross-connection Eloquent instance mutation', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "\$audit->{$method}(['value' => 1]);", | ||
| OperationCatalog::ELOQUENT_INSTANCE_MUTATIONS, | ||
| ); | ||
| $source = "<?php\nnamespace App\\Models;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Facades\\DB;\nclass Audit extends Model { protected \$connection = 'pgsql'; }\nDB::connection('mysql')->transaction(function () {\n\$audit = new Audit;\n".implode("\n", $statements)."\n});\n"; | ||
| $joined = implode("\n", array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter( | ||
| CatalogScanner::scan($source, new AnalysisConfig(defaultDatabaseConnection: 'mysql')), | ||
| static fn ($finding): bool => $finding->rule === 'TG021', | ||
| )), | ||
| )); | ||
|
|
||
| foreach (OperationCatalog::ELOQUENT_INSTANCE_MUTATIONS as $method) { | ||
| expect($joined, $method)->toContain("->{$method}("); | ||
| } | ||
| }); | ||
|
|
||
| it('detects every catalogued cross-connection relation mutation', function (): void { | ||
| $statements = array_map( | ||
| static fn (string $method): string => "\$user->roles()->{$method}(['value' => 1]);", | ||
| OperationCatalog::RELATION_MUTATIONS, | ||
| ); | ||
| $source = "<?php\nnamespace App\\Models;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Facades\\DB;\nclass Role extends Model { protected \$connection = 'pgsql'; }\nclass User extends Model { protected \$connection = 'mysql'; public function roles() { return \$this->belongsToMany(Role::class); } }\nDB::connection('mysql')->transaction(function () {\n\$user = new User;\n".implode("\n", $statements)."\n});\n"; | ||
| $joined = implode("\n", array_map( | ||
| static fn ($finding): string => $finding->snippet, | ||
| array_values(array_filter( | ||
| CatalogScanner::scan($source, new AnalysisConfig(defaultDatabaseConnection: 'mysql')), | ||
| static fn ($finding): bool => $finding->rule === 'TG021', | ||
| )), | ||
| )); | ||
|
|
||
| foreach (OperationCatalog::RELATION_MUTATIONS as $method) { | ||
| expect($joined, $method)->toContain("->{$method}("); | ||
| } | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.