first commit

This commit is contained in:
Saufi
2026-06-24 20:32:14 +08:00
commit 10fb30ad69
201 changed files with 21356 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<?php
namespace Tests\Feature;
use App\Models\Consultant;
use App\Models\DataCentre;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AssignmentTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seedRolesAndPermissions();
}
public function test_jpp_can_assign_consultant_to_data_centre(): void
{
$jpp = $this->makeUserWithRole('Pegawai JPP');
$dc = DataCentre::factory()->create();
$consultant = Consultant::factory()->create();
$this->actingAs($jpp)->post(route('data-centres.assign', $dc), [
'consultant_id' => $consultant->id,
'start_date' => now()->toDateString(),
'reason' => 'Penugasan awal',
])->assertRedirect();
$dc->refresh();
$this->assertEquals($consultant->id, $dc->current_consultant_id);
$this->assertDatabaseHas('data_centre_consultant_assignments', [
'data_centre_id' => $dc->id,
'consultant_id' => $consultant->id,
'is_active' => true,
]);
}
public function test_only_one_active_consultant_per_data_centre(): void
{
$jpp = $this->makeUserWithRole('Pegawai JPP');
$dc = DataCentre::factory()->create();
$first = Consultant::factory()->create();
$second = Consultant::factory()->create();
$this->actingAs($jpp)->post(route('data-centres.assign', $dc), [
'consultant_id' => $first->id,
]);
// Tugaskan perunding kedua — penugasan pertama mesti ditamatkan.
$this->actingAs($jpp)->post(route('data-centres.assign', $dc), [
'consultant_id' => $second->id,
'reason' => 'Tukar perunding',
]);
$dc->refresh();
$this->assertEquals($second->id, $dc->current_consultant_id);
// Hanya satu penugasan aktif.
$activeCount = $dc->assignments()->where('is_active', true)->count();
$this->assertEquals(1, $activeCount);
// Penugasan pertama ditamatkan (ada end_date).
$this->assertDatabaseHas('data_centre_consultant_assignments', [
'data_centre_id' => $dc->id,
'consultant_id' => $first->id,
'is_active' => false,
]);
// Sejarah dikekalkan: dua rekod penugasan.
$this->assertEquals(2, $dc->assignments()->count());
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Tests\Feature;
use App\Models\Consultant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ConsultantManagementTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seedRolesAndPermissions();
}
public function test_jpp_can_create_consultant(): void
{
$jpp = $this->makeUserWithRole('JPP Admin');
$response = $this->actingAs($jpp)->post(route('consultants.store'), [
'nama_perunding' => 'Perunding Hijau Sdn Bhd',
'no_pendaftaran_syarikat' => '202301000999',
'emel' => 'hijau@example.com',
'telefon' => '07-9998888',
'aktif' => 1,
]);
$this->assertDatabaseHas('consultants', ['nama_perunding' => 'Perunding Hijau Sdn Bhd']);
$response->assertRedirect();
}
public function test_jpp_can_create_consultant_with_login_account(): void
{
$jpp = $this->makeUserWithRole('JPP Admin');
$this->actingAs($jpp)->post(route('consultants.store'), [
'nama_perunding' => 'Perunding Akaun Sdn Bhd',
'aktif' => 1,
'buat_akaun' => 1,
'user_name' => 'Perunding Akaun',
'user_email' => 'akaun@example.com',
'user_password' => 'rahsia12345',
]);
$this->assertDatabaseHas('users', ['email' => 'akaun@example.com']);
$consultant = Consultant::where('nama_perunding', 'Perunding Akaun Sdn Bhd')->first();
$this->assertNotNull($consultant->user_id);
$this->assertTrue($consultant->user->hasRole('Perunding'));
}
public function test_perunding_cannot_create_consultant(): void
{
[$user] = $this->makeConsultantUser();
$this->actingAs($user)->post(route('consultants.store'), [
'nama_perunding' => 'Tidak Dibenarkan',
'aktif' => 1,
])->assertForbidden();
$this->assertDatabaseMissing('consultants', ['nama_perunding' => 'Tidak Dibenarkan']);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
// Halaman utama mengalihkan ke papan pemuka (yang seterusnya menuntut log masuk).
$response = $this->get('/');
$response->assertRedirect(route('dashboard'));
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Tests\Feature;
use App\Models\ChecklistTemplate;
use App\Services\ExpressionEvaluator;
use App\Services\FormulaService;
use Database\Seeders\ChecklistTemplateSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class FormulaTest extends TestCase
{
use RefreshDatabase;
public function test_formula_calculation_works_for_a_b_c_d_e(): void
{
$this->seed(ChecklistTemplateSeeder::class);
$template = ChecklistTemplate::activeTemplate();
$service = app(FormulaService::class);
$vars = ['SKOP1' => 100, 'SKOP2' => 200, 'SKOP3' => 50, 'EE' => 30, 'RE' => 20, 'D4I' => 10, 'D4II' => 15];
$results = collect($service->compute($template, $vars))->keyBy('result_token');
$this->assertEquals(350, $results['A']['value']); // 100 + 200 + 50
$this->assertEquals(50, $results['B']['value']); // 30 + 20
$this->assertEquals(300, $results['C']['value']); // A - B
$this->assertEquals(325, $results['D']['value']); // A - 10 - 15
$this->assertEqualsWithDelta(92.857, $results['E']['value'], 0.01); // D / A * 100
}
public function test_division_by_zero_is_guarded(): void
{
$this->seed(ChecklistTemplateSeeder::class);
$template = ChecklistTemplate::activeTemplate();
$service = app(FormulaService::class);
// Semua sifar => A = 0, formula E (D/A*100) mesti mengembalikan 0, bukan ralat.
$vars = ['SKOP1' => 0, 'SKOP2' => 0, 'SKOP3' => 0, 'EE' => 0, 'RE' => 0, 'D4I' => 0, 'D4II' => 0];
$results = collect($service->compute($template, $vars))->keyBy('result_token');
$this->assertEquals(0, $results['E']['value']);
}
public function test_expression_evaluator_respects_operator_precedence(): void
{
$eval = app(ExpressionEvaluator::class);
$this->assertEquals(14, $eval->evaluate('2 + 3 * 4'));
$this->assertEquals(20, $eval->evaluate('(2 + 3) * 4'));
$this->assertEquals(5, $eval->evaluate('A + B', ['A' => 2, 'B' => 3]));
}
public function test_expression_evaluator_rejects_invalid_characters(): void
{
$this->expectException(\RuntimeException::class);
app(ExpressionEvaluator::class)->evaluate('A; DROP TABLE', ['A' => 1]);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Tests\Feature;
use App\Models\DataCentre;
use App\Models\ReportingCycle;
use App\Models\Submission;
use App\Services\SubmissionWorkflowService;
use Database\Seeders\ChecklistTemplateSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ReminderTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seedRolesAndPermissions();
$this->seed(ChecklistTemplateSeeder::class);
}
public function test_reminder_list_removes_consultant_after_submission(): void
{
$jpp = $this->makeUserWithRole('Pegawai JPP');
[$cUser, $consultant] = $this->makeConsultantUser();
$cycle = ReportingCycle::factory()->create();
$dc = DataCentre::factory()->create(['current_consultant_id' => $consultant->id]);
// Pada mulanya pusat data muncul dalam senarai peringatan.
$response = $this->actingAs($jpp)->get(route('reminders.index', ['cycle' => $cycle->id]));
$response->assertOk();
$response->assertSee($dc->nama_pusat_data);
// Perunding hantar serahan.
$submission = Submission::create([
'data_centre_id' => $dc->id,
'reporting_cycle_id' => $cycle->id,
'consultant_id' => $consultant->id,
'status' => 'baru',
'submitted_at' => now(),
]);
// Selepas hantar, pusat data dikeluarkan daripada senarai peringatan.
$response = $this->actingAs($jpp)->get(route('reminders.index', ['cycle' => $cycle->id]));
$response->assertOk();
$response->assertDontSee($dc->nama_pusat_data);
}
public function test_draft_submission_still_appears_in_reminder_list(): void
{
$jpp = $this->makeUserWithRole('Pegawai JPP');
[$cUser, $consultant] = $this->makeConsultantUser();
$cycle = ReportingCycle::factory()->create();
$dc = DataCentre::factory()->create(['current_consultant_id' => $consultant->id]);
// Hanya draf (belum hantar) — masih dalam senarai peringatan.
app(SubmissionWorkflowService::class)->findOrCreateDraft($dc, $cycle);
$response = $this->actingAs($jpp)->get(route('reminders.index', ['cycle' => $cycle->id]));
$response->assertSee($dc->nama_pusat_data);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Tests\Feature;
use App\Models\ReportingCycle;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ReportingCycleTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seedRolesAndPermissions();
}
public function test_reporting_year_is_calculated_from_renewal_year(): void
{
// Peraturan: pembaharuan 2028 => pelaporan 2026.
$this->assertEquals(2026, ReportingCycle::reportingYearFor(2028));
$this->assertEquals(2025, ReportingCycle::reportingYearFor(2027));
}
public function test_jpp_can_create_cycle_with_calculated_reporting_year(): void
{
$jpp = $this->makeUserWithRole('JPP Admin');
$this->actingAs($jpp)->post(route('reporting-cycles.store'), [
'renewal_year' => 2028,
'licence_period_year' => 1,
'status' => 'aktif',
])->assertRedirect();
$cycle = ReportingCycle::where('renewal_year', 2028)->first();
$this->assertNotNull($cycle);
$this->assertEquals(2026, $cycle->reporting_year);
$this->assertEquals('2026-01-01', $cycle->period_start->toDateString());
$this->assertEquals('2026-12-31', $cycle->period_end->toDateString());
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace Tests\Feature;
use App\Models\Consultant;
use App\Models\DataCentre;
use App\Models\ReportingCycle;
use App\Models\Submission;
use App\Services\SubmissionWorkflowService;
use Database\Seeders\ChecklistTemplateSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class SubmissionTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seedRolesAndPermissions();
$this->seed(ChecklistTemplateSeeder::class);
}
/** Cipta perunding+akaun, pusat data yang ditugaskan & kitaran aktif. */
protected function scenario(): array
{
[$user, $consultant] = $this->makeConsultantUser();
$cycle = ReportingCycle::factory()->create();
$dc = DataCentre::factory()->create(['current_consultant_id' => $consultant->id]);
return [$user, $consultant, $dc, $cycle];
}
public function test_consultant_can_submit_checklist_and_pdf(): void
{
Storage::fake('local');
[$user, $consultant, $dc, $cycle] = $this->scenario();
// Cipta draf serahan.
$this->actingAs($user)->post(route('submissions.store'), [
'data_centre_id' => $dc->id,
'reporting_cycle_id' => $cycle->id,
])->assertRedirect();
$submission = Submission::first();
$this->assertEquals('draf', $submission->status);
// Simpan jawapan.
$skop1 = \App\Models\ChecklistItem::where('formula_token', 'SKOP1')->first();
$this->actingAs($user)->put(route('submissions.update', $submission), [
'answers' => [$skop1->id => ['value' => '123', 'page_reference' => '12']],
])->assertRedirect();
$this->assertDatabaseHas('submission_answers', [
'submission_id' => $submission->id,
'checklist_item_id' => $skop1->id,
'value' => '123',
'page_reference' => '12',
]);
// Muat naik laporan PDF.
$this->actingAs($user)->post(route('documents.store', $submission), [
'report' => UploadedFile::fake()->create('laporan.pdf', 500, 'application/pdf'),
])->assertRedirect();
$this->assertDatabaseHas('submission_documents', [
'submission_id' => $submission->id,
'version' => 1,
'is_active' => true,
]);
// Hantar serahan.
$this->actingAs($user)->post(route('submissions.submit', $submission))->assertRedirect();
$submission->refresh();
$this->assertEquals('baru', $submission->status);
$this->assertNotNull($submission->submitted_at);
}
public function test_submission_cannot_be_submitted_without_pdf(): void
{
[$user, $consultant, $dc, $cycle] = $this->scenario();
$submission = app(SubmissionWorkflowService::class)->findOrCreateDraft($dc, $cycle);
$this->actingAs($user)->post(route('submissions.submit', $submission))
->assertSessionHasErrors('document');
$submission->refresh();
$this->assertEquals('draf', $submission->status);
}
public function test_consultant_cannot_edit_locked_submission(): void
{
[$user, $consultant, $dc, $cycle] = $this->scenario();
$submission = Submission::create([
'data_centre_id' => $dc->id,
'reporting_cycle_id' => $cycle->id,
'consultant_id' => $consultant->id,
'status' => 'baru',
'is_locked' => true,
]);
$this->actingAs($user)->get(route('submissions.edit', $submission))->assertForbidden();
$this->actingAs($user)->put(route('submissions.update', $submission), [
'answers' => [],
])->assertForbidden();
}
public function test_consultant_cannot_access_other_consultant_data_centre(): void
{
[$user, $consultant, $dc, $cycle] = $this->scenario();
// Pusat data milik perunding lain.
$otherConsultant = Consultant::factory()->create();
$otherDc = DataCentre::factory()->create(['current_consultant_id' => $otherConsultant->id]);
$this->actingAs($user)->get(route('data-centres.show', $otherDc))->assertForbidden();
// Serahan milik perunding lain.
$otherSubmission = Submission::create([
'data_centre_id' => $otherDc->id,
'reporting_cycle_id' => $cycle->id,
'consultant_id' => $otherConsultant->id,
'status' => 'baru',
]);
$this->actingAs($user)->get(route('submissions.show', $otherSubmission))->assertForbidden();
// Tetapi pusat data sendiri boleh diakses.
$this->actingAs($user)->get(route('data-centres.show', $dc))->assertOk();
}
public function test_jpp_can_change_status_to_pembetulan_only_with_correction_details(): void
{
[$user, $consultant, $dc, $cycle] = $this->scenario();
$jpp = $this->makeUserWithRole('Pegawai JPP');
$submission = Submission::create([
'data_centre_id' => $dc->id,
'reporting_cycle_id' => $cycle->id,
'consultant_id' => $consultant->id,
'status' => 'dalam_tindakan',
]);
// Tanpa permohonan pembetulan terbuka — status tidak boleh ditukar.
$this->actingAs($jpp)->post(route('submissions.status', $submission), [
'status' => 'pembetulan_perunding',
])->assertSessionHas('error');
$submission->refresh();
$this->assertEquals('dalam_tindakan', $submission->status);
// Tambah permohonan pembetulan (no m/s, lokasi, penerangan wajib).
$this->actingAs($jpp)->post(route('corrections.store', $submission), [
'page_reference' => '8',
'location' => 'Seksyen A / Skop 1',
'description' => 'Nilai Skop 1 tidak konsisten dengan lampiran.',
])->assertRedirect();
// Kini status boleh ditukar.
$this->actingAs($jpp)->post(route('submissions.status', $submission), [
'status' => 'pembetulan_perunding',
])->assertRedirect();
$submission->refresh();
$this->assertEquals('pembetulan_perunding', $submission->status);
}
public function test_status_selesai_requires_hardcopy_sent_date(): void
{
[$user, $consultant, $dc, $cycle] = $this->scenario();
$jpp = $this->makeUserWithRole('Pegawai JPP');
$submission = Submission::create([
'data_centre_id' => $dc->id,
'reporting_cycle_id' => $cycle->id,
'consultant_id' => $consultant->id,
'status' => 'dalam_tindakan',
]);
$this->actingAs($jpp)->post(route('submissions.status', $submission), [
'status' => 'selesai',
])->assertSessionHasErrors('hardcopy_sent_date');
$this->actingAs($jpp)->post(route('submissions.status', $submission), [
'status' => 'selesai',
'hardcopy_sent_date' => now()->toDateString(),
])->assertRedirect();
$submission->refresh();
$this->assertEquals('selesai', $submission->status);
$this->assertNotNull($submission->hardcopy_sent_date);
}
public function test_locked_submission_can_be_unlocked_by_officer_with_reason(): void
{
[$user, $consultant, $dc, $cycle] = $this->scenario();
$jpp = $this->makeUserWithRole('Pegawai JPP');
$submission = Submission::create([
'data_centre_id' => $dc->id,
'reporting_cycle_id' => $cycle->id,
'consultant_id' => $consultant->id,
'status' => 'dalam_tindakan',
'is_locked' => true,
]);
$this->actingAs($jpp)->post(route('submissions.unlock', $submission), [
'lock_reason' => 'Dibuka untuk pembetulan tambahan.',
])->assertRedirect();
$submission->refresh();
$this->assertFalse($submission->is_locked);
}
}

34
tests/TestCase.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace Tests;
use App\Models\Consultant;
use App\Models\User;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
/** Semai peranan & kebenaran untuk ujian. */
protected function seedRolesAndPermissions(): void
{
$this->seed(RolesAndPermissionsSeeder::class);
}
protected function makeUserWithRole(string $role, array $attributes = []): User
{
$user = User::factory()->create($attributes);
$user->assignRole($role);
return $user;
}
/** Cipta pengguna perunding + profil perunding yang berkait. */
protected function makeConsultantUser(array $consultantAttributes = []): array
{
$user = $this->makeUserWithRole('Perunding');
$consultant = Consultant::factory()->create(array_merge(['user_id' => $user->id], $consultantAttributes));
return [$user, $consultant];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}