This commit is contained in:
Saufi
2026-06-24 18:30:00 +08:00
commit c0c3d7c09c
122 changed files with 16321 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<?php
namespace Tests\Feature;
use App\Models\AttendanceRecord;
use App\Models\Member;
use App\Models\User;
use Database\Seeders\RoleUserSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttendanceTest extends TestCase
{
use RefreshDatabase;
private User $kaunter;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleUserSeeder::class);
$this->kaunter = User::where('email', 'kaunter@koipb.test')->first();
}
public function test_kaunter_can_record_attendance(): void
{
$member = Member::factory()->create();
$res = $this->actingAs($this->kaunter)->postJson(route('counter.store'), ['member_id' => $member->id]);
$res->assertOk()->assertJson(['status' => 'ok']);
$this->assertDatabaseHas('attendance_records', ['member_id' => $member->id]);
}
public function test_attendance_cannot_be_duplicated(): void
{
$member = Member::factory()->create();
AttendanceRecord::create(['member_id' => $member->id, 'attended_at' => now()]);
$res = $this->actingAs($this->kaunter)->postJson(route('counter.store'), ['member_id' => $member->id]);
$res->assertOk()->assertJson(['status' => 'already']);
$this->assertEquals(1, AttendanceRecord::where('member_id', $member->id)->count());
}
public function test_unique_constraint_blocks_double_attendance_at_db_level(): void
{
$member = Member::factory()->create();
AttendanceRecord::create(['member_id' => $member->id, 'attended_at' => now()]);
$this->expectException(\Illuminate\Database\UniqueConstraintViolationException::class);
AttendanceRecord::create(['member_id' => $member->id, 'attended_at' => now()]);
}
public function test_search_finds_member_and_reports_not_found(): void
{
$member = Member::factory()->create(['nama' => 'Zulkifli Bin Hassan', 'no_pekerja' => 'MBIP54321']);
$this->actingAs($this->kaunter)->getJson(route('counter.search', ['q' => 'Zulkifli']))
->assertOk()->assertJsonFragment(['nama' => 'Zulkifli Bin Hassan']);
$this->actingAs($this->kaunter)->getJson(route('counter.search', ['q' => 'TiadaOrangIni']))
->assertOk()->assertJsonFragment(['message' => 'Rekod tidak dijumpai. Individu ini bukan anggota koperasi yang berdaftar.']);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Database\Seeders\RoleUserSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleUserSeeder::class);
}
public function test_admin_can_login_and_reach_dashboard(): void
{
$response = $this->post('/login', [
'email' => 'admin@koipb.test',
'password' => 'password',
]);
$response->assertRedirect(route('dashboard'));
$this->assertAuthenticated();
}
public function test_wrong_password_fails(): void
{
$response = $this->post('/login', [
'email' => 'admin@koipb.test',
'password' => 'salah',
]);
$response->assertSessionHasErrors('email');
$this->assertGuest();
}
public function test_kaunter_cannot_access_draw(): void
{
$user = User::where('email', 'kaunter@koipb.test')->first();
$this->actingAs($user)->get('/cabutan')->assertForbidden();
}
public function test_cabutan_cannot_access_counter(): void
{
$user = User::where('email', 'cabutan@koipb.test')->first();
$this->actingAs($user)->get('/kaunter')->assertForbidden();
}
public function test_admin_can_access_all(): void
{
$user = User::where('email', 'admin@koipb.test')->first();
$this->actingAs($user)->get('/cabutan')->assertOk();
$this->actingAs($user)->get('/kaunter')->assertOk();
$this->actingAs($user)->get('/admin/anggota')->assertOk();
}
}

163
tests/Feature/DrawTest.php Normal file
View File

