seed(\Database\Seeders\TemplateSeeder::class); Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']); $this->admin = User::factory()->create(['username' => 'kythuat']); $this->admin->assignRole('Full Administrator'); $this->customer = Customer::factory()->create(['fullname' => 'Công ty A', 'email' => 'a@example.com']); $this->network = ModuleNetwork::create(['name' => 'VM Network 56']); } private function pool(string $ip, array $attributes = []): IpPool { return IpPool::create([ 'ip_address' => $ip, 'module_network_id' => $this->network->id, 'cidr' => 22, 'gateway' => '10.0.0.1', 'status' => 'free', ...$attributes, ]); } private function allocation(array $ids): array { return [ 'ids' => $ids, 'customer_uuid' => $this->customer->uuid, 'to' => 'a@example.com', 'cc' => 'kythuat@tgs.com.vn', 'date' => '2026-10-15', 'service_name' => 'Colo 1U', ]; } public function test_preview_lists_ips_without_changing_them(): void { $pool = $this->pool('10.0.0.5'); $response = $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocation/preview', $this->allocation([$pool->id])) ->assertOk() ->assertJsonPath('data.subject', 'Thông báo cấp phát 1 địa chỉ IP'); $this->assertStringContainsString('10.0.0.5', $response->json('data.html')); $this->assertStringContainsString('Colo <b>1U</b>', $response->json('data.html')); $this->assertSame('free', $pool->fresh()->status); } public function test_allocate_assigns_ips_logs_and_emails_customer(): void { Mail::fake(); $first = $this->pool('10.0.0.5'); $second = $this->pool('10.0.0.6'); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocate', $this->allocation([$first->id, $second->id])) ->assertOk(); $first->refresh(); $this->assertSame('assigned', $first->status); $this->assertSame($this->customer->id, $first->customer_id); $this->assertSame('Công ty A', $first->customer_name); $this->assertSame('2026-10-15', $first->allocated_at->toDateString()); $this->assertSame('ALLOCATED_SENT', $first->last_email_type); $this->assertSame('kythuat', $first->updated_by); $log = IpPoolLog::where('ip_pool_id', $first->id)->sole(); $this->assertSame([IpPoolLog::ALLOCATED, 'free', 'assigned', $this->admin->id], [$log->action, $log->status_from, $log->status_to, $log->user_id]); $this->assertSame(['2026-10-15', 'Colo 1U'], [$log->effective_date->toDateString(), $log->service_name]); Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->type === IpAllocationMail::ALLOCATED && count($mail->ips) === 2 && $mail->hasTo('a@example.com') && $mail->hasCc('kythuat@tgs.com.vn')); $this->assertSame(1, MailLog::where('customer_id', $this->customer->id)->count()); } public function test_each_ip_can_have_its_own_service(): void { Mail::fake(); $first = $this->pool('10.0.0.5'); $second = $this->pool('10.0.0.6'); $payload = [...$this->allocation([$first->id, $second->id]), 'service_name' => 'Dịch vụ IP', 'services' => [$second->id => 'CLOUD VPS SILVER']]; $html = $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocation/preview', $payload) ->assertOk() ->json('data.html'); $this->assertStringContainsString('CLOUD VPS SILVER', $html); $this->assertStringContainsString('Dịch vụ IP', $html); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocate', $payload) ->assertOk(); Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->envelope()->subject === 'Thông báo cấp phát 2 địa chỉ IP'); $this->assertSame('Dịch vụ IP', $first->fresh()->service_name); $this->assertSame('CLOUD VPS SILVER', $second->fresh()->service_name); } public function test_admin_can_override_email_subject(): void { Mail::fake(); $pool = $this->pool('10.0.0.5'); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocate', [...$this->allocation([$pool->id]), 'subject' => 'Bàn giao IP cho Công ty A']) ->assertOk(); Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => $mail->envelope()->subject === 'Bàn giao IP cho Công ty A'); $this->assertSame('Bàn giao IP cho Công ty A', MailLog::where('customer_id', $this->customer->id)->value('subject')); } public function test_allocation_email_carries_uploaded_attachments(): void { Mail::fake(); Storage::fake('local'); $pool = $this->pool('10.0.0.5'); $this->actingAs($this->admin, 'admin') ->post(self::BASE . '/allocate', [ ...$this->allocation([$pool->id]), 'attachments' => [UploadedFile::fake()->createWithContent('bien-ban.pdf', "%PDF-1.4\n\xff\xfe\x00\x9c binary")], ], ['Accept' => 'application/json']) ->assertOk(); Mail::assertQueued(IpAllocationMail::class, fn (IpAllocationMail $mail) => count($mail->storedFiles) === 1 && $mail->storedFiles[0]['name'] === 'bien-ban.pdf' && Storage::disk('local')->exists($mail->storedFiles[0]['path']) && count($mail->attachments()) === 1 && json_encode(serialize($mail)) !== false); } public function test_failed_email_dispatch_rolls_back_the_allocation(): void { $pool = $this->pool('10.0.0.5'); Mail::shouldReceive('to')->andThrow(new \RuntimeException('queue down')); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocate', $this->allocation([$pool->id])) ->assertStatus(500); $this->assertSame('free', $pool->fresh()->status); $this->assertNull($pool->fresh()->customer_id); $this->assertSame(0, IpPoolLog::count()); $this->assertSame(0, MailLog::count()); } public function test_allocate_rejects_ips_that_are_not_free(): void { Mail::fake(); $free = $this->pool('10.0.0.5'); $taken = $this->pool('10.0.0.6', ['status' => 'assigned']); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/allocate', $this->allocation([$free->id, $taken->id])) ->assertStatus(422) ->assertJsonFragment(['ids' => ['Chỉ cấp phát được IP đang trống. IP không hợp lệ: 10.0.0.6']]); $this->assertSame('free', $free->fresh()->status); Mail::assertNothingQueued(); } public function test_revoke_groups_by_customer_and_frees_ips(): void { Mail::fake(); $other = Customer::factory()->create(['fullname' => 'Công ty B']); $a = $this->pool('10.0.0.5', ['status' => 'assigned', 'customer_id' => $this->customer->id, 'customer_name' => 'Công ty A', 'service_name' => 'CLOUD VPS SILVER', 'contact_email' => 'a@example.com']); $b = $this->pool('10.0.0.6', ['status' => 'assigned', 'customer_id' => $other->id, 'customer_name' => 'Công ty B']); $groups = $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/revocation/preview', ['ids' => [$a->id, $b->id], 'date' => '2026-10-31']) ->assertOk() ->assertJsonCount(2, 'data') ->json('data'); $missingEmail = collect($groups)->firstWhere('customer_name', 'Công ty B'); $this->assertNull($missingEmail['to']); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/revoke', [ 'ids' => [$a->id, $b->id], 'date' => '2026-10-31', 'recipients' => collect($groups)->map(fn ($g) => ['key' => $g['key'], 'to' => $g['to']])->all(), ]) ->assertStatus(422); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/revoke', [ 'ids' => [$a->id, $b->id], 'date' => '2026-10-31', 'recipients' => collect($groups)->map(fn ($g) => ['key' => $g['key'], 'to' => $g['to'] ?? 'b@example.com'])->all(), ]) ->assertOk(); $a->refresh(); $this->assertSame('free', $a->status); $this->assertNull($a->customer_id); $this->assertSame('2026-10-31', $a->revoked_at->toDateString()); $log = IpPoolLog::where('ip_pool_id', $a->id)->sole(); $this->assertSame([IpPoolLog::REVOKED, 'Công ty A', $this->customer->id], [$log->action, $log->customer_name, $log->customer_id]); $this->assertSame(['2026-10-31', 'CLOUD VPS SILVER'], [$log->effective_date->toDateString(), $log->service_name]); $this->assertNull($a->service_name); Mail::assertQueuedCount(2); } public function test_revoke_refuses_ips_used_by_a_vm(): void { $pool = $this->pool('10.0.0.7', ['status' => 'assigned']); ModuleIpAddress::create(['ip' => '10.0.0.7', 'module_network_id' => $this->network->id, 'status' => 1]); $this->actingAs($this->admin, 'admin') ->postJson(self::BASE . '/revocation/preview', ['ids' => [$pool->id], 'date' => '2026-10-31']) ->assertStatus(422); } public function test_logs_endpoint_returns_history(): void { $pool = $this->pool('10.0.0.8'); IpPoolLog::record($pool, IpPoolLog::ALLOCATED, 'free', ['customer_name' => 'Công ty A']); $this->actingAs($this->admin, 'admin') ->getJson(self::BASE . "/{$pool->id}/logs") ->assertOk() ->assertJsonPath('data.0.action', IpPoolLog::ALLOCATED) ->assertJsonPath('data.0.customer_name', 'Công ty A'); } }