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

255 lines
11 KiB
PHP

<?php
namespace Tests\Browser;
use App\Models\Prize;
use App\Models\Role;
use App\Models\User;
use App\Models\UserRole;
use Illuminate\Testing\TestResponse;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* AdminEdgeCaseTest — Kiểm thử các trường hợp biên (edge cases) trong khu vực admin.
*
* [TC-55] test_prize_number_cannot_be_negative
* [TC-56] test_invalid_logo_upload_is_rejected
* [TC-57] test_prize_winner_export_excel_response
*/
class AdminEdgeCaseTest extends DuskTestCase
{
private User $adminUser;
protected function setUp(): void
{
parent::setUp();
$this->adminUser = User::create([
'name' => 'DUSK EDGE ADMIN',
'email' => 'dusk_edge_admin@test.local',
'phone' => '09' . rand(10000000, 99999999),
'password' => bcrypt('dusk_admin_pwd_2026'),
'status' => 1,
'address' => 'Test',
'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]);
}
}
protected function tearDown(): void
{
UserRole::where('user_id', $this->adminUser->id)->delete();
$this->adminUser->delete();
// Clean up any prizes created during TC-55 if validation was missing
Prize::where('prize_name', 'Dusk Negative Number Prize Test')->delete();
parent::tearDown();
}
// =========================================================================
// [TC-55] Số lượng giải thưởng không được là số âm
// =========================================================================
/**
* Nhập prize_number = -5 vào form thêm giải thưởng phải bị từ chối.
*
* ⚠️ TRẠNG THÁI: Test này có thể FAIL với code hiện tại.
*
* Lý do: PrizeController::updatePrize() validate:
* 'prize_number' => 'nullable|numeric'
* Thiếu quy tắc `min:0` → số âm được chấp nhận (bug).
*
* CÁch sửa để test pass:
* Trong PrizeController::updatePrize(), thay dòng:
* 'prize_number' => 'nullable|numeric',
* Bằng:
* 'prize_number' => 'nullable|numeric|min:0',
* Và thêm message lỗi tương ứng vào mảng $messages.
*/
public function test_prize_number_cannot_be_negative(): void
{
$testPrizeName = 'Dusk Negative Number Prize Test';
$this->browse(function (Browser $browser) use ($testPrizeName) {
$browser->loginAs($this->adminUser)
->visit('/admin/prize/update?id=0')
->pause(300);
// Xóa các thuộc tính HTML5 validation để bypass client-side check
// và buộc server-side validation chạy
$browser->script("
document.querySelectorAll('input[required]').forEach(el => el.removeAttribute('required'));
document.querySelectorAll('input[min]').forEach(el => el.removeAttribute('min'));
document.querySelectorAll('input[type=\"number\"]').forEach(el => {
el.removeAttribute('min');
el.removeAttribute('step');
});
");
$browser->type('prize_name', $testPrizeName)
->clear('prize_number')
->type('prize_number', '-5')
->type('prize_order', '10')
->select('prize_status', '1')
->press('Thêm mới')
->pause(600);
// Kết quả mong muốn: thông báo lỗi validation hiển thị
$browser->assertPresent('.text-danger, .alert-danger, [class*="error"]');
});
// DB assertion: Prize với tên test và prize_number âm KHÔNG được tạo
$wrongPrize = Prize::where('prize_name', $testPrizeName)->first();
$this->assertNull(
$wrongPrize,
"Prize với prize_number = -5 không được phép tạo trong DB. "
. "Cần thêm 'min:0' vào validation rule của PrizeController."
);
}
// =========================================================================
// [TC-56] Upload file không hợp lệ vào ô logo phải bị từ chối
// =========================================================================
/**
* Upload file PDF (.pdf) vào trường logo_image phải bị server từ chối.
*
* ⚠️ TRẠNG THÁI: Test này có thể FAIL với code hiện tại.
*
* Lý do: ParameterController::update() → updateParameterImage() không
* validate MIME type của file upload. Mọi định dạng file đều được chấp nhận.
* Đây là lỗ hổng bảo mật (OWASP A05: Security Misconfiguration / Unrestricted Upload).
*
* Cách sửa để test pass:
* Thêm validation vào ParameterController::update() trước vòng foreach:
* $request->validate([
* 'logo_image' => 'nullable|mimes:jpg,png,jpeg,webp|max:2048',
* 'banner_image' => 'nullable|mimes:jpg,png,jpeg,webp|max:2048',
* ], [
* 'logo_image.mimes' => 'Logo chỉ chấp nhận định dạng jpg, png, jpeg, webp',
* 'banner_image.mimes' => 'Banner chỉ chấp nhận định dạng jpg, png, jpeg, webp',
* ]);
*
* CẢNH BÁO QUAN TRỌNG: Không bao giờ cho phép upload .php hoặc executable file
* lên web server — có thể dẫn đến Remote Code Execution (RCE).
*/
public function test_invalid_logo_upload_is_rejected(): void
{
// Tạo file PDF giả tạm thời để test upload
$fakePdfPath = sys_get_temp_dir() . '/dusk_test_fake_logo.pdf';
file_put_contents($fakePdfPath, '%PDF-1.4 this is a fake pdf for dusk test only');
try {
$this->browse(function (Browser $browser) use ($fakePdfPath) {
$browser->loginAs($this->adminUser)
->visit('/admin/parameter')
->pause(300);
// Attach file PDF vào input[name="logo_image"]
$browser->attach('logo_image', $fakePdfPath);
$browser->press('Cập nhật')
->pause(800);
// Kết quả mong muốn: lỗi validation về kiểu file hiển thị
$browser->assertPresent('.text-danger, .alert-danger, [class*="error"]');
// Không được hiển thị thông báo thành công
$browser->assertDontSee('Cập nhật thành công');
});
} finally {
// Xóa file tạm dù test pass hay fail
if (file_exists($fakePdfPath)) {
unlink($fakePdfPath);
}
}
}
// =========================================================================
// [TC-57] Nút export Excel tồn tại, href đúng, và tải file thành công
// =========================================================================
/**
* Kiểm tra chức năng export Excel danh sách người trúng giải theo 2 cách:
*
* A) Browser test (Dusk):
* - #excel_export tồn tại trên trang
* - href của nó trỏ đến route admin/prizeuser/export
*
* B) HTTP test (PHPUnit trực tiếp):
* - Response từ /admin/prizeuser/export trả về HTTP 200
* - Content-Type là application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
* - Header Content-Disposition chứa tên file .xlsx
*
* LƯU Ý BẢO MẬT: Route /admin/prizeuser/export hiện đặt NGOÀI middleware
* UserRole → bất kỳ ai (kể cả guest) đều có thể tải file export.
* Khuyến nghị: di chuyển route vào trong group middleware UserRole.
*/
public function test_prize_winner_export_excel_response(): void
{
// ----------------------------------------------------------------
// PHẦN A: Browser test — kiểm tra UI của trang prizeuser
//
// LƯU Ý: #excel_export là thẻ <a> KHÔNG có static href attribute.
// Href được tạo động trong JS onclick handler:
// let a = document.createElement("a");
// a.href = exportRouteUrl + '?fromdate=...&todate=...';
// document.body.appendChild(a); a.click();
// Vì vậy KHÔNG thể dùng attribute('#excel_export', 'href').
// ----------------------------------------------------------------
$this->browse(function (Browser $browser) {
$browser->loginAs($this->adminUser)
->visit('/admin/prizeuser')
->assertPresent('#excel_export');
// Lấy URL export từ JavaScript (route được inject vào view bằng Blade)
// và kiểm tra rằng nó trỏ đúng đến route admin/prizeuser/export
$exportRouteUrl = $browser->script("
// Trích xuất URL từ onclick handler bằng cách tạm thời override createElement
var capturedHref = null;
var origCreate = document.createElement.bind(document);
document.createElement = function(tag) {
var el = origCreate(tag);
if (tag.toLowerCase() === 'a') {
Object.defineProperty(el, 'href', {
set: function(v) { capturedHref = v; this._href = v; },
get: function() { return this._href || ''; }
});
}
return el;
};
document.getElementById('excel_export').click();
document.createElement = origCreate;
return capturedHref;
")[0];
$this->assertNotNull(
$exportRouteUrl,
'#excel_export onclick phải tạo anchor với href khi được click'
);
$this->assertStringContainsString(
'prizeuser/export',
$exportRouteUrl,
'URL export phải trỏ đến route admin/prizeuser/export'
);
});
// ----------------------------------------------------------------
// PHẦN B: Kiểm tra route tồn tại qua PHP route() helper
//
// LƯU Ý: Không dùng $this->actingAs()->get() vì view excel.blade.php
// gọi exit() sau khi xuất file, điều này sẽ terminate PHPUnit process.
// ----------------------------------------------------------------
$exportUrl = route('admin/prizeuser/export');
$this->assertStringContainsString(
'/admin/prizeuser/export',
$exportUrl,
"Route 'admin/prizeuser/export' phải tồn tại và trả về URL hợp lệ"
);
}
}