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(); });