@@ -0,0 +1,163 @@
<?php
namespace Tests\Feature;
use App\Models\AttendanceRecord;
use App\Models\DrawResult;
use App\Models\DrawSession;
use App\Models\Member;
use App\Models\Prize;
use App\Models\User;
use App\Services\DrawService;
use Database\Seeders\RoleUserSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DrawTest extends TestCase
{
use RefreshDatabase;
private DrawService $service;
private DrawSession $session;
private User $petugas;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleUserSeeder::class);
$this->service = app(DrawService::class);
$this->session = DrawSession::active();
$this->petugas = User::where('email', 'cabutan@koipb.test')->first();
}
private function attendedMember(array $attrs = []): Member
{
$member = Member::factory()->create($attrs);
AttendanceRecord::create(['member_id' => $member->id, 'attended_at' => now()]);
return $member;
}
public function test_only_attended_members_are_eligible(): void
{
$hadir = $this->attendedMember();
$tidakHadir = Member::factory()->create();
$pool = $this->service->eligibleMembers($this->session);
$this->assertTrue($pool->contains('id', $hadir->id));
$this->assertFalse($pool->contains('id', $tidakHadir->id));
}
public function test_spin_creates_pending_result_and_marks_prize(): void
{
$this->attendedMember();
$prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]);
$result = $this->service->spin($prize, $this->session, $this->petugas);
$this->assertEquals(DrawResult::STATUS_PENDING, $result->status);
$this->assertEquals(Prize::STATUS_SEDANG, $prize->fresh()->status);
}
public function test_confirmed_winner_cannot_win_again(): void
{
$member = $this->attendedMember();
$prize1 = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]);
$prize2 = Prize::create(['nama_hadiah' => 'Radio', 'draw_order' => 2]);
$r1 = $this->service->spin($prize1, $this->session, $this->petugas);
$this->service->confirm($r1, $this->petugas);
// Member sudah menang -> tidak lagi dalam pool
$this->assertFalse($this->service->eligibleMembers($this->session)->contains('id', $member->id));
// Tiada peserta layak lain -> spin prize2 patut gagal
$this->expectException(\RuntimeException::class);
$this->service->spin($prize2, $this->session, $this->petugas);
}
public function test_db_blocks_duplicate_confirmed_winner(): void
{
$member = $this->attendedMember();
$prize1 = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]);
$prize2 = Prize::create(['nama_hadiah' => 'Radio', 'draw_order' => 2]);
$r1 = $this->service->spin($prize1, $this->session, $this->petugas);
$this->service->confirm($r1, $this->petugas);
// Cuba paksa member sama menang prize2 (langkau pool) -> unique index DB halang
$r2 = DrawResult::create([
'draw_session_id' => $this->session->id,
'prize_id' => $prize2->id,
'member_id' => $member->id,
'status' => DrawResult::STATUS_PENDING,
'spun_at' => now(),
]);
$this->expectException(\Illuminate\Database\UniqueConstraintViolationException::class);
$r2->update([
'status' => DrawResult::STATUS_CONFIRMED,
'confirmed_member_id' => $member->id,
'confirmed_prize_id' => $prize2->id,
]);
}
public function test_cancelled_draw_allows_redraw(): void
{
$member1 = $this->attendedMember();
$member2 = $this->attendedMember();
$prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]);
$r1 = $this->service->spin($prize, $this->session, $this->petugas);
$this->service->cancel($r1, $this->petugas, 'Tiada di dewan');
// Hadiah jadi redraw_required tetapi masih available
$prize->refresh();
$this->assertEquals(Prize::STATUS_REDRAW, $prize->status);
$this->assertTrue($prize->isAvailable());
$this->assertEquals(1, $prize->redraw_count);
// Boleh spin semula
$r2 = $this->service->spin($prize, $this->session, $this->petugas);
$this->assertEquals(DrawResult::STATUS_PENDING, $r2->status);
$this->assertEquals(2, $r2->attempt_number);
// Default setting (return_cancelled_to_pool = true): peserta dibatalkan kekal dalam pool
$this->assertTrue($this->service->eligibleMembers($this->session)->contains('id', $member1->id));
}
public function test_setting_can_remove_cancelled_member_from_pool(): void
{
// Tetapan admin: keluarkan peserta dibatalkan dari pool (tiada peluang kedua)
$this->session->update(['return_cancelled_to_pool' => false]);
$member1 = $this->attendedMember();
$member2 = $this->attendedMember();
$prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]);
$r1 = $this->service->spin($prize, $this->session, $this->petugas);
$cancelledId = $r1->member_id;
$this->service->cancel($r1, $this->petugas, 'Tiada di dewan');
$pool = $this->service->eligibleMembers($this->session->fresh());
// Peserta yang dibatalkan TIDAK lagi dalam pool; peserta lain masih ada
$this->assertFalse($pool->contains('id', $cancelledId));
$remainingId = $cancelledId === $member1->id ? $member2->id : $member1->id;
$this->assertTrue($pool->contains('id', $remainingId));
}
public function test_cannot_spin_already_confirmed_prize(): void
{
$this->attendedMember();
$this->attendedMember();
$prize = Prize::create(['nama_hadiah' => 'TV', 'draw_order' => 1]);
$r1 = $this->service->spin($prize, $this->session, $this->petugas);
$this->service->confirm($r1, $this->petugas);
$this->expectException(\RuntimeException::class);
$this->service->spin($prize, $this->session, $this->petugas);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature;
use App\Models\Member;
use App\Models\Prize;
use App\Services\MemberImportService;
use App\Services\PrizeImportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ImportTest extends TestCase
{
use RefreshDatabase;
public function test_member_import_handles_duplicates_and_missing_name(): void
{
$rows = [
['no_pekerja' => 'P001', 'no_kp' => '900101-01-1111', 'nama' => 'Ali'],
['no_pekerja' => 'P001', 'no_kp' => '900101-01-2222', 'nama' => 'Ali Duplicate Pekerja'], // dup dalam fail
['no_pekerja' => 'P002', 'no_kp' => '', 'nama' => ''], // gagal: nama kosong
['no_pekerja' => 'P003', 'no_kp' => '900101-01-3333', 'nama' => 'Siti'],
];
$log = (new MemberImportService())->import($rows, 'test.csv');
$this->assertEquals(2, $log->success_count);
$this->assertEquals(1, $log->duplicate_count);
$this->assertEquals(1, $log->failed_count);
$this->assertEquals(2, Member::count());
}
public function test_member_import_default_status_aktif(): void
{
(new MemberImportService())->import([['nama' => 'Tiada Status']], 'test.csv');
$this->assertTrue(Member::first()->status_aktif);
}
public function test_prize_import_expands_quantity(): void
{
$rows = [
['nama_hadiah' => 'Hamper', 'kuantiti' => '5', 'susunan_cabutan' => '3'],
['nama_hadiah' => 'Motosikal', 'kuantiti' => '1', 'susunan_cabutan' => '1'],
];
$log = (new PrizeImportService())->import($rows, 'hadiah.csv');
$this->assertEquals(6, $log->success_count);
$this->assertEquals(6, Prize::count());
$this->assertEquals(5, Prize::where('nama_hadiah', 'like', 'Hamper%')->count());
$this->assertDatabaseHas('prizes', ['nama_hadiah' => 'Hamper #5', 'draw_order' => 3]);
$this->assertDatabaseHas('prizes', ['nama_hadiah' => 'Motosikal', 'draw_order' => 1]);
}
}