(string) Str::uuid(), 'name' => 'A', 'email' => 'a+'.uniqid().'@x.com']); } private function mailboxClient(): \Mockery\MockInterface { $m = Mockery::mock(MailboxClient::class); $this->app->instance(MailboxClient::class, $m); return $m; } private function wallet(): \Mockery\MockInterface { $w = Mockery::mock(WalletPaymentService::class); $w->shouldReceive('balanceMinor')->andReturn(0)->byDefault(); $this->app->instance(WalletPaymentService::class, $w); return $w; } public function test_root_redirects_guest_to_sso(): void { $this->get('/')->assertRedirect(route('sso.connect')); } public function test_mailboxes_require_login(): void { $this->get(route('email.mailboxes.index'))->assertRedirect(route('login')); } public function test_index_lists_from_api(): void { $this->mailboxClient()->shouldReceive('forUser')->andReturn([ ['id' => 1, 'address' => 'me@acme.com', 'display_name' => 'Me', 'quota_mb' => 10240, 'status' => 'active'], ]); $this->actingAs($this->user())->get(route('email.mailboxes.index'))->assertOk()->assertSee('me@acme.com'); } public function test_create_form_shows_free_banner_within_allowance(): void { config(['email.free_mailboxes' => 1]); $this->mailboxClient()->shouldReceive('forUser')->andReturn([]); $this->wallet(); $dc = Mockery::mock(EmailDomainClient::class); $dc->shouldReceive('verified')->andReturn([['id' => 7, 'domain' => 'acme.com']]); $this->app->instance(EmailDomainClient::class, $dc); $this->actingAs($this->user())->get(route('email.mailboxes.create')) ->assertOk()->assertSee('free, forever', false); } public function test_store_free_1gb_mailbox_creates_without_charge(): void { $mc = $this->mailboxClient(); $mc->shouldReceive('create')->once() ->with(Mockery::any(), 7, 'sales', 'Sales', 'secret123', 1024, false, null) // 1 GB = free ->andReturn(['id' => 9, 'address' => 'sales@acme.com']); $w = $this->wallet(); $w->shouldNotReceive('charge'); $this->actingAs($this->user())->post(route('email.mailboxes.store'), [ 'email_domain_id' => 7, 'local_part' => 'Sales', 'display_name' => 'Sales', 'password' => 'secret123', 'password_confirmation' => 'secret123', 'quota_mb' => 1024, ])->assertRedirect(route('email.mailboxes.show', 9)); } public function test_store_paid_mailbox_charges_tier_price(): void { // 25 GB tier = GHS 30 = 3000 (doubled) $mc = $this->mailboxClient(); $mc->shouldReceive('create')->once() ->withArgs(fn ($pid, $did, $lp, $dn, $pw, $q, $paid, $ref) => $did === 7 && $q === 25600 && $paid === true && is_string($ref)) ->andReturn(['id' => 5, 'address' => 'sales@acme.com']); $w = $this->wallet(); $w->shouldReceive('canAfford')->once()->with(Mockery::any(), 3000)->andReturn(true); $w->shouldReceive('charge')->once()->withArgs(fn ($u, $amt, ...$r) => $amt === 3000)->andReturn(true); $this->actingAs($this->user())->post(route('email.mailboxes.store'), [ 'email_domain_id' => 7, 'local_part' => 'sales', 'display_name' => 'Sales', 'password' => 'secret123', 'password_confirmation' => 'secret123', 'quota_mb' => 25600, ])->assertRedirect(route('email.mailboxes.show', 5)); } public function test_store_rejects_invalid_quota(): void { $mc = $this->mailboxClient(); $mc->shouldNotReceive('create'); $this->wallet(); $this->actingAs($this->user())->from(route('email.mailboxes.create'))->post(route('email.mailboxes.store'), [ 'email_domain_id' => 7, 'local_part' => 'sales', 'display_name' => 'Sales', 'password' => 'secret123', 'password_confirmation' => 'secret123', 'quota_mb' => 999, ])->assertSessionHasErrors('quota_mb'); } public function test_store_paid_blocks_when_wallet_insufficient(): void { $mc = $this->mailboxClient(); $mc->shouldNotReceive('create'); $w = $this->wallet(); $w->shouldReceive('canAfford')->once()->andReturn(false); $this->actingAs($this->user())->from(route('email.mailboxes.create'))->post(route('email.mailboxes.store'), [ 'email_domain_id' => 7, 'local_part' => 'sales', 'display_name' => 'Sales', 'password' => 'secret123', 'password_confirmation' => 'secret123', 'quota_mb' => 10240, ])->assertRedirect(route('email.mailboxes.create'))->assertSessionHas('error'); } public function test_upgrade_charges_new_tier_and_updates_quota(): void { $mc = $this->mailboxClient(); $mc->shouldReceive('show')->andReturn(['id' => 3, 'address' => 'me@acme.com', 'quota_mb' => 1024]); // currently free 1 GB $mc->shouldReceive('updateQuota')->once() ->withArgs(fn ($pid, $id, $mb, $paid, $ref) => $id === 3 && $mb === 10240 && $paid === true && is_string($ref)) ->andReturn(['id' => 3, 'quota_mb' => 10240]); $w = $this->wallet(); $w->shouldReceive('canAfford')->once()->with(Mockery::any(), 2000)->andReturn(true); // 10 GB = GHS 20 $w->shouldReceive('charge')->once()->withArgs(fn ($u, $amt, ...$r) => $amt === 2000)->andReturn(true); $this->actingAs($this->user())->patch(route('email.mailboxes.upgrade.store', 3), ['quota_mb' => 10240]) ->assertRedirect(route('email.mailboxes.show', 3)); } public function test_upgrade_rejects_smaller_or_equal_plan(): void { $mc = $this->mailboxClient(); $mc->shouldReceive('show')->andReturn(['id' => 3, 'address' => 'me@acme.com', 'quota_mb' => 10240]); $mc->shouldNotReceive('updateQuota'); $w = $this->wallet(); $w->shouldNotReceive('charge'); $this->actingAs($this->user())->from(route('email.mailboxes.show', 3)) ->patch(route('email.mailboxes.upgrade.store', 3), ['quota_mb' => 5120]) ->assertRedirect(route('email.mailboxes.show', 3))->assertSessionHas('error'); } public function test_domains_index_lists_from_api(): void { $dc = Mockery::mock(EmailDomainClient::class); $dc->shouldReceive('forUser')->andReturn([['id' => 1, 'domain' => 'acme.com', 'active' => false]]); $this->app->instance(EmailDomainClient::class, $dc); $this->actingAs($this->user())->get(route('email.domains.index')) ->assertOk()->assertSee('acme.com')->assertSee('Pending verification'); } }