- 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>
137 lines
6.2 KiB
PHP
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());
|
|
}
|
|
}
|