test(TwoFactorSetup::class) ->call($action) ->assertForbidden(); } expect($op->refresh()->two_factor_secret)->toBeNull(); }); it('takes a password, then sets two-factor up and confirms it with a real code', function () { $op = operator('Support'); $page = Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'password') ->call('confirmPassword') ->assertHasNoErrors(); $page->call('enableTwoFactor'); expect($op->refresh()->two_factor_secret)->not->toBeNull() // Not on until a code has been accepted: someone who scans nothing and // closes the tab must not be locked out of their own account. ->and($op->two_factor_confirmed_at)->toBeNull(); $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->two_factor_secret)); $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors(); expect($op->refresh()->two_factor_confirmed_at)->not->toBeNull() ->and($page->get('recoveryCodes'))->toBeArray()->not->toBeEmpty(); }); it('rejects a wrong code instead of switching it on', function () { $op = operator('Support'); Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'password') ->call('confirmPassword') ->call('enableTwoFactor') ->set('twoFactorCode', '000000') ->call('confirmTwoFactor') ->assertHasErrors('twoFactorCode'); expect($op->refresh()->two_factor_confirmed_at)->toBeNull(); }); it('does not accept a wrong password, and says so without hinting', function () { $op = operator('Support'); Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'falsch') ->call('confirmPassword') ->assertHasErrors('confirmablePassword') ->call('enableTwoFactor') ->assertForbidden(); }); it('never puts the secret where the browser can see it', function () { // A Livewire property travels to the browser and back in the component // snapshot. The QR image is derived from the secret; the secret is not. $op = operator('Support'); $page = Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'password') ->call('confirmPassword') ->call('enableTwoFactor'); $secret = decrypt($op->refresh()->two_factor_secret); expect(json_encode($page->snapshot))->not->toContain($secret); }); it('actually renders the enrolment control on the page, not just in the component', function () { // The original gap was exactly this: a component with the right methods // but a blade that never called them. Assert on rendered HTML, not // component state, so a forgotten wire:click regresses this test too. $op = operator('Support'); // Nothing to enrol with yet — the password gate is what shows. $page = Livewire::actingAs($op, 'operator')->test(TwoFactorSetup::class); $page->assertSee(__('two_factor_setup.confirm_first')) ->assertSee(__('two_factor_setup.state_off')); $page->set('confirmablePassword', 'password')->call('confirmPassword'); $page->assertSee(__('two_factor_setup.enable')); $page->call('enableTwoFactor'); $page->assertSee(__('two_factor_setup.activate')) ->assertSee(__('two_factor_setup.code')); $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret)); $page->set('twoFactorCode', $code)->call('confirmTwoFactor'); $page->assertSee(__('two_factor_setup.state_on')) ->assertSee(__('two_factor_setup.codes_title')) ->assertSee(__('two_factor_setup.disable')); }); it('lets an operator regenerate recovery codes and disable two-factor once confirmed', function () { $op = operator('Support'); $page = Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'password') ->call('confirmPassword') ->call('enableTwoFactor'); $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret)); $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors(); $firstCodes = $op->refresh()->two_factor_recovery_codes; $page->call('regenerateRecoveryCodes'); expect($op->refresh()->two_factor_recovery_codes)->not->toBe($firstCodes); $page->call('disableTwoFactor')->assertHasNoErrors(); expect($op->refresh()->two_factor_confirmed_at)->toBeNull() ->and($op->two_factor_secret)->toBeNull(); }); it('confirms disabling two-factor through the modal without turning it off itself', function () { // R23: the modal takes no arguments and cannot see whose session this is // — it only dispatches back to the page, which still runs // disableTwoFactor() with its own password check (see the "refuses every // two-factor action" case above, which now covers onDisableConfirmed too). $op = operator('Support'); $page = Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'password') ->call('confirmPassword') ->call('enableTwoFactor'); $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret)); $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors(); Livewire::actingAs($op, 'operator') ->test(ConfirmDisableTwoFactor::class) ->call('confirm') ->assertDispatched('two-factor-disable-confirmed'); expect($op->refresh()->two_factor_confirmed_at)->not->toBeNull(); }); it('disables two-factor once the page receives the confirmed event', function () { $op = operator('Support'); $page = Livewire::actingAs($op, 'operator') ->test(TwoFactorSetup::class) ->set('confirmablePassword', 'password') ->call('confirmPassword') ->call('enableTwoFactor'); $code = app(Google2FA::class)->getCurrentOtp(decrypt($op->refresh()->two_factor_secret)); $page->set('twoFactorCode', $code)->call('confirmTwoFactor')->assertHasNoErrors(); $page->call('onDisableConfirmed')->assertHasNoErrors(); expect($op->refresh()->two_factor_confirmed_at)->toBeNull() ->and($op->two_factor_secret)->toBeNull(); }); it('sends the operator to sign in instead of a 500 when the session has ended', function () { // render() has no requireConfirmedPassword() gate in front of it — unlike // every action above, Livewire calls it on every update including a bare // property sync, so it is directly reachable with no other guard in the // way once the session that owned the page has ended. $op = operator('Support'); $page = Livewire::actingAs($op, 'operator')->test(TwoFactorSetup::class); // The page sat open long enough for the operator's own session to end. Auth::guard('operator')->logout(); $page->call('$refresh')->assertRedirect(route('admin.login')); });