diff --git a/CHANGELOG.md b/CHANGELOG.md index 113172f..86a34ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- **Test credentials are generated at run time.** Passwords, secrets and tokens in the tests and the + workbench factory come from package-tools' `TestCredentials`, so no literal reaches the repository + for a secret scanner to report. `tests/Architecture/NoLiteralCredentialsTest.php` fails the suite + on any literal left behind. The `laranail/package-tools` floor is now `^0.1.10`, the first release + that also ships `assertCredentialPurposesAreWordsIn()`: a second test keeps every + `TestCredentials` purpose a single plain lowercase word, since a secret scanner reads a compound + purpose as the password. + - Pull requests run `composer pint` (Pint with the shared laranail config, check-only) in a new *Code style* workflow. diff --git a/composer.json b/composer.json index 992fc5c..a750247 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "laravel/fortify": "^1.38", "laravel/framework": "^13.0", "laravel/sanctum": "^4.0", - "laranail/package-tools": "^0.1" + "laranail/package-tools": "^0.1.10" }, "require-dev": { "pestphp/pest": "^4.0", diff --git a/tests/Architecture/NoLiteralCredentialsTest.php b/tests/Architecture/NoLiteralCredentialsTest.php new file mode 100644 index 0000000..3f19239 --- /dev/null +++ b/tests/Architecture/NoLiteralCredentialsTest.php @@ -0,0 +1,17 @@ +assertNoLiteralCredentialsIn([dirname(__DIR__), dirname(__DIR__, 2) . '/workbench/database/factories'], minFiles: 21); + expect(true)->toBeTrue(); +}); + +it('names every generated credential with a single word', function (): void { + $this->assertCredentialPurposesAreWordsIn([dirname(__DIR__), dirname(__DIR__, 2) . '/workbench/database/factories'], minFiles: 21); + expect(true)->toBeTrue(); +}); diff --git a/tests/Feature/AttemptEmailPasswordLoginTest.php b/tests/Feature/AttemptEmailPasswordLoginTest.php index 2b1629d..f470da3 100644 --- a/tests/Feature/AttemptEmailPasswordLoginTest.php +++ b/tests/Feature/AttemptEmailPasswordLoginTest.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Workbench\App\Models\User; use Simtabi\Laranail\AuthKit\Enums\AuthStatus; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; use Simtabi\Laranail\AuthKit\Actions\AttemptEmailPasswordLogin; function loginRequest(array $data = [], ?string $ip = null): Request @@ -58,6 +59,8 @@ function loginRequest(array $data = [], ?string $ip = null): Request it('returns failed when credentials are wrong', function (): void { $password = Str::random(16); + expect(TestCredentials::password('wrong'))->not->toBe($password); + User::factory()->create([ 'email' => 'ada@example.com', 'password' => bcrypt($password), @@ -66,7 +69,7 @@ function loginRequest(array $data = [], ?string $ip = null): Request $action = app(AttemptEmailPasswordLogin::class); $result = $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password']), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')]), guard: 'web', ); @@ -82,7 +85,7 @@ function loginRequest(array $data = [], ?string $ip = null): Request ]); $action = app(AttemptEmailPasswordLogin::class); - $request = loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password']); + $request = loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')]); $action->execute(request: $request, guard: 'web'); $result = $action->execute(request: $request, guard: 'web'); @@ -101,12 +104,12 @@ function loginRequest(array $data = [], ?string $ip = null): Request $action = app(AttemptEmailPasswordLogin::class); $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password'], ip: '10.0.0.1'), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')], ip: '10.0.0.1'), guard: 'web', ); $result = $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password'], ip: '10.0.0.2'), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')], ip: '10.0.0.2'), guard: 'web', ); @@ -125,12 +128,12 @@ function loginRequest(array $data = [], ?string $ip = null): Request $action = app(AttemptEmailPasswordLogin::class); $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password'], ip: '10.0.0.1'), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')], ip: '10.0.0.1'), guard: 'web', ); $result = $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password'], ip: '10.0.0.1'), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')], ip: '10.0.0.1'), guard: 'web', ); @@ -149,8 +152,10 @@ function loginRequest(array $data = [], ?string $ip = null): Request $action = app(AttemptEmailPasswordLogin::class); + expect(TestCredentials::password('wrong'))->not->toBe($password); + $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password']), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')]), guard: 'web', ); @@ -162,7 +167,7 @@ function loginRequest(array $data = [], ?string $ip = null): Request expect($result->isPassed())->toBeTrue(); $afterResult = $action->execute( - request: loginRequest(['email' => 'ada@example.com', 'password' => 'wrong-password']), + request: loginRequest(['email' => 'ada@example.com', 'password' => TestCredentials::password('wrong')]), guard: 'web', ); diff --git a/tests/Feature/CreateNewUserTest.php b/tests/Feature/CreateNewUserTest.php index 40137a0..2372cad 100644 --- a/tests/Feature/CreateNewUserTest.php +++ b/tests/Feature/CreateNewUserTest.php @@ -7,17 +7,18 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rules\Password; use Simtabi\Laranail\AuthKit\Actions\CreateNewUser; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; it(description: 'creates a user with a hashed password', closure: function (): void { $user = app(abstract: CreateNewUser::class)->create(input: [ 'name' => 'Ada Lovelace', 'email' => 'ADA@EXAMPLE.COM', - 'password' => 'password', - 'password_confirmation' => 'password', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('registrant'), ]); expect(value: $user->email)->toBe(expected: 'ada@example.com') - ->and(value: $user->password)->not->toBe(expected: 'password'); + ->and(value: $user->password)->not->toBe(expected: TestCredentials::password('registrant')); }); it(description: 'fails validation for duplicate email addresses', closure: function (): void { @@ -26,8 +27,8 @@ $validator = Validator::make(data: [ 'name' => 'Ada Lovelace', 'email' => 'ada@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('registrant'), ], rules: [ 'email' => ['required', 'string', 'email', 'max:255', Rule::unique(table: User::class)], ]); @@ -37,11 +38,13 @@ }); it(description: 'fails validation when password confirmation does not match', closure: function (): void { + expect(TestCredentials::password('different'))->not->toBe(TestCredentials::password('registrant')); + $validator = Validator::make(data: [ 'name' => 'Ada Lovelace', 'email' => 'ada@example.com', - 'password' => 'password', - 'password_confirmation' => 'different', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('different'), ], rules: [ 'password' => ['required', 'string', Password::default(), 'confirmed'], ]); @@ -54,8 +57,8 @@ $validator = Validator::make(data: [ 'name' => '', 'email' => 'ada@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('registrant'), ], rules: [ 'name' => ['required', 'string', 'max:255'], ]); @@ -68,8 +71,8 @@ $validator = Validator::make(data: [ 'name' => 'Ada Lovelace', 'email' => 'not-an-email', - 'password' => 'password', - 'password_confirmation' => 'password', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('registrant'), ], rules: [ 'email' => ['required', 'string', 'email', 'max:255'], ]); @@ -97,8 +100,8 @@ $user = app(abstract: CreateNewUser::class)->create(input: [ 'name' => 'Ada Lovelace', 'email' => 'ada@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('registrant'), ]); expect(value: $user)->toBeInstanceOf(class: User::class); @@ -110,8 +113,8 @@ $user = app(abstract: CreateNewUser::class)->create(input: [ 'name' => 'Ada Lovelace', 'email' => 'ada2@example.com', - 'password' => 'password', - 'password_confirmation' => 'password', + 'password' => TestCredentials::password('registrant'), + 'password_confirmation' => TestCredentials::password('registrant'), ]); expect(value: $user)->toBeInstanceOf(class: User::class); diff --git a/tests/Feature/ResetUserPasswordTest.php b/tests/Feature/ResetUserPasswordTest.php index 59497a9..275e520 100644 --- a/tests/Feature/ResetUserPasswordTest.php +++ b/tests/Feature/ResetUserPasswordTest.php @@ -7,32 +7,37 @@ use Illuminate\Support\Facades\Password; use Illuminate\Validation\ValidationException; use Simtabi\Laranail\AuthKit\Actions\ResetUserPassword; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; it('hashes and persists the new password', function (): void { + expect(TestCredentials::password('new'))->not->toBe(TestCredentials::password('old')); + $user = User::factory()->create([ - 'password' => Hash::make('old-password'), - 'remember_token' => 'stolen-remember-token', + 'password' => Hash::make(TestCredentials::password('old')), + 'remember_token' => TestCredentials::secret('remember'), ]); $user->createToken('stolen-token'); app(ResetUserPassword::class)->reset($user, [ - 'password' => 'new-secret', - 'password_confirmation' => 'new-secret', + 'password' => TestCredentials::password('new'), + 'password_confirmation' => TestCredentials::password('new'), ]); - expect(Hash::check('new-secret', $user->fresh()->password))->toBeTrue() - ->and(Hash::check('old-password', $user->password))->toBeFalse() + expect(Hash::check(TestCredentials::password('new'), $user->fresh()->password))->toBeTrue() + ->and(Hash::check(TestCredentials::password('old'), $user->password))->toBeFalse() ->and($user->fresh()->remember_token)->toBeNull() ->and($user->fresh()->tokens()->count())->toBe(0); }); it('fails validation when password confirmation does not match', function (): void { + expect(TestCredentials::password('different'))->not->toBe(TestCredentials::password('new')); + $user = User::factory()->create(); try { app(ResetUserPassword::class)->reset($user, [ - 'password' => 'new-secret', - 'password_confirmation' => 'different', + 'password' => TestCredentials::password('new'), + 'password_confirmation' => TestCredentials::password('different'), ]); } catch (ValidationException $e) { expect($e->errors())->toHaveKey('password'); @@ -50,15 +55,15 @@ }); it('integrates with laravel password broker for a full reset flow', function (): void { - $user = User::factory()->create(['password' => Hash::make('old-password')]); + $user = User::factory()->create(['password' => Hash::make(TestCredentials::password('old'))]); $token = Password::broker()->createToken($user); $status = Password::broker()->reset( [ 'email' => $user->email, - 'password' => 'broker-secret', - 'password_confirmation' => 'broker-secret', + 'password' => TestCredentials::password('broker'), + 'password_confirmation' => TestCredentials::password('broker'), 'token' => $token, ], function ($user, $password) { @@ -70,5 +75,5 @@ function ($user, $password) { ); expect($status)->toBe(Password::PASSWORD_RESET) - ->and(Hash::check('broker-secret', $user->fresh()->password))->toBeTrue(); + ->and(Hash::check(TestCredentials::password('broker'), $user->fresh()->password))->toBeTrue(); }); diff --git a/tests/Feature/TurnstileTest.php b/tests/Feature/TurnstileTest.php index e903432..de0d4d1 100644 --- a/tests/Feature/TurnstileTest.php +++ b/tests/Feature/TurnstileTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Validator; use Simtabi\Laranail\AuthKit\Support\AuthKit; use Simtabi\Laranail\AuthKit\Rules\TurnstileRule; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; function useTurnstileRoute(string $middleware): void { @@ -20,7 +21,7 @@ function useTurnstileRoute(string $middleware): void beforeEach(function (): void { config()->set('laranail.authkit.turnstile.enabled', true); - config()->set('laranail.authkit.turnstile.secret_key', 'secret-key'); + config()->set('laranail.authkit.turnstile.secret_key', TestCredentials::secret('turnstile')); }); it('accepts a valid token on web requests', function (): void { diff --git a/tests/Feature/UpdateUserPasswordTest.php b/tests/Feature/UpdateUserPasswordTest.php index 311974e..b240d38 100644 --- a/tests/Feature/UpdateUserPasswordTest.php +++ b/tests/Feature/UpdateUserPasswordTest.php @@ -7,6 +7,7 @@ use Illuminate\Validation\ValidationException; use Laravel\Fortify\Contracts\UpdatesUserPasswords; use Simtabi\Laranail\AuthKit\Actions\UpdateUserPassword; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; function expectPasswordUpdateValidationFailure(array $input, string $field, User $user): void { @@ -26,36 +27,40 @@ function expectPasswordUpdateValidationFailure(array $input, string $field, User }); it('updates the password when the current password is valid', function (): void { + expect(TestCredentials::password('new'))->not->toBe(TestCredentials::password('old')); + $user = User::factory()->create([ - 'password' => Hash::make('old-password'), - 'remember_token' => 'stolen-remember-token', + 'password' => Hash::make(TestCredentials::password('old')), + 'remember_token' => TestCredentials::secret('remember'), ]); $user->createToken('stolen-token'); $this->actingAs($user); app(UpdateUserPassword::class)->update($user, [ - 'current_password' => 'old-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', + 'current_password' => TestCredentials::password('old'), + 'password' => TestCredentials::password('new'), + 'password_confirmation' => TestCredentials::password('new'), ]); - expect(Hash::check('new-password', $user->fresh()->password))->toBeTrue() - ->and(Hash::check('old-password', $user->fresh()->password))->toBeFalse() + expect(Hash::check(TestCredentials::password('new'), $user->fresh()->password))->toBeTrue() + ->and(Hash::check(TestCredentials::password('old'), $user->fresh()->password))->toBeFalse() ->and($user->fresh()->remember_token)->toBeNull() ->and($user->fresh()->tokens()->count())->toBe(0); }); it('rejects an invalid current password', function (): void { - $user = User::factory()->create(['password' => Hash::make('old-password')]); + expect(TestCredentials::password('invalid'))->not->toBe(TestCredentials::password('old')); + + $user = User::factory()->create(['password' => Hash::make(TestCredentials::password('old'))]); $this->actingAs($user); expectPasswordUpdateValidationFailure( input: [ - 'current_password' => 'invalid-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', + 'current_password' => TestCredentials::password('invalid'), + 'password' => TestCredentials::password('new'), + 'password_confirmation' => TestCredentials::password('new'), ], field: 'current_password', user: $user, @@ -63,15 +68,17 @@ function expectPasswordUpdateValidationFailure(array $input, string $field, User }); it('rejects an unconfirmed password', function (): void { - $user = User::factory()->create(['password' => Hash::make('old-password')]); + expect(TestCredentials::password('different'))->not->toBe(TestCredentials::password('new')); + + $user = User::factory()->create(['password' => Hash::make(TestCredentials::password('old'))]); $this->actingAs($user); expectPasswordUpdateValidationFailure( input: [ - 'current_password' => 'old-password', - 'password' => 'new-password', - 'password_confirmation' => 'different-password', + 'current_password' => TestCredentials::password('old'), + 'password' => TestCredentials::password('new'), + 'password_confirmation' => TestCredentials::password('different'), ], field: 'password', user: $user, diff --git a/tests/Feature/UpdateUserProfileInformationTest.php b/tests/Feature/UpdateUserProfileInformationTest.php index 0736146..3c27714 100644 --- a/tests/Feature/UpdateUserProfileInformationTest.php +++ b/tests/Feature/UpdateUserProfileInformationTest.php @@ -5,6 +5,7 @@ use Workbench\App\Models\User; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Validation\ValidationException; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; use Laravel\Fortify\Contracts\UpdatesUserProfileInformation; use Simtabi\Laranail\AuthKit\Actions\UpdateUserProfileInformation; @@ -106,7 +107,7 @@ function expectProfileUpdateValidationFailure(array $input, string $field, User $user = new ProfileVerifiableUser([ 'name' => 'Ada Lovelace', 'email' => 'ada@example.com', - 'password' => 'password', + 'password' => TestCredentials::password('profile'), 'email_verified_at' => now(), ]); $user->save(); diff --git a/tests/Pest.php b/tests/Pest.php index c03b0fd..1d8a316 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -5,4 +5,4 @@ use Simtabi\Laranail\AuthKit\Tests\TestCase; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; -uses(TestCase::class, LazilyRefreshDatabase::class)->in('Feature', 'Unit'); +uses(TestCase::class, LazilyRefreshDatabase::class)->in('Feature', 'Unit', 'Architecture'); diff --git a/workbench/database/factories/UserFactory.php b/workbench/database/factories/UserFactory.php index 07ca8f3..dc34afd 100644 --- a/workbench/database/factories/UserFactory.php +++ b/workbench/database/factories/UserFactory.php @@ -8,6 +8,7 @@ use Workbench\App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Database\Eloquent\Factories\Factory; +use Simtabi\Laranail\Package\Tools\Testing\TestCredentials; class UserFactory extends Factory { @@ -19,7 +20,7 @@ public function definition(): array 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), - 'password' => Hash::make('password'), + 'password' => Hash::make(TestCredentials::password('factory')), 'remember_token' => Str::random(10), ]; }