Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ Entries below `Unreleased` are written by CI from the GitHub release body — se

### Changed

- **Test credentials are generated at run time.** Passwords and secrets the suite only needed as
arbitrary values are now `TestCredentials::password()` / `TestCredentials::secret()`, so no
literal a secret scanner would report reaches the repository, and
`tests/Architecture/NoLiteralCredentialsTest.php` fails on any literal added later. Inputs whose
exact value is what a rule test proves (too short, missing one character class) keep their literal
and are marked exempt with a reason. The `laranail/package-tools` floor is now `^0.1.10`, and a
second check keeps every purpose passed to `TestCredentials` a single plain word, since a secret
scanner reads a compound purpose as the password itself.
- `scripts/verify-tag-currency.sh` passes a tag that is behind `main` only by `.github/`-only commits (a Dependabot bump ships nothing), and tells a package on real releases to cut a patch rather than move a published tag.
- `PostIncDecToPreIncDecRector` is skipped. Rector rewrote `$i++` to `++$i` in 21 files and Pint's
`increment_style` rewrote every one of them back, so the two gates could never both pass.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"illuminate/translation": "^13.0",
"illuminate/validation": "^13.0",
"laranail/console": "^0.1",
"laranail/package-tools": "^0.1.3",
"laranail/package-tools": "^0.1.10",
"laravel/framework": "^13.0",
"symfony/process": "^7.4 || ^8.0"
},
Expand Down
17 changes: 17 additions & 0 deletions tests/Architecture/NoLiteralCredentialsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

use Simtabi\Laranail\Package\Tools\Testing\AssertsNoLiteralCredentials;

uses(AssertsNoLiteralCredentials::class);

it('keeps literal credentials out of the tests', function (): void {
$this->assertNoLiteralCredentialsIn([dirname(__DIR__)], minFiles: 143);
expect(true)->toBeTrue();
});

it('names every generated credential with a single word', function (): void {
$this->assertCredentialPurposesAreWordsIn([dirname(__DIR__)], minFiles: 143);
expect(true)->toBeTrue();
});
13 changes: 7 additions & 6 deletions tests/FastCheckParityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Facades\Validator;
use Simtabi\Laranail\Validation\FastCheckCompiler;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

