Files
Investidor-V2/tests/Feature/EducationTest.php
T
nala 6ddafc95ba
tests / ci (push) Canceled after 0s
oportunidades 4
2026-10-10 07:03:59 +02:00

276 lines
16 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Course;
use App\Models\EducationInvestmentProposal;
use App\Models\LabourDemand;
use App\Models\Occupation;
use App\Models\Opportunity;
use App\Models\Project;
use App\Models\Sector;
use App\Models\Skill;
use App\Models\TrainingCapacity;
use App\Models\TrainingProvider;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
class EducationTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// No fresh migrations or destructive operations: require a prepared isolated DB.
$this->assertSame('testing', app()->environment());
$this->assertSame('pgsql', DB::connection()->getDriverName());
$this->assertStringEndsWith('_test', DB::connection()->getDatabaseName());
$this->assertSame(DB::connection()->getDatabaseName(), DB::selectOne('select current_database() as name')->name);
DB::beginTransaction();
$this->beforeApplicationDestroyed(fn () => DB::rollBack());
$this->withoutVite();
config(['education.enabled' => true]);
}
private function manager(): User
{
$user = User::factory()->create();
config(['portal.managers' => [...config('portal.managers', []), $user->email]]);
$this->actingAs($user);
return $user;
}
private function declaration(): array
{
$country = (string) Str::uuid();
$province = (string) Str::uuid();
$territory = (string) Str::uuid();
DB::table('geo_locations')->insert(['id' => $country, 'name' => 'País sintético', 'code' => $country, 'location_type' => 'country']);
DB::table('geo_locations')->insert(['id' => $province, 'name' => 'Província sintética', 'code' => $province,
'location_type' => 'province', 'parent_id' => $country]);
DB::table('geo_locations')->insert(['id' => $territory, 'name' => 'Distrito sintético', 'code' => $territory,
'location_type' => 'district', 'parent_id' => $province]);
return [
'project_id' => Project::create(['code' => (string) Str::uuid(), 'name' => 'Projecto sintético', 'status' => 'draft'])->id,
'occupation_id' => Occupation::create(['code' => (string) Str::uuid(), 'name' => 'Ocupação sintética'])->id,
'skill_id' => Skill::create(['code' => (string) Str::uuid(), 'name' => 'Competência sintética'])->id,
'geo_location_id' => $territory, 'employer' => 'Empregador sintético', 'qualification' => 'Nível de teste',
'quantity' => 100, 'period_start' => now()->addMonths(3)->toDateString(),
'period_end' => now()->addMonths(6)->toDateString(), 'reference_date' => now()->toDateString(),
'valid_until' => now()->addYear()->toDateString(), 'source' => 'Fonte exclusivamente sintética para teste.',
];
}
private function course(): Course
{
$provider = TrainingProvider::create(['name' => 'Instituição sintética', 'status' => 'candidate']);
return Course::create(['provider_id' => $provider->id, 'code' => (string) Str::uuid(), 'title' => 'Curso sintético', 'status' => 'draft']);
}
public function test_educational_proposal_requires_review_and_is_a_single_unpublished_canonical_opportunity(): void
{
$manager = $this->manager();
$demandData = $this->declaration();
$this->post('/management/education/demands', $demandData)->assertSessionHasNoErrors();
$demand = LabourDemand::where('project_id', $demandData['project_id'])->firstOrFail();
$sector = Sector::create(['name' => 'Sector sintético', 'slug' => (string) Str::uuid(), 'is_active' => true]);
$data = ['demand_version' => 1, 'sector_id' => $sector->id, 'title' => 'Oficina sintética',
'intervention' => 'Resposta formativa sintética para verificação do fluxo.', 'risks' => 'Custos e viabilidade por confirmar.',
'source' => 'Fonte sintética identificada no teste.', 'currency' => 'MZN'];
$data['district_id'] = $demandData['geo_location_id'];
$data['province_id'] = DB::table('geo_locations')->where('id', $demandData['geo_location_id'])->value('parent_id');
$url = '/management/education/demands/'.$demand->id.'/investment';
$this->post($url, $data)->assertUnprocessable();
$this->manager();
$this->post('/management/education/demands/'.$demand->id.'/review', ['status' => 'reviewed', 'version_no' => 1,
'review_reason' => 'Revisão independente sintética.'])->assertSessionHasNoErrors();
$this->post($url, $data)->assertConflict();
$data['demand_version'] = 2;
$this->post($url, [...$data, 'status' => 'published'])->assertRedirect()->assertSessionHasNoErrors();
$this->post($url, $data)->assertRedirect()->assertSessionHasNoErrors();
$this->assertSame(1, EducationInvestmentProposal::where('labour_demand_id', $demand->id)->count());
$proposal = EducationInvestmentProposal::where('labour_demand_id', $demand->id)->firstOrFail();
$opportunity = Opportunity::findOrFail($proposal->opportunity_id);
$this->assertSame('draft', $opportunity->status);
$this->assertNull($opportunity->published_at);
$this->assertNull($opportunity->estimated_investment_amount);
$this->assertFalse(Opportunity::published()->whereKey($opportunity->id)->exists());
$this->assertSame(1, DB::table('audit_events')->where('entity_id', $proposal->id)->count());
$this->get('/management/education/demands/'.$demand->id.'/analysis')->assertInertia(fn (AssertableInertia $page) => $page
->where('proposal.opportunity_id', $opportunity->id)->where('proposal.demand_version', 2));
$this->put('/management/opportunities/'.$opportunity->id, [
'sector_id' => $sector->id, 'reference' => $opportunity->reference, 'title' => $opportunity->title,
'slug' => $opportunity->slug, 'status' => 'published', 'is_featured' => false, 'estimated_investment_currency' => 'MZN',
])->assertSessionHasErrors('status');
$this->assertSame('draft', $opportunity->fresh()->status);
$this->travel(2)->years();
$this->post($url, $data)->assertUnprocessable();
$this->travelBack();
}
public function test_education_filters_and_record_indicators_do_not_treat_expired_records_as_current(): void
{
$this->manager();
$data = $this->declaration();
$this->post('/management/education/demands', $data)->assertSessionHasNoErrors();
$this->get('/management/education?q=Nível&status=submitted&geo_location_id='.$data['geo_location_id'])
->assertInertia(fn (AssertableInertia $page) => $page->has('demands.data', 1)
->where('indicators.pending_demands', 1)->where('indicators.expired_demands', 0));
$this->get('/management/education?q=inexistente')->assertInertia(fn (AssertableInertia $page) => $page->has('demands.data', 0));
$this->travel(2)->years();
$this->get('/management/education?status=submitted')->assertInertia(fn (AssertableInertia $page) => $page
->has('demands.data', 0)->where('indicators.pending_demands', 0)->where('indicators.expired_demands', 1));
$this->get('/management/education?status=expired')->assertInertia(fn (AssertableInertia $page) => $page->has('demands.data', 1));
$this->travelBack();
}
public function test_provider_and_course_catalogues_follow_documented_capacity_territory(): void
{
$manager = $this->manager();
$first = $this->declaration();
$firstCourse = $this->course();
$capacity = $first;
unset($capacity['project_id'], $capacity['employer']);
TrainingCapacity::create([...$capacity, 'course_id' => $firstCourse->id, 'submitted_by' => $manager->id]);
$otherProvince = (string) Str::uuid();
$otherDistrict = (string) Str::uuid();
$country = DB::table('geo_locations')->where('id', $first['geo_location_id'])->value('parent_id');
$country = DB::table('geo_locations')->where('id', $country)->value('parent_id');
DB::table('geo_locations')->insert(['id' => $otherProvince, 'name' => 'Província de teste B', 'code' => $otherProvince,
'location_type' => 'province', 'parent_id' => $country]);
DB::table('geo_locations')->insert(['id' => $otherDistrict, 'name' => 'Distrito de teste B', 'code' => $otherDistrict,
'location_type' => 'district', 'parent_id' => $otherProvince]);
$otherCourse = $this->course();
TrainingCapacity::create([...$capacity, 'geo_location_id' => $otherDistrict, 'course_id' => $otherCourse->id, 'submitted_by' => $manager->id]);
$this->get('/management/education?geo_location_id='.$first['geo_location_id'])
->assertInertia(fn (AssertableInertia $page) => $page
->where('filters.geo_location_id', $first['geo_location_id'])
->has('providers', 1)->where('providers.0.id', $firstCourse->provider_id)
->has('courses', 1)->where('courses.0.id', $firstCourse->id));
}
public function test_access_requires_verified_manager_and_enabled_feature(): void
{
$this->get('/management/education')->assertRedirect('/login');
$this->actingAs(User::factory()->create())->get('/management/education')->assertForbidden();
$this->manager();
config(['education.enabled' => false]);
$this->get('/management/education')->assertNotFound();
$this->post('/management/education/catalog/skills', ['name' => 'Teste', 'code' => 'TEST'])->assertNotFound();
config(['education.enabled' => true]);
$this->get('/management/education')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
->component('education/index')->where('educationEnabled', true));
$this->actingAs(User::factory()->unverified()->create())->get('/management/education')->assertRedirect('/email/verify');
}
public function test_intake_does_not_accept_official_status_or_caller_supplied_reviewer(): void
{
$user = $this->manager();
$data = $this->declaration();
$this->post('/management/education/demands', [...$data, 'status' => 'certified', 'reviewed_by' => $user->id])
->assertSessionHasNoErrors()->assertRedirect();
$demand = LabourDemand::where('project_id', $data['project_id'])->firstOrFail();
$this->assertSame('submitted', $demand->status);
$this->assertNull($demand->reviewed_by);
$this->assertSame($user->id, $demand->submitted_by);
$audit = DB::table('audit_events')->where('entity_id', $demand->id)->first();
$this->assertNotNull($audit);
$this->assertStringNotContainsString($data['source'], $audit->after_data);
$this->post('/management/education/demands', [...$data, 'quantity' => -1])->assertSessionHasErrors('quantity');
$this->post('/management/education/demands', [...$data, 'period_end' => now()->toDateString()])->assertSessionHasErrors('period_end');
$country = (string) Str::uuid();
DB::table('geo_locations')->insert([
'id' => $country, 'name' => 'País fora do escopo', 'code' => $country, 'location_type' => 'country',
]);
$this->post('/management/education/demands', [...$data, 'geo_location_id' => $country])
->assertSessionHasErrors('geo_location_id');
Skill::findOrFail($data['skill_id'])->delete();
$this->post('/management/education/demands', $data)->assertSessionHasErrors('skill_id');
}
public function test_review_requires_another_person_current_evidence_and_matching_version(): void
{
$this->manager();
$data = $this->declaration();
$this->post('/management/education/demands', $data)->assertSessionHasNoErrors();
$demand = LabourDemand::where('project_id', $data['project_id'])->firstOrFail();
$url = '/management/education/demands/'.$demand->id.'/review';
$decision = ['status' => 'reviewed', 'version_no' => 1, 'review_reason' => 'Revisão independente com evidência sintética.'];
$this->post($url, $decision)->assertForbidden();
$reviewer = $this->manager();
$this->post($url, [...$decision, 'status' => 'accredited'])->assertSessionHasErrors('status');
$this->post($url, $decision)->assertSessionHasNoErrors()->assertRedirect();
$this->assertSame($reviewer->id, $demand->fresh()->reviewed_by);
$this->assertSame(2, $demand->fresh()->version_no);
$this->post($url, $decision)->assertConflict();
$this->travel(2)->years();
$this->post($url, [...$decision, 'version_no' => 2])->assertUnprocessable();
$this->travelBack();
}
public function test_retired_courses_and_inactive_providers_cannot_receive_capacity(): void
{
$this->manager();
$data = $this->declaration();
$course = $this->course();
$data['course_id'] = $course->id;
$this->post('/management/education/capacities', $data)->assertRedirect()->assertSessionHasNoErrors();
$this->assertDatabaseHas('training_capacities', ['course_id' => $course->id, 'status' => 'submitted']);
$course->update(['status' => 'retired']);
$this->post('/management/education/capacities', $data)->assertSessionHasErrors('course_id');
$course->update(['status' => 'draft']);
TrainingProvider::findOrFail($course->provider_id)->update(['status' => 'inactive']);
$this->post('/management/education/capacities', $data)->assertSessionHasErrors('course_id');
}
public function test_catalogue_keeps_institutions_and_courses_nonofficial(): void
{
$this->manager();
$name = 'Instituição sintética '.Str::uuid();
$this->post('/management/education/catalog/providers', ['name' => $name, 'status' => 'approved'])
->assertRedirect()->assertSessionHasNoErrors();
$provider = TrainingProvider::where('name', $name)->firstOrFail();
$this->assertSame('candidate', $provider->status);
$data = ['provider_id' => $provider->id, 'title' => 'Curso sintético', 'code' => (string) Str::uuid(), 'status' => 'published'];
$this->post('/management/education/catalog/courses', $data)->assertRedirect()->assertSessionHasNoErrors();
$this->assertDatabaseHas('courses', ['code' => $data['code'], 'status' => 'draft']);
$this->post('/management/education/catalog/courses', $data)->assertSessionHasErrors('code');
$provider->update(['status' => 'inactive']);
$this->post('/management/education/catalog/courses', [...$data, 'code' => (string) Str::uuid()])
->assertSessionHasErrors('provider_id');
}
public function test_matching_never_subtracts_training_places_from_workers_and_excludes_unavailable_courses(): void
{
$submitter = $this->manager();
$data = $this->declaration();
$this->post('/management/education/demands', $data)->assertSessionHasNoErrors();
$demand = LabourDemand::where('project_id', $data['project_id'])->firstOrFail();
$course = $this->course();
$capacity = $data;
unset($capacity['project_id'], $capacity['employer']);
$reviewer = $this->manager();
TrainingCapacity::create([...$capacity, 'course_id' => $course->id, 'submitted_by' => $submitter->id,
'reviewed_by' => $reviewer->id, 'status' => 'reviewed', 'review_reason' => 'Evidência sintética revista.',
'period_start' => now()->toDateString(), 'period_end' => $data['period_start']]);
$url = '/management/education/demands/'.$demand->id.'/analysis';
$this->get($url)->assertOk()->assertInertia(fn (AssertableInertia $page) => $page->where('eligible', false)->has('candidates', 0));
$this->post('/management/education/demands/'.$demand->id.'/review', ['status' => 'reviewed', 'version_no' => 1,
'review_reason' => 'Revisão independente sintética.'])->assertSessionHasNoErrors();
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->component('education/analysis')
->where('eligible', true)->has('candidates', 1)->where('gap.people', null)->where('gap.state', 'insufficient_data'));
$course->delete();
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->has('candidates', 0)->where('gap.people', null));
$this->travel(2)->years();
$this->get($url)->assertInertia(fn (AssertableInertia $page) => $page->where('eligible', false)->where('gap.state', 'expired'));
$this->travelBack();
}
}