hoàn thành 58 test case
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":1,"defects":[],"times":{"Tests\\Browser\\BulkWorkflowTest::test_1000_users_full_workflow_from_excel":33.781}}
|
||||
+89
-5
@@ -488,6 +488,8 @@ FILE 11: tests/Browser/AdminEdgeCaseTest.php (3 tests)
|
||||
--------------------------------------------------------------------------------
|
||||
Chuẩn bị chung: Tạo adminUser với đầy đủ roles trong DB.
|
||||
Dọn dẹp chung: Xóa UserRole, adminUser khỏi DB; xóa prize test nếu còn sót.
|
||||
Chuẩn bị chung: Tạo adminUser với đầy đủ roles trong DB.
|
||||
Dọn dẹp chung: Xóa UserRole, adminUser khỏi DB; xóa prize test nếu còn sót.
|
||||
|
||||
[TC-55] test_prize_number_cannot_be_negative
|
||||
Mô tả : Nhập prize_number âm (-5) vào form thêm giải thưởng phải bị
|
||||
@@ -534,14 +536,94 @@ Dọn dẹp chung: Xóa UserRole, adminUser khỏi DB; xóa prize test nếu cò
|
||||
Lưu ý : Không dùng actingAs()->get(exportRoute) vì excel.blade.php gọi exit()
|
||||
sau khi xuất file — điều này sẽ terminate PHPUnit process.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
FILE 12: tests/Browser/BulkWorkflowTest.php (1 test)
|
||||
--------------------------------------------------------------------------------
|
||||
Chuẩn bị chung: Tạo adminUser (đầy đủ roles), tạo prize có prize_number=2000,
|
||||
tạo file Excel tạm chứa 1000 bản ghi người dùng bằng PhpSpreadsheet.
|
||||
Dọn dẹp chung: Xóa 1000 users + PrizeUser (theo chunk 200), xóa admin, prize, file Excel.
|
||||
|
||||
[TC-58] test_1000_users_full_workflow_from_excel
|
||||
Mô tả : Kiểm tra toàn bộ workflow hệ thống (end-to-end) với 1000 người dùng
|
||||
được cung cấp từ file Excel — từ import dữ liệu đến quay số đến
|
||||
xác nhận trên giao diện admin.
|
||||
Luồng :
|
||||
[A] Đọc file Excel → Xác nhận file có đúng 1001 dòng (1 header + 1000 data).
|
||||
[B] Bulk-import 1000 users vào DB (đọc từng ô Excel, insert theo chunk 100).
|
||||
[C] Simulate 1000 lượt quay số (bulk insert prize_users + update spined=1).
|
||||
[D] 1 lượt quay thực qua HTTP POST /updateprizeuser để kiểm tra API layer.
|
||||
[E] Browser: Admin mở /admin/users → bảng #table hiển thị.
|
||||
Browser: Admin mở /admin/prizeuser → bảng + bộ lọc ngày + nút export hiển thị.
|
||||
[F] Browser: Xác nhận href của #excel_export chứa "prizeuser/export";
|
||||
PHP route('admin/prizeuser/export') trả URL hợp lệ.
|
||||
|
||||
Chi tiết từng bước:
|
||||
|
||||
[A] Tạo & Đọc Excel:
|
||||
Chuẩn bị: generateTestExcel() dùng PhpOffice\PhpSpreadsheet tạo file .xlsx
|
||||
với 6 cột: STT | HỌ TÊN | SỐ ĐIỆN THOẠI | ĐỊA CHỈ | NGHỀ NGHIỆP | NGUỒN BIẾT SIS.
|
||||
Phone dùng dải riêng 0900000001..0900001000 (tránh đụng dữ liệu thật).
|
||||
Bước : XlsxReader::load() → getHighestDataRow() = 1001.
|
||||
Kết quả : File Excel hợp lệ, đọc được 1001 dòng.
|
||||
|
||||
[B] Bulk-import 1000 users:
|
||||
Bước : Đọc từng row (2..1001) → lấy phone, name (mb_strtoupper), address,
|
||||
job, known_sis → build $importBatch[].
|
||||
DB::table('users')->insert() theo chunk 100.
|
||||
Lấy IDs: User::whereIn('phone', ...)->pluck('id').
|
||||
Kết quả : assertCount(1000, $bulkUserIds) — 1000 users được import.
|
||||
|
||||
[C] Simulate 1000 lần quay:
|
||||
Bước : Build $spinBatch[] với user_id + prize_id cho 1000 user.
|
||||
DB::table('prize_users')->insert() theo chunk 100.
|
||||
User::whereIn('id', chunk)->update(['spined' => 1]) theo chunk 200.
|
||||
Kết quả : PrizeUser::whereIn('user_id', $bulkUserIds)->count() = 1000.
|
||||
User::whereIn(...)->where('spined', 1)->count() = 1000.
|
||||
|
||||
[D] Kiểm tra luồng spin thực qua Browser (1 user mới, giao diện game):
|
||||
Chuẩn bị: Tạo 1 user riêng (phone='0900000000') ngoài dải bulk, spined=0.
|
||||
Bước : browser->loginAs(spinUser) → visit('/game') → pause(800ms)
|
||||
→ assertAttribute('#spinButton','data-check','true')
|
||||
→ click('#spinButton') → waitForText('XIN CHÚC MỪNG', 30s)
|
||||
→ pause(1500ms) để DB commit.
|
||||
Kết quả : spinUser->refresh()->spined = 1.
|
||||
PrizeUser::where('user_id', spinUserId)->first() != null.
|
||||
Lợi thế : Dùng Browser thực → CSRF token có trong session → không bị lỗi 419.
|
||||
Đây là cách kiểm tra API /updateprizeuser trung thực nhất.
|
||||
Dọn dẹp : Xóa spinUser và PrizeUser của spinUser ngay sau khi assert.
|
||||
|
||||
[E] Browser admin xác nhận:
|
||||
Bước : browser->loginAs($adminUser).
|
||||
Truy cập /admin/users → waitFor('#table', 15) → assertPresent('#table').
|
||||
Truy cập /admin/prizeuser → waitFor('table, .table', 15)
|
||||
→ assertPresent('#fromDateTime', '#toDateTime', '#excel_export').
|
||||
Kết quả : Bảng quản lý người dùng và bảng người trúng giải đều hiển thị đúng.
|
||||
|
||||
[F] Xác nhận export Excel:
|
||||
Bước : JS override document.createElement để capture href khi click #excel_export.
|
||||
PHP: route('admin/prizeuser/export') kiểm tra URL.
|
||||
Kết quả : capturedHref chứa 'prizeuser/export' (nếu JS capture thành công).
|
||||
route('admin/prizeuser/export') chứa '/admin/prizeuser/export'.
|
||||
Lưu ý : Không gọi actingAs()->get(exportUrl) vì excel.blade.php gọi exit()
|
||||
sau khi xuất file → terminate PHPUnit process.
|
||||
|
||||
Dữ liệu test:
|
||||
- 1000 users với phone 0900000001..0900001000
|
||||
- 1 prize "Giải Thưởng Bulk Test" (prize_number=2000, prize_order=98)
|
||||
- 1 admin "DUSK BULK ADMIN" (email: dusk_bulk_admin@test.local)
|
||||
- 1 file Excel tạm (sys_get_temp_dir()/dusk_bulk_{timestamp}.xlsx)
|
||||
|
||||
Assertions: 15 assertions
|
||||
Thời gian : ~37 giây
|
||||
|
||||
================================================================================
|
||||
TỔNG KẾT
|
||||
================================================================================
|
||||
|
||||
Số file test : 11 files
|
||||
Số test cases : 57 test cases
|
||||
Số assertions : 114 assertions
|
||||
Thời gian : ~178 giây
|
||||
Số file test : 12 files
|
||||
Số test cases : 58 test cases
|
||||
Số assertions : 129 assertions
|
||||
Thời gian : ~215 giây
|
||||
Kết quả : ALL PASSED ✓
|
||||
|
||||
Phân bổ theo chức năng:
|
||||
@@ -559,8 +641,9 @@ TỔNG KẾT
|
||||
│ Logic game nâng cao │ 3 │
|
||||
│ Bảo mật & phân quyền │ 2 │
|
||||
│ Admin edge cases │ 3 │
|
||||
│ Bulk workflow 1000 users từ Excel │ 1 │
|
||||
├──────────────────────────────────────────┼────────┤
|
||||
│ TỔNG CỘNG │ 57 │
|
||||
│ TỔNG CỘNG │ 58 │
|
||||
└──────────────────────────────────────────┴────────┘
|
||||
|
||||
Công nghệ kiểm thử:
|
||||
@@ -570,6 +653,7 @@ TỔNG KẾT
|
||||
- Môi trường : WSL2 + WSLg, DUSK_HEADLESS_DISABLED=true
|
||||
- Server : php artisan serve --port=8001
|
||||
- DB : MySQL (fsg3kct926qj_quaysoDB)
|
||||
- Excel lib : PhpOffice\PhpSpreadsheet (có sẵn trong vendor/phpoffice/)
|
||||
|
||||
Sửa lỗi ứng dụng phát hiện qua quá trình kiểm thử:
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# dusk — Wrapper chạy Laravel Dusk tests
|
||||
#
|
||||
# Cách dùng:
|
||||
# ./dusk # Có giao diện Chrome (mặc định)
|
||||
# ./dusk --headless # Chạy headless (không mở cửa sổ)
|
||||
# ./dusk --headless --filter=BulkWorkflowTest
|
||||
# ./dusk --filter=AdminPrizeTest
|
||||
#
|
||||
# Tất cả tham số còn lại được chuyển thẳng sang php artisan dusk.
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
HEADLESS=0
|
||||
ARTISAN_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--headless" ]]; then
|
||||
HEADLESS=1
|
||||
else
|
||||
ARTISAN_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $HEADLESS -eq 1 ]]; then
|
||||
echo "[dusk] Chạy HEADLESS (không mở cửa sổ Chrome)"
|
||||
# Set DUSK_HEADLESS=true trước khi PHP khởi động.
|
||||
# Laravel dùng Dotenv::createImmutable() → OS env var có độ ưu tiên cao hơn .env
|
||||
# → DUSK_HEADLESS_DISABLED trong .env sẽ bị bỏ qua bởi hasHeadlessDisabled()
|
||||
exec env DUSK_HEADLESS=true php artisan dusk "${ARTISAN_ARGS[@]}"
|
||||
else
|
||||
echo "[dusk] Chạy có giao diện Chrome (DUSK_HEADLESS_DISABLED=true)"
|
||||
exec php artisan dusk "${ARTISAN_ARGS[@]}"
|
||||
fi
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
beStrictAboutTestsThatDoNotTestAnything="false"
|
||||
colors="true"
|
||||
processIsolation="false"
|
||||
stopOnError="false"
|
||||
stopOnFailure="false"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
backupStaticProperties="false">
|
||||
<testsuites>
|
||||
<testsuite name="Browser Test Suite">
|
||||
<directory suffix="Test.php">./tests/Browser</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -24,6 +24,11 @@ class AdminEdgeCaseTest extends DuskTestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$stale = User::where('email', 'dusk_edge_admin@test.local')->first();
|
||||
if ($stale) {
|
||||
UserRole::where('user_id', $stale->id)->delete();
|
||||
$stale->delete();
|
||||
}
|
||||
$this->adminUser = User::create([
|
||||
'name' => 'DUSK EDGE ADMIN',
|
||||
'email' => 'dusk_edge_admin@test.local',
|
||||
|
||||
@@ -15,6 +15,14 @@ class AdminLoginTest extends DuskTestCase
|
||||
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_admin_test@test.local')->first();
|
||||
if ($stale) {
|
||||
UserRole::where('user_id', $stale->id)->delete();
|
||||
$stale->delete();
|
||||
}
|
||||
|
||||
// Tạo admin test user với đầy đủ roles
|
||||
$this->adminUser = User::create([
|
||||
'name' => 'DUSK ADMIN TEST',
|
||||
|
||||
@@ -16,6 +16,11 @@ class AdminParameterTest extends DuskTestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$stale = User::where('email', 'dusk_param_admin@test.local')->first();
|
||||
if ($stale) {
|
||||
UserRole::where('user_id', $stale->id)->delete();
|
||||
$stale->delete();
|
||||
}
|
||||
$this->adminUser = User::create([
|
||||
'name' => 'DUSK PARAM ADMIN',
|
||||
'email' => 'dusk_param_admin@test.local',
|
||||
|
||||
@@ -17,6 +17,14 @@ class AdminPrizeTest extends DuskTestCase
|
||||
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_prize_admin@test.local')->first();
|
||||
if ($stale) {
|
||||
UserRole::where('user_id', $stale->id)->delete();
|
||||
$stale->delete();
|
||||
}
|
||||
|
||||
$this->adminUser = User::create([
|
||||
'name' => 'DUSK PRIZE ADMIN',
|
||||
'email' => 'dusk_prize_admin@test.local',
|
||||
|
||||
@@ -16,6 +16,11 @@ class AdminUserTest extends DuskTestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$stale = User::where('email', 'dusk_user_admin@test.local')->first();
|
||||
if ($stale) {
|
||||
UserRole::where('user_id', $stale->id)->delete();
|
||||
$stale->delete();
|
||||
}
|
||||
$this->adminUser = User::create([
|
||||
'name' => 'DUSK USER ADMIN',
|
||||
'email' => 'dusk_user_admin@test.local',
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,11 @@ class SecurityTest extends DuskTestCase
|
||||
]);
|
||||
|
||||
// Admin user — có đầy đủ roles
|
||||
$stale = User::where('email', 'dusk_secure_admin@test.local')->first();
|
||||
if ($stale) {
|
||||
UserRole::where('user_id', $stale->id)->delete();
|
||||
$stale->delete();
|
||||
}
|
||||
$this->adminUser = User::create([
|
||||
'name' => 'DUSK SECURE ADMIN',
|
||||
'email' => 'dusk_secure_admin@test.local',
|
||||
|
||||
@@ -11,6 +11,23 @@ use PHPUnit\Framework\Attributes\BeforeClass;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* Kiểm tra xem có nên chạy Chrome ở chế độ headless không.
|
||||
*
|
||||
* Ưu tiên:
|
||||
* 1. Nếu DUSK_HEADLESS=true (set từ shell trước khi PHP chạy) → headless
|
||||
* 2. Ngược lại fallback về DUSK_HEADLESS_DISABLED trong .env
|
||||
*/
|
||||
protected function hasHeadlessDisabled(): bool
|
||||
{
|
||||
if (isset($_SERVER['DUSK_HEADLESS']) || isset($_ENV['DUSK_HEADLESS'])) {
|
||||
return false; // Buộc headless dù .env có DUSK_HEADLESS_DISABLED
|
||||
}
|
||||
|
||||
return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
|
||||
isset($_ENV['DUSK_HEADLESS_DISABLED']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare for Dusk test execution.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user