137 lines
9.0 KiB
PHP
137 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Course;
|
|
use App\Models\Occupation;
|
|
use App\Models\Project;
|
|
use App\Models\Skill;
|
|
use App\Models\TrainingProvider;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
final class EducationDeclarationDraftsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $owner;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
$this->owner = User::factory()->create();
|
|
config(['education.enabled' => true, 'portal.managers' => [$this->owner->email]]);
|
|
$this->actingAs($this->owner);
|
|
}
|
|
|
|
private function valid(string $kind = 'demands'): array
|
|
{
|
|
$province = (string) Str::uuid();
|
|
DB::table('geo_locations')->insert(['id' => $province, 'name' => 'SYNTHETIC QA province', 'code' => $province, 'location_type' => 'province']);
|
|
$data = ['occupation_id' => Occupation::create(['code' => (string) Str::uuid(), 'name' => 'Synthetic occupation'])->id,
|
|
'skill_id' => Skill::create(['code' => (string) Str::uuid(), 'name' => 'Synthetic skill'])->id, 'geo_location_id' => $province,
|
|
'qualification' => 'Synthetic qualification', 'quantity' => 20, 'period_start' => today()->addMonth()->toDateString(),
|
|
'period_end' => today()->addMonths(2)->toDateString(), 'source' => 'SENSITIVE SYNTHETIC QA SOURCE TEXT',
|
|
'reference_date' => today()->toDateString(), 'valid_until' => today()->addYear()->toDateString()];
|
|
if ($kind === 'demands') {
|
|
return [...$data, 'employer' => 'SENSITIVE SYNTHETIC QA EMPLOYER', 'project_id' => Project::create(['code' => (string) Str::uuid(), 'name' => 'Synthetic project', 'status' => 'draft'])->id];
|
|
}
|
|
$provider = TrainingProvider::create(['name' => 'Synthetic provider', 'status' => 'candidate']);
|
|
|
|
return [...$data, 'course_id' => Course::create(['provider_id' => $provider->id, 'code' => (string) Str::uuid(), 'title' => 'Synthetic course', 'status' => 'draft'])->id];
|
|
}
|
|
|
|
private function create(string $kind, array $payload = []): array
|
|
{
|
|
$input = ['client_key' => (string) Str::uuid(), 'version_no' => 0, 'payload' => $payload];
|
|
$response = $this->postJson('/management/education/drafts/'.$kind, $input)->assertOk()->assertHeader('Cache-Control', 'no-store, private');
|
|
|
|
return [$response->json('id'), $input];
|
|
}
|
|
|
|
public function test_incomplete_drafts_are_private_encrypted_recoverable_and_repeated_requests_do_not_duplicate(): void
|
|
{
|
|
[$id, $input] = $this->create('demands', ['employer' => 'PRIVATE SYNTHETIC EMPLOYER']);
|
|
$this->postJson('/management/education/drafts/demands', $input)->assertOk()->assertJsonPath('version_no', 1)->assertJsonPath('id', $id);
|
|
$this->assertDatabaseCount('education_declaration_drafts', 1);
|
|
$this->assertDatabaseCount('labour_demands', 0);
|
|
$this->assertStringNotContainsString('PRIVATE SYNTHETIC', DB::table('education_declaration_drafts')->sole()->payload);
|
|
$this->assertStringNotContainsString('PRIVATE SYNTHETIC', DB::table('audit_events')->where('source_system', 'education-drafts')->first()->after_data);
|
|
$other = User::factory()->create();
|
|
config(['portal.managers' => [$this->owner->email, $other->email]]);
|
|
$this->actingAs($other)->getJson('/management/education/drafts/demands/'.$id)->assertNotFound();
|
|
$this->get('/management/education?draft='.$id)->assertNotFound();
|
|
$this->get('/management/education')->assertOk()->assertInertia(fn ($page) => $page->has('ownDrafts.data', 0)->where('selectedDraft', null));
|
|
$this->getJson('/management/education/drafts/demands')->assertOk()->assertJsonCount(0, 'data');
|
|
$this->putJson('/management/education/drafts/demands/'.$id, ['version_no' => 1, 'payload' => []])->assertNotFound();
|
|
$this->actingAs(User::findOrFail($this->owner->id))->getJson('/management/education/drafts/demands/'.$id)->assertOk()->assertJsonPath('payload.employer', 'PRIVATE SYNTHETIC EMPLOYER');
|
|
$this->get('/management/education?draft='.$id)->assertOk()->assertHeader('Cache-Control', 'no-store, private')
|
|
->assertInertia(fn ($page) => $page->component('education/index')->where('selectedDraft.id', $id)
|
|
->where('selectedDraft.payload.employer', 'PRIVATE SYNTHETIC EMPLOYER')->has('ownDrafts.data', 1)->missing('ownDrafts.data.0.payload'));
|
|
$this->actingAs(User::factory()->create())->postJson('/management/education/drafts/demands', $input)->assertForbidden();
|
|
}
|
|
|
|
public function test_stale_edits_and_wrong_kind_are_rejected_while_incomplete_submission_creates_nothing(): void
|
|
{
|
|
[$id, $input] = $this->create('demands');
|
|
$url = '/management/education/drafts/demands/'.$id;
|
|
$this->putJson($url, ['version_no' => 1, 'payload' => ['quantity' => 0]])->assertOk()->assertJsonPath('version_no', 2);
|
|
$this->putJson($url, ['version_no' => 1, 'payload' => ['quantity' => 0]])->assertOk()->assertJsonPath('version_no', 2);
|
|
$this->putJson($url, ['version_no' => 1, 'payload' => ['quantity' => 5]])->assertConflict();
|
|
$this->postJson('/management/education/drafts/capacities', $input)->assertConflict();
|
|
$this->getJson('/management/education/drafts/capacities/'.$id)->assertNotFound();
|
|
$this->putJson($url, ['version_no' => 2, 'payload' => ['status' => 'reviewed']])->assertUnprocessable();
|
|
$this->postJson($url.'/submit', ['version_no' => 2, 'payload' => ['quantity' => 0]])->assertUnprocessable();
|
|
$this->assertDatabaseCount('labour_demands', 0);
|
|
$this->getJson($url)->assertJsonPath('state', 'draft');
|
|
}
|
|
|
|
public function test_distinct_numeric_text_is_not_collapsed_and_rollback_preserves_drafts(): void
|
|
{
|
|
[$id] = $this->create('demands', ['employer' => '001']);
|
|
$this->putJson('/management/education/drafts/demands/'.$id, ['version_no' => 1, 'payload' => ['employer' => '1']])->assertOk()
|
|
->assertJsonPath('version_no', 2)->assertJsonPath('payload.employer', '1');
|
|
$migration = require database_path('migrations/2026_11_02_230000_education_declaration_drafts.php');
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('Preserve education drafts');
|
|
$migration->down();
|
|
}
|
|
|
|
public function test_submission_is_single_preserves_snapshot_and_still_requires_independent_review(): void
|
|
{
|
|
$payload = $this->valid();
|
|
[$id] = $this->create('demands', ['employer' => $payload['employer']]);
|
|
$url = '/management/education/drafts/demands/'.$id;
|
|
$input = ['version_no' => 1, 'payload' => $payload];
|
|
$record = $this->postJson($url.'/submit', $input)->assertOk()->assertJsonPath('state', 'submitted')->json('record_id');
|
|
$this->postJson($url.'/submit', $input)->assertOk()->assertJsonPath('record_id', $record);
|
|
$this->assertDatabaseCount('labour_demands', 1);
|
|
$this->assertSame('submitted', DB::table('labour_demands')->sole()->status);
|
|
$this->postJson('/management/education/demands/'.$record.'/review', ['version_no' => 1, 'status' => 'reviewed', 'review_reason' => 'Synthetic self-review must be refused.'])->assertForbidden();
|
|
$this->putJson($url, ['version_no' => 2, 'payload' => $payload])->assertConflict();
|
|
$this->postJson($url.'/submit', [...$input, 'payload' => [...$payload, 'quantity' => 50]])->assertConflict();
|
|
Project::whereKey($payload['project_id'])->update(['name' => 'Changed synthetic project']);
|
|
$this->getJson($url)->assertJsonPath('references.project_id.name', 'Synthetic project')->assertJsonPath('payload.quantity', 20);
|
|
$this->get('/management/education?draft='.$id)->assertOk()->assertInertia(fn ($page) => $page->where('selectedDraft.state', 'submitted')
|
|
->where('selectedDraft.references.project_id.name', 'Synthetic project')->where('ownDrafts.data.0.label', 'Synthetic qualification'));
|
|
}
|
|
|
|
public function test_capacity_reuses_final_validation_and_does_not_accept_employer_or_project_fields(): void
|
|
{
|
|
$payload = $this->valid('capacities');
|
|
[$id] = $this->create('capacities', [...$payload, 'employer' => 'NOT APPLICABLE', 'project_id' => 'not-applicable']);
|
|
$url = '/management/education/drafts/capacities/'.$id;
|
|
$this->getJson($url)->assertJsonMissingPath('payload.employer')->assertJsonMissingPath('payload.project_id');
|
|
Course::whereKey($payload['course_id'])->update(['status' => 'retired']);
|
|
$this->postJson($url.'/submit', ['version_no' => 1, 'payload' => $payload])->assertUnprocessable();
|
|
Course::whereKey($payload['course_id'])->update(['status' => 'draft']);
|
|
$this->postJson($url.'/submit', ['version_no' => 1, 'payload' => $payload])->assertOk()->assertJsonPath('state', 'submitted');
|
|
$this->assertDatabaseCount('training_capacities', 1);
|
|
$this->assertDatabaseCount('labour_demands', 0);
|
|
}
|
|
}
|