get(route('register'))->assertOk()->assertSee(__('auth.register_title')); }); it('links between login and signup', function () { $this->get(route('login'))->assertOk()->assertSee(route('register')); $this->get(route('register'))->assertOk()->assertSee(route('login')); }); it('registers a new account and signs in', function () { $this->post(route('register.store'), [ 'name' => 'New Co', 'email' => 'new@signup.test', 'customer_type' => Customer::TYPE_BUSINESS, 'password' => 'password1234', 'password_confirmation' => 'password1234', ])->assertRedirect(); $user = User::query()->where('email', 'new@signup.test')->first(); // A registered user is never an operator: the two no longer share a table // or a role system to check "isOperator" against at all. expect($user)->not->toBeNull(); $this->assertAuthenticatedAs($user); // A linked customer is created so the portal has something to work with. $customer = Customer::query()->where('user_id', $user->id)->first(); expect($customer)->not->toBeNull() ->and($customer->email)->toBe('new@signup.test') // The answer they gave, stored as given. Everything downstream — the // withdrawal right, the invoice wording, reverse charge — reads this // and not the VAT field. ->and($customer->customer_type)->toBe(Customer::TYPE_BUSINESS) ->and($customer->isBusiness())->toBeTrue(); }); it('refuses a signup that does not say whether it is a consumer or a business', function () { // Required rather than optional-with-a-default. A default would be an // answer nobody gave, and the whole point of the column is that it holds a // recorded answer or nothing at all. $this->post(route('register.store'), [ 'name' => 'Unanswered', 'email' => 'unanswered@signup.test', 'password' => 'password1234', 'password_confirmation' => 'password1234', ])->assertSessionHasErrors('customer_type'); expect(User::query()->where('email', 'unanswered@signup.test')->exists())->toBeFalse(); }); it('refuses a customer type that is neither of the two answers', function () { $this->post(route('register.store'), [ 'name' => 'Sneaky', 'email' => 'sneaky@signup.test', 'customer_type' => 'charity', 'password' => 'password1234', 'password_confirmation' => 'password1234', ])->assertSessionHasErrors('customer_type'); expect(Customer::query()->where('email', 'sneaky@signup.test')->exists())->toBeFalse(); }); it('refuses to register with an existing customer email (account-claim guard)', function () { Customer::factory()->create(['email' => 'claim@signup.test']); $this->post(route('register.store'), [ 'name' => 'Claimer', 'email' => 'claim@signup.test', 'password' => 'password1234', 'password_confirmation' => 'password1234', ])->assertSessionHasErrors('email'); expect(User::query()->where('email', 'claim@signup.test')->exists())->toBeFalse(); }); it('refuses to register with an existing operator email', function () { // R21: the operator side of the same account-claim guard above — an // address that already belongs to staff must not become a customer too. Operator::factory()->create(['email' => 'staff@signup.test']); $this->post(route('register.store'), [ 'name' => 'Claimer', 'email' => 'staff@signup.test', 'password' => 'password1234', 'password_confirmation' => 'password1234', ])->assertSessionHasErrors('email'); expect(User::query()->where('email', 'staff@signup.test')->exists())->toBeFalse(); }); it('throttles repeated registration attempts', function () { $hit429 = false; for ($i = 0; $i < 24; $i++) { $res = $this->post(route('register.store'), [ 'name' => 'S', 'email' => "spam{$i}@signup.test", 'password' => 'password1234', 'password_confirmation' => 'password1234', ]); if ($res->status() === 429) { $hit429 = true; break; } } expect($hit429)->toBeTrue(); }); it('rejects a mismatched password confirmation', function () { $this->post(route('register.store'), [ 'name' => 'X', 'email' => 'x@signup.test', 'password' => 'password1234', 'password_confirmation' => 'different', ])->assertSessionHasErrors('password'); expect(User::query()->where('email', 'x@signup.test')->exists())->toBeFalse(); });