Files
-S.I.S-QuaySo_v2/tests/Browser/BulkWorkflowTest.php
T

394 lines
16 KiB
PHP

<?php
namespace Tests\Browser;
use App\Models\Prize;
use App\Models\PrizeUser;
use App\Models\Role;
use App\Models\User;
use App\Models\UserRole;
use Illuminate\Support\Facades\DB;
use Laravel\Dusk\Browser;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use Tests\DuskTestCase;
/**
* BulkWorkflowTest — Kiểm thử toàn bộ workflow hệ thống với 1000 người dùng từ file Excel.
*
* Luồng kiểm thử (end-to-end):
* [A] Tạo file Excel 1000 người dùng bằng PhpSpreadsheet
* [B] Đọc Excel → bulk-import 1000 users vào DB
* [C] Simulate 1000 lần quay (bulk PrizeUser + spined=1)
* [D] 1 lần quay thực qua HTTP endpoint /updateprizeuser (kiểm tra API thực)
* [E] Browser: Admin xác nhận danh sách users và danh sách người trúng giải
* [F] Browser: Xác nhận nút export Excel tồn tại và route hợp lệ
*
* [TC-58] test_1000_users_full_workflow_from_excel
*/
class BulkWorkflowTest extends DuskTestCase
{
private User $adminUser;
private Prize $bulkPrize;
private array $bulkUserIds = [];
private string $excelPath = '';
const BULK_COUNT = 1000;
// =========================================================================
// Số điện thoại dải riêng để không đụng dữ liệu thật trong DB
// Format: 0900XXXXXX (10 chữ số), XXXXXX = 000001..001000
// =========================================================================
private function makeTestPhone(int $index): string
{
return '0900' . str_pad($index, 6, '0', STR_PAD_LEFT);
}
// =========================================================================
// setUp / tearDown
// =========================================================================
protected function setUp(): void
{
parent::setUp();
// Bảo vệ chống duplicate từ lần chạy bị interrupt trước đó
$stale = User::where('email', 'dusk_bulk_admin@test.local')->first();
if ($stale) {
UserRole::where('user_id', $stale->id)->delete();
$stale->delete();
}
Prize::where('prize_name', 'Giải Thưởng Bulk Test')->each(function ($p) {
\App\Models\PrizeUser::where('prize_id', $p->id)->delete();
$p->delete();
});
// Xóa bulk users tồn đọng (phone 0900000000..0900001000)
$staleUsers = User::whereBetween('phone', ['0900000000', '0900001000'])->pluck('id');
if ($staleUsers->isNotEmpty()) {
\App\Models\PrizeUser::whereIn('user_id', $staleUsers)->delete();
User::whereIn('id', $staleUsers)->delete();
}
// Tạo admin với đầy đủ roles
$this->adminUser = User::create([
'name' => 'DUSK BULK ADMIN',
'email' => 'dusk_bulk_admin@test.local',
'phone' => '0900999999',
'password' => bcrypt('dusk_bulk_admin_2026'),
'status' => 1,
'address' => 'Cần Thơ',
'known_sis' => 0,
'job' => 'Admin',
'spined' => 0,
]);
$roles = Role::where('role_status', 1)->get();
foreach ($roles as $role) {
UserRole::create(['user_id' => $this->adminUser->id, 'role_id' => $role->id]);
}
// Tạo giải thưởng với prize_number đủ lớn cho 1001 lượt (1000 bulk + 1 HTTP)
$this->bulkPrize = Prize::create([
'prize_name' => 'Giải Thưởng Bulk Test',
'prize_status' => 1,
'prize_number' => 2000,
'prize_order' => 98,
'user_id' => 0,
]);
// Tạo file Excel tạm chứa 1000 bản ghi người dùng
$this->excelPath = sys_get_temp_dir() . '/dusk_bulk_' . time() . '.xlsx';
$this->generateTestExcel($this->excelPath, self::BULK_COUNT);
}
protected function tearDown(): void
{
// Dọn dẹp theo thứ tự: PrizeUser → Users → Prize → AdminUser
if (!empty($this->bulkUserIds)) {
foreach (array_chunk($this->bulkUserIds, 200) as $chunk) {
PrizeUser::whereIn('user_id', $chunk)->delete();
User::whereIn('id', $chunk)->delete();
}
}
// Xóa spin verify test user nếu còn
$spinUser = User::where('phone', $this->makeTestPhone(0))->first();
if ($spinUser) {
PrizeUser::where('user_id', $spinUser->id)->delete();
$spinUser->delete();
}
UserRole::where('user_id', $this->adminUser->id)->delete();
$this->adminUser->delete();
$this->bulkPrize->delete();
if ($this->excelPath && file_exists($this->excelPath)) {
unlink($this->excelPath);
}
parent::tearDown();
}
// =========================================================================
// Helper: Tạo file Excel 1000 bản ghi người dùng
// =========================================================================
private function generateTestExcel(string $path, int $count): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Danh Sách Người Dùng');
// Header row
$sheet->setCellValue('A1', 'STT');
$sheet->setCellValue('B1', 'HỌ TÊN');
$sheet->setCellValue('C1', 'SỐ ĐIỆN THOẠI');
$sheet->setCellValue('D1', 'ĐỊA CHỈ');
$sheet->setCellValue('E1', 'NGHỀ NGHIỆP');
$sheet->setCellValue('F1', 'NGUỒN BIẾT SIS');
// Data rows (index 1..1000 → phone 0900000001..0900001000)
for ($i = 1; $i <= $count; $i++) {
$row = $i + 1;
$sheet->setCellValue('A' . $row, $i);
$sheet->setCellValue('B' . $row, 'Người Dùng Bulk ' . $i);
$sheet->setCellValue('C' . $row, $this->makeTestPhone($i));
$sheet->setCellValue('D' . $row, ($i % 2 === 0) ? 'Hà Nội' : 'Cần Thơ');
$sheet->setCellValue('E' . $row, ($i % 3 === 0) ? 'Giáo viên' : 'Kỹ sư');
$sheet->setCellValue('F' . $row, $i % 2); // 0 hoặc 1
}
$writer = new XlsxWriter($spreadsheet);
$writer->save($path);
}
// =========================================================================
// [TC-58] Toàn bộ workflow 1000 người dùng từ file Excel
// =========================================================================
/**
* [BƯỚC A] Đọc 1000 bản ghi từ file Excel.
* [BƯỚC B] Bulk-import 1000 users vào DB (giả lập chức năng admin import từ Excel).
* [BƯỚC C] Simulate 1000 lượt quay số (DB bulk insert + cập nhật spined).
* [BƯỚC D] 1 lượt quay thực qua HTTP POST /updateprizeuser (kiểm tra API layer).
* [BƯỚC E] Browser: Admin mở /admin/users và /admin/prizeuser, xác nhận bảng hiển thị.
* [BƯỚC F] Browser: Xác nhận #excel_export tồn tại; route export hợp lệ.
*/
public function test_1000_users_full_workflow_from_excel(): void
{
// ==================================================================
// [A] Đọc file Excel
// ==================================================================
$this->assertFileExists($this->excelPath, 'File Excel test phải được tạo thành công');
$reader = new XlsxReader();
$spreadsheet = $reader->load($this->excelPath);
$sheet = $spreadsheet->getActiveSheet();
$highestRow = $sheet->getHighestDataRow(); // phải là 1001 (1 header + 1000 data)
$this->assertEquals(
self::BULK_COUNT + 1,
$highestRow,
'File Excel phải có ' . (self::BULK_COUNT + 1) . ' dòng (1 header + 1000 data)'
);
// ==================================================================
// [B] Bulk-import 1000 users vào DB từ dữ liệu trong Excel
// ==================================================================
$now = now();
$importBatch = [];
for ($row = 2; $row <= $highestRow; $row++) {
$phone = (string) $sheet->getCell('C' . $row)->getValue();
$name = mb_strtoupper(
(string) $sheet->getCell('B' . $row)->getValue(), 'UTF-8'
);
$address = (string) $sheet->getCell('D' . $row)->getValue();
$job = (string) $sheet->getCell('E' . $row)->getValue();
$knownSis = (int) $sheet->getCell('F' . $row)->getValue();
$importBatch[] = [
'name' => $name,
'phone' => $phone,
'email' => $phone . '@phone.local',
'password' => bcrypt('bulk2026'),
'address' => $address,
'known_sis' => $knownSis,
'job' => $job,
'status' => 1,
'spined' => 0,
'created_at' => $now,
'updated_at' => $now,
];
}
// Chia chunks 100 để tránh quá nhiều placeholders trong 1 query
foreach (array_chunk($importBatch, 100) as $chunk) {
DB::table('users')->insert($chunk);
}
// Lấy IDs của 1000 users vừa import
$importedPhones = array_column($importBatch, 'phone');
$this->bulkUserIds = User::whereIn('phone', $importedPhones)
->pluck('id')
->toArray();
$this->assertCount(
self::BULK_COUNT,
$this->bulkUserIds,
'1000 users phải được import thành công từ file Excel'
);
// ==================================================================
// [C] Simulate 1000 lượt quay số (bulk PrizeUser + update spined)
// ==================================================================
$prizeId = $this->bulkPrize->id;
$spinBatch = [];
foreach ($this->bulkUserIds as $userId) {
$spinBatch[] = [
'user_id' => $userId,
'prize_id' => $prizeId,
'created_at' => $now,
'updated_at' => $now,
];
}
foreach (array_chunk($spinBatch, 100) as $chunk) {
DB::table('prize_users')->insert($chunk);
}
// Bulk update spined = 1 cho tất cả users vừa import
foreach (array_chunk($this->bulkUserIds, 200) as $chunk) {
User::whereIn('id', $chunk)->update(['spined' => 1]);
}
// DB assertions sau khi spin
$prizeUserCount = PrizeUser::whereIn('user_id', $this->bulkUserIds)->count();
$this->assertEquals(
self::BULK_COUNT,
$prizeUserCount,
'1000 bản ghi PrizeUser phải được tạo — mỗi user có đúng 1 lần quay'
);
$spinedCount = User::whereIn('id', $this->bulkUserIds)
->where('spined', 1)
->count();
$this->assertEquals(
self::BULK_COUNT,
$spinedCount,
'1000 users phải có spined = 1 sau khi quay thành công'
);
// ==================================================================
// [D] 1 lần quay thực qua Browser (spin 1 user chưa quay qua trang game)
// Dùng browser + loginAs thay vì HTTP test client để tránh vấn đề CSRF.
// Luồng: user mới (spined=0) → /game → click #spinButton → waitFor kết quả
// → DB: PrizeUser + spined=1
// ==================================================================
$spinTestPhone = $this->makeTestPhone(0); // '0900000000' — ngoài dải bulk
// Cleanup nếu còn tồn đọng từ lần chạy trước
$staleSpinUser = User::where('phone', $spinTestPhone)->first();
if ($staleSpinUser) {
PrizeUser::where('user_id', $staleSpinUser->id)->delete();
$staleSpinUser->delete();
}
$spinUser = User::create([
'name' => 'DUSK SPIN VERIFY USER',
'phone' => $spinTestPhone,
'email' => $spinTestPhone . '@phone.local',
'password' => bcrypt('spin_verify_2026'),
'address' => 'Cần Thơ',
'known_sis' => 1,
'job' => 'Tester',
'status' => 1,
'spined' => 0,
]);
$spinUserId = $spinUser->id;
$this->browse(function (Browser $browser) use ($spinUserId, $prizeId) {
$spinUser = User::find($spinUserId);
$browser->loginAs($spinUser)
->visit('/game')
->pause(800); // Chờ wheel canvas render
$browser->assertAttribute('#spinButton', 'data-check', 'true');
$browser->click('#spinButton');
$browser->waitForText('XIN CHÚC MỪNG', 30); // Spin mất ~10-15s
$browser->pause(1500); // Chờ DB transaction commit
});
// DB assertions sau khi spin thực
$spinUser->refresh();
$this->assertEquals(1, $spinUser->spined,
'/game spin thực: user.spined phải = 1 sau khi quay thành công');
$spinPrizeUser = PrizeUser::where('user_id', $spinUserId)->first();
$this->assertNotNull($spinPrizeUser,
'/game spin thực: bản ghi PrizeUser phải tồn tại sau khi quay');
// Cleanup spin test user
PrizeUser::where('user_id', $spinUserId)->delete();
$spinUser->delete();
// ==================================================================
// [E] Browser: Admin xác nhận giao diện quản trị hiển thị đúng
// ==================================================================
$this->browse(function (Browser $browser) {
$browser->loginAs($this->adminUser);
// Trang danh sách người dùng
$browser->visit('/admin/users')
->waitFor('#table', 15)
->assertPresent('#table');
// Trang danh sách người trúng giải
$browser->visit('/admin/prizeuser')
->waitFor('table, .table', 15)
->assertPresent('#fromDateTime')
->assertPresent('#toDateTime')
->assertPresent('#excel_export');
// ==================================================================
// [F] Xác nhận route export Excel hợp lệ (không click để tránh exit())
// ==================================================================
$capturedHref = $browser->script("
var captured = null;
var _orig = document.createElement.bind(document);
document.createElement = function(tag) {
var el = _orig(tag);
if (tag.toLowerCase() === 'a') {
Object.defineProperty(el, 'href', {
set: function(v) { captured = v; },
get: function() { return captured || ''; }
});
}
return el;
};
try {
document.getElementById('excel_export').click();
} catch(e) {}
document.createElement = _orig;
return captured;
")[0];
// capturedHref có thể là null nếu JS build URL khác cách — kiểm tra fallback
if ($capturedHref !== null) {
$browser->assertScript(
"return ('" . addslashes($capturedHref) . "').indexOf('prizeuser/export') !== -1",
true
);
}
// Xác nhận route export đã được đăng ký trong hệ thống (PHP route helper)
$exportUrl = route('admin/prizeuser/export');
$this->assertStringContainsString(
'/admin/prizeuser/export',
$exportUrl,
'Route admin/prizeuser/export phải tồn tại trong hệ thống'
);
});
}
}