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' ); }); } }