Files
portal-api/tests/Feature/Api/Admin/IpPool/IpPoolSheetImportTest.php
baodev0andClaude Opus 5.5 46aa138eec feat(admin): IP pool allocation, billing exports, service attachments
- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke
  with notification email (ip_allocated/ip_revoked), ip_pool_logs history.
- Orders/invoices: Excel export on branded template (BrandedReportWriter),
  shared billing filters, order item service info in list responses.
- Web design quote Excel export; activity log export.
- Service attachments with copy-to-invoice; deployment invoice attachments.
- Customer email attachments stored on disk and attached by path.
- Admin password change endpoint; user.fullname backfill migration;
  orders.user_id backfill from logs.
- Disable deploy workflow until CI/CD is set up.
- Ignore stored customer email and service attachment uploads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:58:36 +07:00

137 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Admin\IpPool;
use App\Models\IpPool;
use App\Models\ModuleIpAddress;
use App\Models\ModuleNetwork;
use App\Models\User;
use App\Services\IpPool\IpPoolSheetImporter;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class IpPoolSheetImportTest extends TestCase
{
use RefreshDatabase;
private const HEADER = [
'TEN_MANG', 'CIDR', 'OWNER_TYPE', 'IP_ADDRESS', 'IP_ROLE', 'PARENT_IP', 'SUBNET_MASK', 'GATEWAY',
'TEN_KHACH_HANG', 'TEN_DICH_VU', 'NGAY_CAP_PHAT', 'NGAY_THU_HOI', 'TRANG_THAI', 'CONTACT_EMAIL',
'CONTACT_CC', 'CONTACT_PHONE', 'MONTHLY_COST', 'SELLING_PRICE', 'THONG_TIN_BO_SUNG',
'LAST_EMAIL_TYPE', 'LAST_EMAIL_AT', 'UPDATED_AT', 'UPDATED_BY', 'GHI_CHU', 'NOTE',
];
private function admin(): User
{
$role = Role::create(['name' => 'role-' . uniqid(), 'guard_name' => 'admin']);
$role->givePermissionTo(Permission::firstOrCreate(['name' => 'module-vcenter.manager', 'guard_name' => 'admin']));
$admin = User::factory()->create();
$admin->assignRole($role);
return $admin;
}
private function row(array $values): array
{
return array_map(fn ($header) => $values[$header] ?? null, self::HEADER);
}
private function xlsx(array $rows): UploadedFile
{
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->setTitle('CONFIG');
$sheet = $spreadsheet->createSheet()->setTitle('IP_POOL');
$sheet->fromArray([self::HEADER, ...$rows]);
$sheet->setCellValue('Y2', '=XLOOKUP(D2,CONFIG!A:A,CONFIG!B:B)');
$path = tempnam(sys_get_temp_dir(), 'ip') . '.xlsx';
(new Xlsx($spreadsheet))->setPreCalculateFormulas(false)->save($path);
return new UploadedFile($path, 'ip.xlsx', null, null, true);
}
public function test_status_follows_sheet_unless_the_system_owns_the_ip(): void
{
$this->assertSame('assigned', IpPoolSheetImporter::resolveStatus(null, false, 'ALLOCATED'));
$this->assertSame('assigned', IpPoolSheetImporter::resolveStatus('free', false, 'ALLOCATED'));
$this->assertSame('free', IpPoolSheetImporter::resolveStatus('assigned', false, 'FREE'));
$this->assertSame('assigned', IpPoolSheetImporter::resolveStatus('assigned', true, 'FREE'));
$this->assertSame('reserved', IpPoolSheetImporter::resolveStatus('reserved', true, 'ALLOCATED'));
$this->assertSame('free', IpPoolSheetImporter::resolveStatus(null, false, null));
}
public function test_preview_reports_changes_without_writing_and_import_applies_them(): void
{
$network = ModuleNetwork::create(['name' => 'VM Network 56']);
IpPool::create([
'ip_address' => '10.0.0.5', 'module_network_id' => $network->id, 'cidr' => 24,
'gateway' => '10.0.0.1', 'status' => 'free',
]);
IpPool::create([
'ip_address' => '10.0.0.6', 'module_network_id' => $network->id, 'cidr' => 24, 'status' => 'free',
]);
ModuleIpAddress::create(['ip' => '10.0.0.6', 'module_network_id' => $network->id, 'status' => 1]);
$file = $this->xlsx([
$this->row([
'TEN_MANG' => "THEGIOISO-VN\t\t", 'CIDR' => '/22', 'OWNER_TYPE' => 'thế giới số',
'IP_ADDRESS' => '10.0.0.5', 'TEN_KHACH_HANG' => 'Khách A', 'TRANG_THAI' => 'ALLOCATED',
'NGAY_CAP_PHAT' => ExcelDate::PHPToExcel(new \DateTime('2026-04-15')),
'CONTACT_EMAIL' => "\ta@example.com; b@example.com", 'CONTACT_PHONE' => 835123959,
'MONTHLY_COST' => 60000,
]),
$this->row(['TEN_MANG' => 'THEGIOISO-VN', 'IP_ADDRESS' => '10.0.0.6', 'TRANG_THAI' => 'FREE']),
$this->row(['TEN_MANG' => 'HOITUSO-VN', 'IP_ADDRESS' => '10.0.1.9', 'TRANG_THAI' => 'ALLOCATED']),
$this->row(['CONTACT_CC' => 'kythuat@tgs.com.vn']),
$this->row(['TEN_MANG' => 'HOITUSO-VN', 'IP_ADDRESS' => '10.0.1.10', 'CONTACT_EMAIL' => 'not-an-email']),
]);
$admin = $this->admin();
$this->actingAs($admin, 'admin')
->post('/api/v1/administrator/ip-pool/import/preview', ['file' => $file], ['Accept' => 'application/json'])
->assertOk()
->assertJsonPath('data.valid', 3)
->assertJsonPath('data.skipped_blank', 1)
->assertJsonPath('data.error_count', 1)
->assertJsonPath('data.errors.0.row', 6)
->assertJsonPath('data.create', 1)
->assertJsonPath('data.update', 2)
->assertJsonPath('data.to_assigned', 2)
->assertJsonPath('data.kept_by_system', 1);
$this->assertSame(2, IpPool::count());
$this->actingAs($admin, 'admin')
->post('/api/v1/administrator/ip-pool/import', ['file' => $file], ['Accept' => 'application/json'])
->assertOk();
$updated = IpPool::where('ip_address', '10.0.0.5')->sole();
$this->assertSame($network->id, $updated->module_network_id);
$this->assertSame('assigned', $updated->status);
$this->assertSame('THEGIOISO-VN', $updated->ip_block);
$this->assertSame('THẾ GIỚI SỐ', $updated->owner_type);
$this->assertSame(22, $updated->cidr);
$this->assertSame('10.0.0.1', $updated->gateway);
$this->assertSame('a@example.com, b@example.com', $updated->contact_email);
$this->assertSame('0835123959', $updated->contact_phone);
$this->assertSame('2026-04-15', $updated->allocated_at->toDateString());
$this->assertSame('60000.00', $updated->monthly_cost);
$this->assertSame('free', IpPool::where('ip_address', '10.0.0.6')->value('status'));
$created = IpPool::where('ip_address', '10.0.1.9')->sole();
$this->assertNull($created->module_network_id);
$this->assertSame('assigned', $created->status);
$this->assertFalse(IpPool::where('ip_address', '10.0.1.10')->exists());
}
}