create(); $codes = $user->replaceRecoveryCodes(); // Opened from "Backup-Codes verwalten" with no fresh-generation flag: never reveal. Livewire::actingAs($user)->test(RecoveryCodes::class) ->assertSet('revealed', false) ->assertDontSee($codes[0]) ->assertDontSee($codes[7]) ->assertSee(__('auth.recovery_hidden_notice')); } public function test_fresh_flag_reveals_the_codes_once(): void { $user = User::factory()->create(); $codes = $user->replaceRecoveryCodes(); session(['2fa.codes_fresh' => true]); Livewire::actingAs($user)->test(RecoveryCodes::class) ->assertSet('revealed', true) ->assertSee($codes[0]) ->assertSee($codes[7]); // pull() forgets the flag — it is single-use. $this->assertFalse(session()->has('2fa.codes_fresh')); } public function test_regenerate_from_manage_view_reveals_the_new_codes(): void { $user = User::factory()->create(); $old = $user->replaceRecoveryCodes(); // Manage view (no flag) → not revealed → regenerate → the NEW set is revealed once. Livewire::actingAs($user)->test(RecoveryCodes::class) ->assertSet('revealed', false) ->call('regenerate') ->assertSet('revealed', true) ->assertDontSee($old[0]); $new = $user->fresh()->recoveryCodes(); $this->assertNotEquals($old, $new); Livewire::actingAs($user)->test(RecoveryCodes::class) ->call('regenerate') ->assertSee($user->fresh()->recoveryCodes()[0]); } public function test_revealed_property_is_locked_against_client_tampering(): void { $user = User::factory()->create(); $codes = $user->replaceRecoveryCodes(); // Manage view (no fresh flag): codes stay hidden. A crafted client update that flips // the #[Locked] $revealed prop must be rejected — Livewire throws on a locked set. $component = Livewire::actingAs($user)->test(RecoveryCodes::class) ->assertSet('revealed', false) ->assertDontSee($codes[0]); $this->expectException(CannotUpdateLockedPropertyException::class); $component->set('revealed', true); } public function test_download_requires_a_fresh_grant(): void { $user = User::factory()->create(); $user->replaceRecoveryCodes(); // Without a fresh-generation grant, a direct hit of the download route is forbidden, // so stored codes can never be re-downloaded by an authed user at will. $this->actingAs($user) ->get(route('two-factor.recovery.download')) ->assertForbidden(); // With the one-time grant (set when codes are freshly generated/regenerated) the // download succeeds exactly once. session(['2fa.download_grant' => true]); $this->actingAs($user) ->get(route('two-factor.recovery.download')) ->assertOk() ->assertDownload('clusev-recovery-codes.txt'); // The grant is single-use: pull() consumed it, so an immediate re-download 403s. $this->actingAs($user) ->get(route('two-factor.recovery.download')) ->assertForbidden(); } public function test_old_recovery_route_is_gone(): void { $this->assertFalse(Route::has('two-factor.recovery')); $this->assertTrue(Route::has('two-factor.recovery.download')); } }