withoutVite(); config(['education.enabled' => true]); $identity = DB::selectOne('select current_database() as name,inet_server_port() as port'); $this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name); $this->assertSame(55441, (int) $identity->port); $this->reviewer = $this->manager(); } private function manager(): User { $u = User::factory()->create(); config(['portal.managers' => [...config('portal.managers', []), $u->email]]); $this->actingAs($u); return $u; } private function record(string $kind): LabourDemand|TrainingCapacity { $place = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $place, 'name' => 'SYNTHETIC province', 'code' => $place, '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' => $place, 'qualification' => 'SYNTHETIC qualification', 'quantity' => 20, 'period_start' => today()->addMonth()->toDateString(), 'period_end' => today()->addMonths(2)->toDateString(), 'source' => 'SYNTHETIC documentary source', 'reference_date' => today()->toDateString(), 'valid_until' => today()->addYear()->toDateString(), 'submitted_by' => User::factory()->create()->id, 'status' => 'submitted', 'version_no' => 1]; if ($kind === 'demands') { return LabourDemand::create([...$data, 'project_id' => Project::create(['code' => (string) Str::uuid(), 'name' => 'SYNTHETIC project', 'status' => 'draft'])->id, 'employer' => 'SYNTHETIC employer']); } $provider = TrainingProvider::create(['name' => 'SYNTHETIC provider', 'status' => 'candidate']); $course = Course::create(['provider_id' => $provider->id, 'code' => (string) Str::uuid(), 'title' => 'SYNTHETIC course', 'status' => 'draft']); return TrainingCapacity::create([...$data, 'course_id' => $course->id]); } private function state(LabourDemand|TrainingCapacity $record): array { $base = ['version_no' => 1, 'qualification' => 'SYNTHETIC qualification', 'quantity' => 20, 'status' => 'submitted', 'period_start' => $record->getRawOriginal('period_start'), 'period_end' => $record->getRawOriginal('period_end'), 'valid_until' => $record->getRawOriginal('valid_until'), 'source' => 'SYNTHETIC documentary source', 'reference_date' => today()->toDateString(), 'employer' => '', 'references' => []]; return ['values' => ['version_no' => 1, 'status' => 'reviewed', 'review_reason' => ' SENSITIVE SYNTHETIC PRIVATE REVIEW NOTE '], 'generation' => ['status' => 0, 'review_reason' => 1], 'base' => $base, 'pending' => null]; } private function url(string $kind, LabourDemand|TrainingCapacity $r): string { return '/management/education/'.$kind.'/'.$r->id.'/review-preparation'; } public function test_private_preparation_is_encrypted_actor_scoped_idempotent_and_preserves_the_declaration(): void { foreach (['demands', 'capacities'] as $kind) { $record = $this->record($kind); $before = $record->fresh()->getAttributes(); $url = $this->url($kind, $record); $body = ['actor_id' => $this->reviewer->id, 'version' => 0, 'operation_key' => (string) Str::uuid(), 'snapshot' => $this->state($record)]; $response = $this->postJson($url, $body)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertJsonPath('version', 1); $this->assertStringContainsString('"references":{}', $response->getContent()); $saved = $response->json(); $this->assertSame($body['snapshot'], $saved['snapshot']); $this->postJson($url, $body)->assertOk()->assertJsonPath('version', 1); $query = DB::table('audit_events')->where('source_system', 'portal-education-review')->where('entity_id', $record->id)->where('action', 'review_preparation_saved'); $this->assertSame(1, $query->count()); $this->assertStringNotContainsString('SENSITIVE SYNTHETIC PRIVATE REVIEW NOTE', $query->value('after_data')); $this->getJson($url)->assertOk()->assertJsonPath('snapshot.values.review_reason', $body['snapshot']['values']['review_reason']); $this->assertSame($before, $record->fresh()->getAttributes()); $this->assertSame(0, DB::table('audit_events')->where('entity_id', $record->id)->where('action', 'review_committed')->count()); $this->get('/management/education/'.$kind.'/'.$record->id)->assertOk()->assertInertia(fn ($page) => $page->where('preparation.version', 1)->where('history.total', 0)); $this->manager(); $this->getJson($url)->assertOk()->assertJsonPath('version', 0)->assertJsonPath('snapshot', null); $this->actingAs($this->reviewer); $this->postJson($url, [...$body, 'snapshot' => [...$body['snapshot'], 'values' => [...$body['snapshot']['values'], 'review_reason' => 'DIFFERENT NOTE USING THE SAME KEY']]])->assertConflict(); } } public function test_private_preparation_optimistic_version_and_late_replay_never_replace_newer_notes(): void { $r = $this->record('demands'); $url = $this->url('demands', $r); $first = ['actor_id' => $this->reviewer->id, 'version' => 0, 'operation_key' => (string) Str::uuid(), 'snapshot' => $this->state($r)]; $this->postJson($url, $first)->assertOk(); $second = [...$first, 'version' => 1, 'operation_key' => (string) Str::uuid()]; $second['snapshot']['values']['review_reason'] = 'SYNTHETIC deliberate newer note from a second window.'; $this->postJson($url, $second)->assertOk()->assertJsonPath('version', 2); $this->postJson($url, $first)->assertOk()->assertJsonPath('version', 2)->assertJsonPath('snapshot.values.review_reason', $second['snapshot']['values']['review_reason']); $this->postJson($url, [...$first, 'operation_key' => (string) Str::uuid()])->assertConflict(); $this->assertSame(2, DB::table('audit_events')->where('entity_id', $r->id)->where('action', 'review_preparation_saved')->count()); $other = $this->manager(); $this->postJson($url, $first)->assertConflict(); $this->getJson($url.'?actor_id='.$this->reviewer->id)->assertConflict(); $this->getJson($url)->assertOk()->assertJsonPath('snapshot', null)->assertJsonPath('actor_id', $other->id); } public function test_uncertain_review_is_recovered_from_its_private_intent_and_same_native_command_once(): void { $r = $this->record('demands'); $state = $this->state($r); $key = (string) Str::uuid(); $state['pending'] = ['values' => [...$state['values'], 'client_key' => $key], 'reasonGeneration' => 1, 'statusGeneration' => 0, 'base' => $state['base']]; $state['values']['review_reason'] = 'SYNTHETIC later edit retained after an uncertain response.'; $state['generation']['review_reason'] = 2; $url = $this->url('demands', $r); $this->postJson($url, ['actor_id' => $this->reviewer->id, 'version' => 0, 'operation_key' => (string) Str::uuid(), 'snapshot' => $state])->assertOk(); $native = '/management/education/demands/'.$r->id.'/review'; $this->post($native, $state['pending']['values'])->assertRedirect(); $recovered = $this->getJson($url)->assertOk()->json('snapshot'); $this->assertSame($state, $recovered); $this->post($native, $recovered['pending']['values'])->assertRedirect(); $this->assertSame(2, $r->fresh()->version_no); $this->assertSame(1, DB::table('audit_events')->where('entity_id', $r->id)->where('action', 'review_committed')->count()); $context = app(EducationReviews::class)->context($this->reviewer, $r->fresh()); $this->assertSame($key, $context['commands'][0]['key']); $page = $this->get('/management/education/demands/'.$r->id)->assertOk()->viewData('page')['props']; $directory = base_path('outputs/education-review-preparation-20261111'); if (! is_dir($directory)) { mkdir($directory, 0700, true); } file_put_contents($directory.'/recovered-page.json', json_encode(['synthetic' => true, 'transactional_QA' => true, 'props' => array_intersect_key($page, array_flip(['declaration', 'kind', 'actorId', 'today', 'references', 'preparation', 'history', 'backUrl']))], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE)); } public function test_unreadable_private_preparation_and_unauthorised_access_preserve_previous_bytes(): void { $r = $this->record('demands'); $url = $this->url('demands', $r); $bytes = json_encode(['education_preparation' => ['actor_id' => $this->reviewer->id, 'version' => 1, 'operation_key' => (string) Str::uuid(), 'hash' => str_repeat('a', 64), 'snapshot' => 'SYNTHETIC unreadable original encrypted bytes']], JSON_THROW_ON_ERROR); DB::table('audit_events')->insert(['source_system' => 'portal-education-review', 'actor_user_id' => $this->reviewer->id, 'entity_type' => 'labour_demands', 'entity_id' => $r->id, 'action' => 'review_preparation_saved', 'data_classification' => 'restricted', 'after_data' => $bytes]); $stored = DB::table('audit_events')->where('entity_id', $r->id)->where('action', 'review_preparation_saved')->value('after_data'); $this->getJson($url)->assertOk()->assertJsonPath('unavailable', true)->assertJsonPath('snapshot', null); $this->postJson($url, ['actor_id' => $this->reviewer->id, 'version' => 1, 'operation_key' => (string) Str::uuid(), 'snapshot' => $this->state($r)])->assertConflict(); $this->assertSame($stored, DB::table('audit_events')->where('entity_id', $r->id)->where('action', 'review_preparation_saved')->value('after_data')); $this->actingAs(User::factory()->create()); $this->getJson($url)->assertForbidden(); $this->postJson($url, [])->assertForbidden(); } public function test_invalid_decrypted_snapshots_are_unavailable_without_losing_the_encrypted_original(): void { foreach (['{', json_encode(['unexpected' => 'SYNTHETIC invalid snapshot'], JSON_THROW_ON_ERROR)] as $plain) { $r = $this->record('demands'); $cipher = Crypt::encryptString($plain); DB::table('audit_events')->insert(['source_system' => 'portal-education-review', 'actor_user_id' => $this->reviewer->id, 'entity_type' => 'labour_demands', 'entity_id' => $r->id, 'action' => 'review_preparation_saved', 'data_classification' => 'restricted', 'after_data' => json_encode(['education_preparation' => ['version' => 1, 'operation_key' => (string) Str::uuid(), 'hash' => str_repeat('a', 64), 'snapshot' => $cipher]], JSON_THROW_ON_ERROR)]); $this->getJson($this->url('demands', $r))->assertOk()->assertJsonPath('unavailable', true)->assertJsonPath('snapshot', null); $actual = json_decode(DB::table('audit_events')->where('entity_id', $r->id)->value('after_data'), true); $this->assertSame($cipher, $actual['education_preparation']['snapshot']); } } }