212 lines
14 KiB
PHP
212 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\InvestmentDossier;
|
|
use App\Models\Project;
|
|
use App\Models\Sector;
|
|
use App\Models\User;
|
|
use App\Services\PartnershipExchange;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
final class PartnershipEditingTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $owner;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
Http::fake();
|
|
Mail::fake();
|
|
$this->owner = User::factory()->create();
|
|
$this->actingAs($this->owner);
|
|
}
|
|
|
|
private function input(array $changes = []): array
|
|
{
|
|
return ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC partnership draft', 'kind' => 'seeking', 'partnership_type' => 'joint_venture',
|
|
'offer' => 'SYNTHETIC existing resources', 'seeking' => 'SYNTHETIC collaboration', 'payload' => ['summary' => 'SYNTHETIC summary', 'requirements' => 'SYNTHETIC requirements', 'mandatory' => ['capacity'], 'consent' => true, 'confidentiality' => 'private', 'availability' => 'SYNTHETIC resources available from November'], 'origins' => [], 'locations' => [], ...$changes];
|
|
}
|
|
|
|
private function payload(int $id): array
|
|
{
|
|
return json_decode(DB::table('portal_partnerships')->where('id', $id)->value('payload'), true, 512, JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
private function organization(string $name): string
|
|
{
|
|
$id = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $id, 'name' => 'SYNTHETIC '.$name, 'source_system' => 'unit-synthetic', 'organization_type' => 'private']);
|
|
|
|
return $id;
|
|
}
|
|
|
|
private function links(): array
|
|
{
|
|
$place = (string) Str::uuid();
|
|
// Positive QA fixture for the existing workbook predicate, not an imported official place or geometry.
|
|
DB::table('geo_locations')->insert(['id' => $place, 'code' => $place, 'name' => 'SYNTHETIC coverage reference', 'location_type' => 'country', 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:synthetic-'.substr($place, 0, 8)]);
|
|
$project = Project::factory()->create();
|
|
InvestmentDossier::create(['project_id' => $project->id, 'title' => 'SYNTHETIC source dossier', 'source' => 'Synthetic fixture, no actual disclosure', 'content' => [], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'data_classification' => 'public', 'approval_basis' => 'document_owner', 'approval_evidence' => 'Synthetic QA fixture only']);
|
|
|
|
return ['origins' => [['type' => 'project', 'target_id' => $project->id]], 'locations' => [['geo_location_id' => $place, 'role' => 'coverage', 'source_reference' => 'SYNTHETIC declared coverage']]];
|
|
}
|
|
|
|
public function test_partial_edits_preserve_organization_and_scalar_fields_without_transferring_ownership(): void
|
|
{
|
|
$organization = $this->organization('retained organization');
|
|
$officer = User::factory()->create();
|
|
foreach ([$this->owner->id, $officer->id] as $actor) {
|
|
DB::table('memberships')->insert(['user_id' => $actor, 'organization_id' => $organization, 'role' => 'officer']);
|
|
}
|
|
$sector = Sector::create(['name' => 'SYNTHETIC retained sector', 'slug' => 'synthetic-'.Str::uuid(), 'is_active' => true]);
|
|
$input = $this->input(['organization_id' => $organization, 'sector_id' => $sector->id, 'deadline' => now()->addMonth()->toDateString(), ...$this->links()]);
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
$origins = DB::table('partnership_origins')->where('partnership_id', $record->id)->get()->toArray();
|
|
$locations = DB::table('partnership_locations')->where('partnership_id', $record->id)->get()->toArray();
|
|
$this->actingAs($officer)->putJson('/parcerias/'.$record->id, ['version' => $record->version, 'kind' => 'seeking', 'payload' => ['summary' => 'SYNTHETIC summary only']])->assertOk();
|
|
$saved = DB::table('portal_partnerships')->where('id', $record->id)->first();
|
|
foreach (['title', 'offer', 'seeking', 'organization_id', 'sector_id', 'partnership_type', 'deadline'] as $field) {
|
|
$this->assertSame($input[$field], $saved->{$field});
|
|
}
|
|
$this->assertSame($record->user_id, $saved->user_id);
|
|
$this->assertEquals($origins, DB::table('partnership_origins')->where('partnership_id', $record->id)->get()->toArray());
|
|
$this->assertEquals($locations, DB::table('partnership_locations')->where('partnership_id', $record->id)->get()->toArray());
|
|
$this->get('/parcerias/'.$record->id.'/editar')->assertOk();
|
|
}
|
|
|
|
public function test_explicit_empty_scalar_and_relation_values_still_remove_the_selected_information(): void
|
|
{
|
|
$organization = $this->organization('explicitly removed link');
|
|
DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $organization, 'role' => 'officer']);
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $this->input(['organization_id' => $organization, ...$this->links()]));
|
|
$this->putJson('/parcerias/'.$record->id, ['version' => $record->version, 'kind' => 'seeking', 'title' => null, 'offer' => null, 'seeking' => null, 'organization_id' => null, 'sector_id' => null, 'partnership_type' => null, 'deadline' => null, 'origins' => [], 'locations' => []])->assertOk();
|
|
$saved = DB::table('portal_partnerships')->where('id', $record->id)->first();
|
|
$this->assertSame('', $saved->title);
|
|
$this->assertSame('', $saved->offer);
|
|
$this->assertSame('', $saved->seeking);
|
|
$this->assertNull($saved->organization_id);
|
|
$this->assertNull($saved->sector_id);
|
|
$this->assertNull($saved->deadline);
|
|
$this->assertNull($saved->partnership_type);
|
|
$this->assertSame(0, DB::table('partnership_origins')->where('partnership_id', $record->id)->count());
|
|
$this->assertSame(0, DB::table('partnership_locations')->where('partnership_id', $record->id)->count());
|
|
$this->assertSame('SYNTHETIC resources available from November', $this->payload($record->id)['availability']);
|
|
$this->get('/parcerias/'.$record->id.'/editar')->assertOk();
|
|
}
|
|
|
|
public function test_omitted_availability_is_preserved_when_an_older_editor_saves_other_fields(): void
|
|
{
|
|
$input = $this->input();
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
unset($input['payload']['availability']);
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC changed title'])->assertOk();
|
|
$this->assertSame('SYNTHETIC resources available from November', $this->payload($record->id)['availability'] ?? null);
|
|
}
|
|
|
|
public function test_explicit_clearing_is_preserved_and_previous_event_is_not_rewritten(): void
|
|
{
|
|
$input = $this->input();
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
$before = DB::table('partnership_events')->where('partnership_id', $record->id)->orderBy('aggregate_version')->first();
|
|
$input['payload']['availability'] = null;
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version])->assertOk();
|
|
$this->assertNull($this->payload($record->id)['availability']);
|
|
$this->assertSame((array) $before, (array) DB::table('partnership_events')->where('id', $before->id)->first());
|
|
}
|
|
|
|
public function test_omitted_payload_fields_are_retained_but_explicit_false_empty_text_and_lists_are_applied(): void
|
|
{
|
|
$input = $this->input();
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
$original = $this->payload($record->id);
|
|
$this->putJson('/parcerias/'.$record->id, ['version' => $record->version, 'kind' => 'seeking', 'payload' => ['summary' => 'SYNTHETIC only new summary']])->assertOk();
|
|
$saved = $this->payload($record->id);
|
|
$this->assertSame($original['availability'], $saved['availability']);
|
|
$this->assertSame($original['mandatory'], $saved['mandatory']);
|
|
$this->assertTrue($saved['consent']);
|
|
$version = DB::table('portal_partnerships')->where('id', $record->id)->value('version');
|
|
$this->putJson('/parcerias/'.$record->id, ['version' => $version, 'kind' => 'seeking', 'payload' => ['mandatory' => [], 'consent' => false, 'requirements' => '']])->assertOk();
|
|
$saved = $this->payload($record->id);
|
|
$this->assertSame([], $saved['mandatory']);
|
|
$this->assertFalse($saved['consent']);
|
|
$this->assertNull($saved['requirements']);
|
|
$this->assertSame($original['_creation_hash'], $saved['_creation_hash']);
|
|
}
|
|
|
|
public function test_creation_replay_requires_current_representation_after_the_record_moves_to_another_organization(): void
|
|
{
|
|
$a = $this->organization('original organization');
|
|
$b = $this->organization('current organization');
|
|
$representative = User::factory()->create();
|
|
foreach ([[$this->owner->id, $a], [$representative->id, $a], [$representative->id, $b]] as [$user, $organization]) {
|
|
DB::table('memberships')->insert(['user_id' => $user, 'organization_id' => $organization, 'role' => 'officer']);
|
|
}
|
|
$input = $this->input(['organization_id' => $a]);
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
app(PartnershipExchange::class)->save($representative, [...$input, 'version' => $record->version, 'organization_id' => $b], $record->id);
|
|
$before = (array) DB::table('portal_partnerships')->where('id', $record->id)->first();
|
|
$this->postJson('/parcerias/rascunhos', $input)->assertForbidden();
|
|
$this->assertSame($before, (array) DB::table('portal_partnerships')->where('id', $record->id)->first());
|
|
$this->get('/parcerias/'.$record->id.'/editar')->assertForbidden();
|
|
}
|
|
|
|
public function test_identical_creation_replay_is_read_only_even_when_its_sector_is_no_longer_active(): void
|
|
{
|
|
$sector = Sector::create(['name' => 'SYNTHETIC old sector', 'slug' => 'synthetic-'.Str::uuid(), 'is_active' => true]);
|
|
$input = $this->input(['sector_id' => $sector->id]);
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
$sector->update(['is_active' => false]);
|
|
$before = (array) DB::table('portal_partnerships')->where('id', $record->id)->first();
|
|
$events = DB::table('partnership_events')->count();
|
|
$this->postJson('/parcerias/rascunhos', $input)->assertOk()->assertJsonPath('id', $record->id)->assertJsonPath('version', $record->version);
|
|
$this->assertSame($before, (array) DB::table('portal_partnerships')->where('id', $record->id)->first());
|
|
$this->assertSame($events, DB::table('partnership_events')->count());
|
|
$this->postJson('/parcerias/rascunhos', [...$input, 'client_key' => (string) Str::uuid()])->assertUnprocessable()->assertJsonValidationErrors('sector_id')->assertJsonPath('errors.sector_id.0', 'Seleccione um sector activo ou deixe a classificação por definir.');
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version])->assertUnprocessable()->assertJsonValidationErrors('sector_id');
|
|
}
|
|
|
|
public function test_stale_version_and_invalid_fields_cannot_overwrite_current_payload(): void
|
|
{
|
|
$input = $this->input();
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC current revision'])->assertOk();
|
|
$current = (array) DB::table('portal_partnerships')->where('id', $record->id)->first();
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC stale revision'])->assertConflict();
|
|
$this->assertSame($current, (array) DB::table('portal_partnerships')->where('id', $record->id)->first());
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $current['version'], 'payload' => ['availability' => str_repeat('a', 1001)]])->assertUnprocessable()->assertJsonValidationErrors('payload.availability')->assertJsonPath('errors', fn ($errors) => ($errors['payload.availability'][0] ?? null) === 'Indique a disponibilidade em até 1000 caracteres.');
|
|
$this->assertSame($current, (array) DB::table('portal_partnerships')->where('id', $record->id)->first());
|
|
}
|
|
|
|
public function test_editing_a_published_record_still_returns_to_draft_and_preserves_omitted_availability(): void
|
|
{
|
|
$manager = User::factory()->create();
|
|
config(['portal.managers' => [$manager->email]]);
|
|
$input = $this->input();
|
|
$record = app(PartnershipExchange::class)->save($this->owner, $input);
|
|
$exchange = app(PartnershipExchange::class);
|
|
$record = $exchange->transition($this->owner, $record->id, $record->version, 'submit', 'SYNTHETIC submission');
|
|
$record = $exchange->transition($manager, $record->id, $record->version, 'publish', 'SYNTHETIC independent publication');
|
|
unset($input['payload']['availability']);
|
|
$this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC revision for review'])->assertOk()->assertJsonPath('status', 'draft');
|
|
$this->assertSame('SYNTHETIC resources available from November', $this->payload($record->id)['availability']);
|
|
}
|
|
|
|
public function test_unverified_personal_drafts_and_editor_remain_available_without_organization_or_mfa(): void
|
|
{
|
|
$this->owner->update(['email_verified_at' => null]);
|
|
$record = $this->postJson('/parcerias/rascunhos', $this->input(['title' => '']))->assertOk()->json('id');
|
|
$this->get('/parcerias/'.$record.'/editar')->assertOk();
|
|
Http::assertNothingSent();
|
|
Mail::assertNothingSent();
|
|
}
|
|
}
|