|
|
|
@@ -0,0 +1,393 @@
|
|
|
|
|
<?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'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|