Files
Investidor-V2/tests/Feature/PartnershipPublicationRecoveryTest.php
T

190 lines
11 KiB
PHP

<?php
namespace Tests\Feature;
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\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
final class PartnershipPublicationRecoveryTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private User $moderator;
private object $record;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
Http::fake();
Mail::fake();
Queue::fake();
$this->owner = User::factory()->create();
$this->moderator = User::factory()->create();
config(['portal.managers' => [$this->moderator->email]]);
$this->record = app(PartnershipExchange::class)->save($this->owner, ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC publication proposal', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC offer', 'payload' => ['consent' => true, 'confidentiality' => 'private']]);
$this->actingAs($this->owner);
}
private function command(string $action, ?int $version = null): array
{
return ['client_key' => (string) Str::uuid(), 'version' => $version ?? $this->record->version, 'action' => $action, 'reason' => 'SYNTHETIC documented action'];
}
private function url(): string
{
return '/parcerias/'.$this->record->id.'/estado';
}
public function test_repeating_a_committed_transition_is_a_read_after_a_later_transition(): void
{
$data = $this->command('archive');
$this->postJson($this->url(), $data)->assertOk();
$version = (int) DB::table('portal_partnerships')->where('id', $this->record->id)->value('version');
$this->postJson($this->url(), $this->command('renew', $version))->assertOk();
$events = DB::table('partnership_events')->count();
$this->postJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('status', 'draft');
$this->assertSame($events, DB::table('partnership_events')->count());
}
public function test_current_authority_precedes_a_stale_version_conflict(): void
{
$this->actingAs(User::factory()->create())->postJson($this->url(), $this->command('archive', $this->record->version + 99))->assertForbidden();
}
public function test_publication_actions_match_existing_server_authority_including_suspension(): void
{
$service = app(PartnershipExchange::class);
$submitted = $service->transition($this->owner, $this->record->id, $this->record->version, 'submit', 'SYNTHETIC submit');
$published = $service->transition($this->moderator, $this->record->id, $submitted->version, 'publish', 'SYNTHETIC publish');
$this->actingAs($this->moderator)->getJson('/parcerias/'.$published->id)->assertOk()->assertJsonPath('publicationActions', ['suspend']);
}
public function test_same_nonce_with_changed_action_or_reason_is_refused_without_new_writes(): void
{
$data = $this->command('archive');
$this->postJson($this->url(), $data)->assertOk()->assertJsonPath('replayed', false);
$events = DB::table('partnership_events')->count();
$notices = DB::table('partnership_notifications')->count();
$this->postJson($this->url(), [...$data, 'reason' => 'SYNTHETIC different reason'])->assertConflict();
$this->postJson($this->url(), [...$data, 'action' => 'renew'])->assertConflict();
$this->postJson($this->url(), [...$data, 'client_key' => strtoupper($data['client_key']), 'reason' => ' '.$data['reason'].' '])->assertOk()->assertJsonPath('replayed', true);
$this->assertSame($events, DB::table('partnership_events')->count());
$this->assertSame($notices, DB::table('partnership_notifications')->count());
}
public function test_audit_keeps_the_proposal_used_before_and_after_transition_without_child_cascades(): void
{
$data = $this->command('submit');
$this->postJson($this->url(), $data)->assertOk();
$event = DB::table('partnership_events')->where('event_type', 'partnership_opportunity.submitted.v1')->first();
$metadata = json_decode($event->payload, true)['payload'];
$this->assertSame('SYNTHETIC publication proposal', $metadata['previous']['title']);
$this->assertSame('SYNTHETIC offer', $metadata['applied']['offer']);
$this->assertSame('draft', $metadata['previous']['status']);
$this->assertSame('submitted', $metadata['applied']['status']);
$this->assertSame($this->record->version, $metadata['previous']['version']);
$this->assertSame($this->record->version + 1, $metadata['result_version']);
$this->assertSame('private', $metadata['previous']['payload']['confidentiality']);
$this->assertArrayNotHasKey('_creation_hash', $metadata['previous']['payload']);
$this->assertSame(0, DB::table('partnership_interests')->count());
$this->assertSame(0, DB::table('partnership_rooms')->count());
}
public function test_revoked_organization_author_cannot_replay_even_a_committed_transition(): void
{
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC publisher organization', 'organization_type' => 'private']);
DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $org, 'role' => 'officer']);
DB::table('portal_partnerships')->where('id', $this->record->id)->update(['organization_id' => $org]);
$data = $this->command('archive');
$this->postJson($this->url(), $data)->assertOk();
DB::table('memberships')->where('user_id', $this->owner->id)->where('organization_id', $org)->update(['deleted_at' => now()]);
$this->postJson($this->url(), $data)->assertForbidden();
}
public function test_owner_cannot_publish_their_own_proposal_even_with_the_moderation_gate(): void
{
$service = app(PartnershipExchange::class);
$record = $service->transition($this->owner, $this->record->id, $this->record->version, 'submit', 'SYNTHETIC submit');
config(['portal.managers' => [$this->owner->email]]);
$this->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('publicationActions', []);
$this->postJson($this->url(), $this->command('publish', $record->version))->assertForbidden();
}
public function test_command_keys_are_private_to_the_current_authorized_actor_and_raw_events_stay_private(): void
{
$data = $this->command('submit');
$this->postJson($this->url(), $data)->assertOk();
$response = $this->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonPath('publicationCommands.0.key', strtolower($data['client_key']));
$response->assertJsonMissingPath('history');
$summary = $response->json('historySummary.entries.0');
$this->assertIsArray($summary);
foreach (['payload', 'event_type', 'actor_id', 'command_hash'] as $private) {
$this->assertArrayNotHasKey($private, $summary);
}
$this->assertStringNotContainsString('command_hash', $response->getContent());
$this->assertStringNotContainsString('_notification_context', $response->getContent());
$this->actingAs($this->moderator)->getJson('/parcerias/'.$this->record->id)->assertOk()->assertJsonPath('publicationCommands', []);
$record = app(PartnershipExchange::class)->find($this->record->id);
app(PartnershipExchange::class)->transition($this->moderator, $record->id, $record->version, 'publish', 'SYNTHETIC publish');
$this->actingAs(User::factory()->create())->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('publicationCommands', [])->assertJsonPath('publicationActions', [])->assertJsonPath('historySummary', null)->assertJsonMissingPath('history');
}
public function test_validation_and_legacy_requests_preserve_existing_submission_requirements(): void
{
$events = DB::table('partnership_events')->count();
$this->postJson($this->url(), [...$this->command('submit'), 'reason' => ' '])->assertUnprocessable();
$this->postJson($this->url(), [...$this->command('submit'), 'client_key' => 'invalid'])->assertUnprocessable();
DB::table('portal_partnerships')->where('id', $this->record->id)->update(['payload' => json_encode(['consent' => false])]);
$this->postJson($this->url(), $this->command('submit'))->assertUnprocessable();
$this->assertSame($events, DB::table('partnership_events')->count());
DB::table('portal_partnerships')->where('id', $this->record->id)->update(['payload' => json_encode(['consent' => true])]);
$legacy = $this->command('submit');
unset($legacy['client_key']);
$this->postJson($this->url(), $legacy)->assertOk()->assertJsonPath('replayed', false);
}
public function test_losing_moderation_authority_prevents_replay_and_removes_its_command_metadata(): void
{
$service = app(PartnershipExchange::class);
$record = $service->transition($this->owner, $this->record->id, $this->record->version, 'submit', 'SYNTHETIC submit');
$data = $this->command('publish', $record->version);
$this->actingAs($this->moderator)->postJson($this->url(), $data)->assertOk();
config(['portal.managers' => []]);
$this->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('publicationCommands', []);
$this->postJson($this->url(), $data)->assertForbidden();
}
public function test_stale_version_does_not_write_and_suspension_uses_the_existing_state_graph(): void
{
$this->postJson($this->url(), $this->command('archive', $this->record->version + 1))->assertConflict();
$service = app(PartnershipExchange::class);
$record = $service->transition($this->owner, $this->record->id, $this->record->version, 'submit', 'SYNTHETIC submit');
$record = $service->transition($this->moderator, $record->id, $record->version, 'publish', 'SYNTHETIC publish');
$this->actingAs($this->moderator)->postJson($this->url(), $this->command('suspend', $record->version))->assertOk()->assertJsonPath('status', 'suspended');
$this->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('publicationActions', []);
$this->actingAs($this->owner)->getJson('/parcerias/'.$record->id)->assertOk()->assertJsonPath('publicationActions', ['archive', 'renew']);
}
public function test_nonce_is_scoped_to_process_and_actor_and_another_owner_cannot_recover_it(): void
{
$data = $this->command('archive');
$this->postJson($this->url(), $data)->assertOk();
$other = User::factory()->create();
$record = app(PartnershipExchange::class)->save($other, ['client_key' => (string) Str::uuid(), 'kind' => 'seeking']);
$this->actingAs($other)->postJson('/parcerias/'.$record->id.'/estado', [...$data, 'version' => $record->version])->assertOk()->assertJsonPath('replayed', false);
$this->postJson($this->url(), $data)->assertForbidden();
}
}