/**
* Parity suite: for every fast-checkable rule, the compiled closure's
Expand Down Expand Up @@ -396,27 +397,27 @@ function itemAwareConfirmedParityGrid(): iterable
$cases = [
'default match' => [
'required|confirmed', 'password',
'hunter2', ['password' => 'hunter2', 'password_confirmation' => 'hunter2'],
TestCredentials::password('confirmed'), ['password' => TestCredentials::password('confirmed'), 'password_confirmation' => TestCredentials::password('confirmed')],
],
'default mismatch' => [
'required|confirmed', 'password',
'hunter2', ['password' => 'hunter2', 'password_confirmation' => 'hunter3'],
TestCredentials::password('confirmed'), ['password' => TestCredentials::password('confirmed'), 'password_confirmation' => TestCredentials::password('mismatch')],
],
'default confirmation missing' => [
'required|confirmed', 'password',
'hunter2', ['password' => 'hunter2'],
TestCredentials::password('confirmed'), ['password' => TestCredentials::password('confirmed')],
],
'default confirmation null' => [
'required|confirmed', 'password',
'hunter2', ['password' => 'hunter2', 'password_confirmation' => null],
TestCredentials::password('confirmed'), ['password' => TestCredentials::password('confirmed'), 'password_confirmation' => null],
],
'custom name match' => [
'required|confirmed:check', 'pwd',
'hunter2', ['pwd' => 'hunter2', 'check' => 'hunter2'],
TestCredentials::password('confirmed'), ['pwd' => TestCredentials::password('confirmed'), 'check' => TestCredentials::password('confirmed')],
],
'custom name mismatch' => [
'required|confirmed:check', 'pwd',
'hunter2', ['pwd' => 'hunter2', 'check' => 'hunter3'],
TestCredentials::password('confirmed'), ['pwd' => TestCredentials::password('confirmed'), 'check' => TestCredentials::password('mismatch')],
],
'nullable value null' => [
'nullable|confirmed', 'password',
Expand Down
9 changes: 7 additions & 2 deletions tests/FluentRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Illuminate\Validation\ValidationException;
use Illuminate\Contracts\Validation\ValidationRule;
use Simtabi\Laranail\Validation\Builder\Nodes\StringRule;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Simtabi\Laranail\Validation\Tests\Fixtures\TestIntEnum;
use Simtabi\Laranail\Validation\Tests\Fixtures\TestUnitEnum;
use Simtabi\Laranail\Validation\Tests\Fixtures\TestStringEnum;
Expand Down Expand Up @@ -487,6 +488,7 @@
);

$compiled = RuleSet::compile(['password' => $stringRule]);
// @credential-literal-exempt: shorter than the min(12) the admin branch applies
$validator = makeValidator(['role' => 'admin', 'password' => 'short'], $compiled);
expect($validator->passes())->toBeFalse();
});
Expand All @@ -498,6 +500,7 @@
);

$compiled = RuleSet::compile(['password' => $stringRule]);
// @credential-literal-exempt: shorter than the min(12) the skipped branch would apply, so passing proves the skip
$validator = makeValidator(['role' => 'user', 'password' => 'short'], $compiled);
expect($validator->passes())->toBeTrue();
});
Expand All @@ -510,6 +513,7 @@
);

$compiled = RuleSet::compile(['password' => $stringRule]);
// @credential-literal-exempt: longer than the max(5) the default branch applies
$validator = makeValidator(['role' => 'user', 'password' => 'toolong'], $compiled);
expect($validator->passes())->toBeFalse();
});
Expand Down Expand Up @@ -763,6 +767,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void

it('uses message inside when() conditional', function (): void {
$validator = makeValidator(
// @credential-literal-exempt: shorter than the minimum length the rule under test enforces
['password' => 'short'],
['password' => FluentRule::string()->required()->when(true, fn (StringRule $r): StringRule => $r->min(12)->message('Admin passwords need 12+ chars.'))],
);
Expand Down Expand Up @@ -934,7 +939,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
});

it('validates field with prohibited', function (): void {
$v = makeValidator(['secret' => 'value'], ['secret' => FluentRule::string()->prohibited()]);
$v = makeValidator(['secret' => TestCredentials::secret('prohibited')], ['secret' => FluentRule::string()->prohibited()]);
expect($v->passes())->toBeFalse();

$v = makeValidator([], ['secret' => FluentRule::string()->prohibited()]);
Expand All @@ -945,7 +950,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
$v = makeValidator([], ['secret' => FluentRule::string()->missing()]);
expect($v->passes())->toBeTrue();

$v = makeValidator(['secret' => 'value'], ['secret' => FluentRule::string()->missing()]);
$v = makeValidator(['secret' => TestCredentials::secret('missing')], ['secret' => FluentRule::string()->missing()]);
expect($v->passes())->toBeFalse();
});

Expand Down
3 changes: 2 additions & 1 deletion tests/InlineMessageParamPhase3eTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Simtabi\Laranail\Validation\FluentRule;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

// =========================================================================
// Phase 3e — `message:` on FluentRule static factories. Every factory
Expand Down Expand Up @@ -201,7 +202,7 @@

it('FluentRule::uuid(message: ...) surfaces in live validation', function (): void {
$v = makeValidator(
['token' => 'notuuid'],
['token' => TestCredentials::secret('opaque')],
['token' => FluentRule::uuid(message: 'Must be a UUID.')],
);

Expand Down
2 changes: 2 additions & 0 deletions tests/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
// Note: Rule::when() requires an array or string, not a bare object.
// Wrap the fluent rule in an array.
$validator = makeValidator(
// @credential-literal-exempt: shorter than the minimum length the rule under test enforces
['secret' => 'short'],
['secret' => LaravelRule::when($isAdmin, [FluentRule::string()->required()->min(12)])],
);
Expand All @@ -185,6 +186,7 @@
$isAdmin = false;

$validator = makeValidator(
// @credential-literal-exempt: shorter than the min(12) the skipped branch would apply, so passing proves the skip
['secret' => 'short'],
['secret' => LaravelRule::when($isAdmin, [FluentRule::string()->required()->min(12)])],
);
Expand Down
7 changes: 4 additions & 3 deletions tests/MemoizingValidatorParityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Simtabi\Laranail\Validation\MemoizingValidator;
use Simtabi\Laranail\Validation\OptimizedValidator;
use Illuminate\Validation\Validator as BaseValidator;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

/**
* Parity suite for {@see MemoizingValidator}. The class memoizes string-rule
Expand Down Expand Up @@ -85,11 +86,11 @@ function memoParityScenarios(): iterable
['type' => 'required|in:person,company', 'vat' => 'required_if:type,company|string'],
];
yield 'required_with + same — mismatch' => [
['password' => 'secret', 'password_confirmation' => 'other'],
['password' => TestCredentials::password('confirmed'), 'password_confirmation' => TestCredentials::password('mismatch')],
['password' => 'required|string', 'password_confirmation' => 'required_with:password|same:password'],
];
yield 'confirmed — passing' => [
['password' => 'secret', 'password_confirmation' => 'secret'],
['password' => TestCredentials::password('confirmed'), 'password_confirmation' => TestCredentials::password('confirmed')],
['password' => 'required|string|confirmed'],
];
yield 'gt/lte field refs — passing' => [
Expand Down Expand Up @@ -117,7 +118,7 @@ function memoParityScenarios(): iterable
['role' => ['required', Rule::in(['admin', 'editor'])]],
];
yield 'closure rule bypass — failing' => [
['token' => 'nope'],
['token' => TestCredentials::secret('closure')],
['token' => ['required', 'string', function (string $attribute, mixed $value, Closure $fail): void {
if ($value !== 'ok') {
$fail('The :attribute is not ok.');
Expand Down
Loading
Loading