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 @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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__), 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();
});
21 changes: 13 additions & 8 deletions tests/Feature/AttemptEmailPasswordLoginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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',
);

Expand All @@ -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');
Expand All @@ -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',
);

Expand All @@ -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',
);

Expand All @@ -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',
);

Expand All @@ -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',
);

Expand Down
33 changes: 18 additions & 15 deletions tests/Feature/CreateNewUserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)],
]);
Expand All @@ -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'],
]);
Expand All @@ -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'],
]);
Expand All @@ -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'],
]);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
29 changes: 17 additions & 12 deletions tests/Feature/ResetUserPasswordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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) {
Expand All @@ -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();
});
3 changes: 2 additions & 1 deletion tests/Feature/TurnstileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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 {
Expand Down
37 changes: 22 additions & 15 deletions tests/Feature/UpdateUserPasswordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -26,52 +27,58 @@ 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,
);
});

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,
Expand Down
Loading
Loading