154 lines
6.9 KiB
PHP
154 lines
6.9 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Models\Tenant;
|
||
|
|
use App\Models\User;
|
||
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Illuminate\Support\Facades\Notification;
|
||
|
|
use Illuminate\Support\Facades\Password;
|
||
|
|
use PragmaRX\Google2FA\Google2FA;
|
||
|
|
|
||
|
|
uses(DatabaseTransactions::class);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset the Auth manager's default guard and cached guard instances back to
|
||
|
|
* the 'web' SessionGuard.
|
||
|
|
*
|
||
|
|
* Necessary because auth:sanctum middleware calls Auth::shouldUse('sanctum')
|
||
|
|
* on every successfully-authenticated request, which permanently changes
|
||
|
|
* config('auth.defaults.guard') to 'sanctum' in the shared test application
|
||
|
|
* instance. Laravel feature tests reuse the same $this->app between HTTP calls,
|
||
|
|
* so this pollution persists across requests. Any subsequent call to
|
||
|
|
* Auth::login() (which internally calls Auth::guard()->login()) then resolves
|
||
|
|
* to the Sanctum RequestGuard — which has no login() method — and throws a
|
||
|
|
* BadMethodCallException.
|
||
|
|
*
|
||
|
|
* The reset must happen *before* any request whose controller calls Auth::login()
|
||
|
|
* without an explicit guard argument (i.e. login and 2fa/verify routes).
|
||
|
|
*/
|
||
|
|
function resetAuthToWebGuard(): void
|
||
|
|
{
|
||
|
|
app('auth')->forgetGuards();
|
||
|
|
app('auth')->setDefaultDriver('web');
|
||
|
|
}
|
||
|
|
|
||
|
|
it('full auth-flow writes all expected auth_log events', function () {
|
||
|
|
Notification::fake();
|
||
|
|
|
||
|
|
$tenant = Tenant::factory()->create();
|
||
|
|
|
||
|
|
// ── Step 1: Register ─────────────────────────────────────────────────────
|
||
|
|
$this->postJson('/api/auth/register', [
|
||
|
|
'email' => 'flow-test@example.ru',
|
||
|
|
'password' => 'secure-pass-1234',
|
||
|
|
'accept_offer' => true,
|
||
|
|
'accept_pdn' => true,
|
||
|
|
])->assertStatus(201);
|
||
|
|
// logs: register_success
|
||
|
|
|
||
|
|
$user = User::where('email', 'flow-test@example.ru')->first();
|
||
|
|
expect($user)->not->toBeNull();
|
||
|
|
|
||
|
|
// ── Step 2: Login (no 2FA yet) — establish session auth ──────────────────
|
||
|
|
// No prior auth:sanctum request, so no reset needed here.
|
||
|
|
$this->postJson('/api/auth/login', [
|
||
|
|
'email' => 'flow-test@example.ru',
|
||
|
|
'password' => 'secure-pass-1234',
|
||
|
|
])->assertOk();
|
||
|
|
// logs: login_success (first direct login, 2FA not yet enabled)
|
||
|
|
|
||
|
|
// ── Step 3: 2FA init (session-authenticated via web guard) ───────────────
|
||
|
|
// auth:sanctum middleware → shouldUse('sanctum') → default becomes 'sanctum'
|
||
|
|
$this->postJson('/api/2fa/init')->assertOk();
|
||
|
|
// logs: 2fa_setup_init
|
||
|
|
$secret = session('auth.pending_totp_secret');
|
||
|
|
expect($secret)->not->toBeNull();
|
||
|
|
|
||
|
|
// ── Step 4: 2FA confirm ───────────────────────────────────────────────────
|
||
|
|
$google2fa = new Google2FA;
|
||
|
|
$code = $google2fa->getCurrentOtp($secret);
|
||
|
|
// auth:sanctum middleware → shouldUse('sanctum') again
|
||
|
|
$this->postJson('/api/2fa/confirm', ['code' => $code])->assertOk();
|
||
|
|
// logs: 2fa_setup_confirmed (totp_enabled now true)
|
||
|
|
|
||
|
|
// ── Step 5: Logout ────────────────────────────────────────────────────────
|
||
|
|
// auth:sanctum middleware → shouldUse('sanctum') again
|
||
|
|
$this->postJson('/api/auth/logout')->assertOk();
|
||
|
|
// logs: logout
|
||
|
|
|
||
|
|
// ── Step 6: Login with 2FA enabled ────────────────────────────────────────
|
||
|
|
// auth.defaults.guard is now 'sanctum' from previous auth:sanctum requests.
|
||
|
|
// Reset to 'web' so Auth::login() inside AuthController::login() finds the
|
||
|
|
// SessionGuard (which implements login()) rather than the RequestGuard.
|
||
|
|
resetAuthToWebGuard();
|
||
|
|
|
||
|
|
$this->postJson('/api/auth/login', [
|
||
|
|
'email' => 'flow-test@example.ru',
|
||
|
|
'password' => 'secure-pass-1234',
|
||
|
|
])->assertOk();
|
||
|
|
// requires_2fa=true, pending_user_id stored in session
|
||
|
|
|
||
|
|
// ── Step 7: 2FA verify — completes login ─────────────────────────────────
|
||
|
|
// No auth:sanctum request happened since the last reset, so no reset needed.
|
||
|
|
$validCode = $google2fa->getCurrentOtp($secret);
|
||
|
|
$this->postJson('/api/auth/2fa/verify', ['code' => $validCode])->assertOk();
|
||
|
|
// logs: 2fa_verify_success
|
||
|
|
|
||
|
|
// ── Step 8: 2FA disable (session-authenticated from step 7) ──────────────
|
||
|
|
// auth:sanctum middleware → shouldUse('sanctum') again
|
||
|
|
$this->postJson('/api/2fa/disable', ['password' => 'secure-pass-1234'])->assertOk();
|
||
|
|
// logs: 2fa_disabled
|
||
|
|
|
||
|
|
// ── Step 9: Logout ────────────────────────────────────────────────────────
|
||
|
|
// auth:sanctum middleware → shouldUse('sanctum') again
|
||
|
|
$this->postJson('/api/auth/logout')->assertOk();
|
||
|
|
|
||
|
|
// ── Step 10: Login without 2FA — direct login_success ────────────────────
|
||
|
|
// Reset again: auth.defaults.guard is 'sanctum' from Step 8+9 auth:sanctum.
|
||
|
|
resetAuthToWebGuard();
|
||
|
|
|
||
|
|
$this->postJson('/api/auth/login', [
|
||
|
|
'email' => 'flow-test@example.ru',
|
||
|
|
'password' => 'secure-pass-1234',
|
||
|
|
])->assertOk();
|
||
|
|
// logs: login_success (direct login, 2FA now disabled)
|
||
|
|
|
||
|
|
// ── Step 11: Forgot password ──────────────────────────────────────────────
|
||
|
|
$this->postJson('/api/auth/logout')->assertOk();
|
||
|
|
|
||
|
|
$this->postJson('/api/auth/forgot', [
|
||
|
|
'email' => 'flow-test@example.ru',
|
||
|
|
])->assertOk();
|
||
|
|
// logs: password_reset_requested
|
||
|
|
|
||
|
|
// ── Step 12: Reset password ───────────────────────────────────────────────
|
||
|
|
$token = Password::createToken($user);
|
||
|
|
$this->postJson('/api/auth/reset-password', [
|
||
|
|
'token' => $token,
|
||
|
|
'email' => 'flow-test@example.ru',
|
||
|
|
'password' => 'new-secure-pass-5678',
|
||
|
|
'password_confirmation' => 'new-secure-pass-5678',
|
||
|
|
])->assertOk();
|
||
|
|
// logs: password_reset_completed
|
||
|
|
|
||
|
|
// ── Assert all expected events were recorded for this user ────────────────
|
||
|
|
$events = DB::table('auth_log')
|
||
|
|
->where('user_id', $user->id)
|
||
|
|
->pluck('event')
|
||
|
|
->all();
|
||
|
|
|
||
|
|
expect($events)->toContain(
|
||
|
|
'register_success',
|
||
|
|
'2fa_setup_init',
|
||
|
|
'2fa_setup_confirmed',
|
||
|
|
'logout',
|
||
|
|
'login_success',
|
||
|
|
'2fa_verify_success',
|
||
|
|
'2fa_disabled',
|
||
|
|
'password_reset_requested',
|
||
|
|
'password_reset_completed',
|
||
|
|
);
|
||
|
|
});
|