Skip to content

Commit 3994b86

Browse files
test: improve coverage
1 parent 132c5f1 commit 3994b86

15 files changed

Lines changed: 2265 additions & 0 deletions
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
use Beliven\Lockout\Events\EntityUnlocked;
4+
use Beliven\Lockout\Lockout;
5+
use Beliven\Lockout\Tests\Fixtures\User;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Support\Facades\DB;
9+
use Illuminate\Support\Facades\Event;
10+
use Illuminate\Support\Facades\Schema;
11+
12+
it('creates a persistent lock for a user, unlocks it via Lockout::unlockModel, clears attempts and dispatches EntityUnlocked', function () {
13+
// Use array cache to keep tests deterministic
14+
config()->set('lockout.cache_store', 'array');
15+
Cache::store('array')->flush();
16+
17+
// Ensure login_field is email for the fixture
18+
config()->set('lockout.login_field', 'email');
19+
20+
// Prepare database tables used by the integration flow
21+
Schema::dropIfExists('model_lockouts');
22+
Schema::create('model_lockouts', function (Blueprint $table) {
23+
$table->id();
24+
$table->string('model_type')->nullable();
25+
$table->unsignedBigInteger('model_id')->nullable();
26+
$table->timestamp('locked_at')->nullable();
27+
$table->timestamp('unlocked_at')->nullable();
28+
$table->timestamp('expires_at')->nullable();
29+
$table->text('reason')->nullable();
30+
$table->json('meta')->nullable();
31+
$table->timestamps();
32+
});
33+
34+
Schema::dropIfExists('users');
35+
Schema::create('users', function (Blueprint $table) {
36+
$table->id();
37+
$table->string('email')->unique();
38+
$table->string('password')->nullable();
39+
$table->timestamp('locked_at')->nullable();
40+
$table->timestamps();
41+
});
42+
43+
// Create a notifiable user fixture
44+
$user = User::query()->create([
45+
'email' => 'integration@example.test',
46+
'password' => 'secret',
47+
]);
48+
49+
/** @var Lockout $service */
50+
$service = app(Lockout::class);
51+
52+
// Create a persistent lock via the trait helper (delegates to Lockout::lockModel)
53+
$createdLock = $user->lock(['reason' => 'integration-test']);
54+
expect($createdLock)->not->toBeNull();
55+
56+
// Sanity: ensure an active lock exists in DB
57+
$active = DB::table('model_lockouts')
58+
->where('model_type', User::class)
59+
->where('model_id', $user->id)
60+
->whereNull('unlocked_at')
61+
->first();
62+
expect($active)->not->toBeNull();
63+
64+
// Seed an attempt counter for this user's identifier so clearAttempts will run
65+
$service->incrementAttempts($user->email);
66+
expect($service->getAttempts($user->email))->toBe(1);
67+
68+
// Fake events so we can assert EntityUnlocked dispatch
69+
Event::fake();
70+
71+
// Perform unlock via service
72+
$result = $service->unlockModel($user, [
73+
'reason' => 'manual-unlock',
74+
'requestData' => (object) ['ip' => '127.0.0.1'],
75+
]);
76+
77+
// Should return the lock object when saved successfully
78+
expect($result)->not->toBeNull();
79+
80+
// Reload most recent lock row and assert unlocked_at was set
81+
$row = DB::table('model_lockouts')
82+
->where('model_type', User::class)
83+
->where('model_id', $user->id)
84+
->orderByDesc('id')
85+
->first();
86+
87+
expect($row)->not->toBeNull();
88+
expect($row->unlocked_at)->not->toBeNull();
89+
90+
// Attempts counter for the user's identifier should have been cleared
91+
expect($service->getAttempts($user->email))->toBe(0);
92+
93+
// Event should have been dispatched with the expected payload
94+
Event::assertDispatched(EntityUnlocked::class, function ($event) use ($user) {
95+
return $event->model instanceof User && $event->model->id === $user->id;
96+
});
97+
98+
// Cleanup DB
99+
Schema::dropIfExists('model_lockouts');
100+
Schema::dropIfExists('users');
101+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
use Beliven\Lockout\Commands\PruneLockouts;
4+
use Illuminate\Console\Command;
5+
6+
afterEach(function () {
7+
// Ensure any custom binding is cleared to avoid polluting other tests.
8+
try {
9+
app()->forgetInstance(PruneLockouts::class);
10+
} catch (\Throwable $_) {
11+
// ignore
12+
}
13+
});
14+
15+
it('returns failure when pruneLockoutLogs throws an exception', function () {
16+
// Bind a custom command that throws during pruneLockoutLogs()
17+
app()->bind(PruneLockouts::class, function () {
18+
return new class extends PruneLockouts
19+
{
20+
protected function pruneLockoutLogs(int $days): int
21+
{
22+
throw new \RuntimeException('simulated-prune-error');
23+
}
24+
};
25+
});
26+
27+
$this->artisan(PruneLockouts::class, ['--force' => true])
28+
->assertExitCode(Command::FAILURE);
29+
});
30+
31+
it('returns failure when pruneModelLockouts throws an exception', function () {
32+
// Bind a custom command that succeeds pruning logs but throws during pruneModelLockouts()
33+
app()->bind(PruneLockouts::class, function () {
34+
return new class extends PruneLockouts
35+
{
36+
protected function pruneLockoutLogs(int $days): int
37+
{
38+
return 0;
39+
}
40+
41+
protected function pruneModelLockouts(int $days): int
42+
{
43+
throw new \RuntimeException('simulated-model-prune-error');
44+
}
45+
};
46+
});
47+
48+
$this->artisan(PruneLockouts::class, ['--force' => true])
49+
->assertExitCode(Command::FAILURE);
50+
});

0 commit comments

Comments
 (0)