352 lines
22 KiB
PHP
352 lines
22 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use App\Services\OfferAvailability;
|
|
use App\Services\OfferPublicCatalogue;
|
|
use App\Services\OpportunityManagement;
|
|
use App\Services\PortalProfileAdministration;
|
|
use App\Services\PrivateOfferConfidentiality;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Foundation\Vite;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Testing\TestResponse;
|
|
use Tests\TestCase;
|
|
|
|
final class PrivateOfferOperationsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const BASE = '/minhas-oportunidades/ofertas';
|
|
|
|
private const REASON = 'Fundamento exclusivamente sintético para ensaio QA isolado.';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutVite();
|
|
Queue::fake();
|
|
Mail::fake();
|
|
Storage::fake('local');
|
|
config(['portal.profile_access_enabled' => true, 'editorial_workflow.enabled' => true, 'private_offers.validity_days' => null, 'private_offers.validity_policy_reference' => null]);
|
|
}
|
|
|
|
private function actor(?string $role = null, string $category = 'business_sale'): User
|
|
{
|
|
$actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('SYNTHETIC QA'), 'two_factor_confirmed_at' => now()]);
|
|
if ($role) {
|
|
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), ['user_id' => $actor->id,
|
|
'role' => $role, 'organization_id' => null, 'responsibilities' => [], 'opportunity_types' => [$category], 'source' => 'synthetic-test-fixture']);
|
|
}
|
|
|
|
return $actor;
|
|
}
|
|
|
|
private function login(User $actor): static
|
|
{
|
|
$actor = $actor->fresh();
|
|
|
|
return $this->actingAs($actor)->withSession(['login.id' => null, 'portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
|
|
}
|
|
|
|
private function draft(User $owner): Opportunity
|
|
{
|
|
$contact = (string) Str::uuid();
|
|
DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'PRIVATE CONTACT CANARY', 'source_system' => 'portal-user-contact', 'external_id' => 'user:'.$owner->id]);
|
|
$id = $this->login($owner)->postJson(self::BASE.'/rascunhos', ['creation_key' => (string) Str::uuid(), 'opportunity_category' => 'business_sale', 'title' => 'PRIVATE TITLE CANARY',
|
|
'description' => 'PRIVATE DESCRIPTION CANARY', 'private_offer' => ['contact_id' => $contact, 'price_mode' => 'amount', 'amount' => '1200.01', 'currency' => 'MZN'], 'reason' => self::REASON])->assertCreated()->json('record.id');
|
|
|
|
return Opportunity::findOrFail($id);
|
|
}
|
|
|
|
private function etag(Opportunity $record): string
|
|
{
|
|
return app(OpportunityManagement::class)->etag($record->fresh());
|
|
}
|
|
|
|
private function approve(User $owner, Opportunity $record): void
|
|
{
|
|
$this->login($owner)->postJson(self::BASE.'/'.$record->id.'/submeter', ['etag' => $this->etag($record), 'confirm' => true])->assertOk();
|
|
$reviewer = $this->actor('validator');
|
|
foreach (['start_review', 'approve'] as $action) {
|
|
$this->login($reviewer)->postJson(self::BASE.'/'.$record->id.'/revisao', ['etag' => $this->etag($record), 'action' => $action, 'reason' => self::REASON])->assertOk();
|
|
}
|
|
}
|
|
|
|
private function publication(Opportunity $record, ?string $at = null): void
|
|
{
|
|
$record = $record->fresh();
|
|
$context = $record->workflow_context;
|
|
$payload = ['title' => 'Synthetic public summary', 'summary' => 'Safe synthetic summary.', 'category' => 'business_sale', 'province_id' => null, 'district_id' => null,
|
|
'price_mode' => 'amount', 'amount' => '1200.01', 'currency' => 'MZN'];
|
|
$hash = OfferPublicCatalogue::projectionHash($payload);
|
|
$context['offer_publication'] = ['contract' => OfferPublicCatalogue::CONTRACT, 'qa_fixture' => true, 'revision' => 1, 'payload' => $payload,
|
|
'approval' => ['state' => 'approved', 'hash' => $hash, 'actor_id' => 1, 'at' => now()->subYears(2)->toIso8601String()],
|
|
'publication' => ['state' => 'published', 'hash' => $hash, 'actor_id' => 2, 'at' => $at ?? now()->subHour()->toIso8601String()],
|
|
'offer_cycle' => $context['offer_operation']['cycle'] ?? 0, 'editorial_hash' => $context['approval']['hash'] ?? 'synthetic-legacy'];
|
|
$record->workflow_context = $context;
|
|
$record->save();
|
|
$this->app->instance(OfferPublicCatalogue::class, new OfferPublicCatalogue(true));
|
|
}
|
|
|
|
private function owner(User $actor, Opportunity $record, string $action, ?string $etag = null): TestResponse
|
|
{
|
|
return $this->login($actor)->postJson(self::BASE.'/'.$record->id.'/operacoes', ['action' => $action, 'etag' => $etag ?? $this->etag($record), 'confirm' => true]);
|
|
}
|
|
|
|
public function test_withdrawal_is_immediate_with_a_stale_form_and_excludes_every_public_consumer(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->publication($record);
|
|
$this->getJson('/api/v1/offers')->assertJsonPath('results.total', 1)->assertHeader('Cache-Control', 'no-store, private');
|
|
$this->owner($owner, $record, 'withdraw', str_repeat('0', 64))->assertOk()->assertJsonPath('availability.state', 'withdrawn');
|
|
$this->getJson('/api/v1/offers')->assertJsonPath('results.total', 0);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
|
|
$this->get('/ofertas/'.$record->id)->assertNotFound();
|
|
$this->getJson('/api/v1/offers/map')->assertJsonPath('matched_count', 0)->assertJsonPath('features', []);
|
|
$this->assertStringNotContainsString('Synthetic public summary', $this->get('/api/v1/offers/export')->streamedContent());
|
|
$this->owner($owner, $record, 'withdraw')->assertOk();
|
|
$this->assertSame(1, DB::table('audit_events')->where('action', 'offer_withdrawn')->count());
|
|
$this->assertNotNull($record->fresh()->workflow_context['offer_publication']);
|
|
$this->assertNull($record->fresh()->published_at);
|
|
}
|
|
|
|
public function test_reactivation_requires_a_new_exact_validation_and_does_not_resurrect_old_publication(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$this->publication($record);
|
|
$old = $record->fresh()->workflow_context['approval']['hash'];
|
|
$this->owner($owner, $record, 'withdraw')->assertOk();
|
|
$this->owner($owner, $record, 'reactivate')->assertConflict();
|
|
$this->owner($owner, $record, 'request_reactivation')->assertOk()->assertJsonPath('availability.state', 'revalidation');
|
|
$this->owner($owner, $record, 'reactivate')->assertConflict();
|
|
$this->approve($owner, $record);
|
|
$fresh = $record->fresh();
|
|
$this->assertNotSame($old, $fresh->workflow_context['approval']['hash']);
|
|
$this->assertSame(1, $fresh->workflow_context['submission']['payload']['operational_cycle']);
|
|
$this->owner($owner, $record, 'reactivate')->assertOk()->assertJsonPath('availability.state', 'available');
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
|
|
$this->publication($record);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertOk()->assertJsonPath('data.title', 'Synthetic public summary');
|
|
$this->assertSame('draft', $record->fresh()->status);
|
|
$this->assertNull($record->fresh()->published_at);
|
|
}
|
|
|
|
public function test_withdrawal_during_review_can_be_reopened_by_the_owner_without_erasing_history(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->login($owner)->postJson(self::BASE.'/'.$record->id.'/submeter', ['etag' => $this->etag($record), 'confirm' => true])->assertOk();
|
|
$submission = $record->fresh()->workflow_context['submission'];
|
|
$this->owner($owner, $record, 'withdraw')->assertOk();
|
|
$this->owner($owner, $record, 'request_reactivation')->assertOk();
|
|
$history = $record->fresh()->workflow_context['decision_history'];
|
|
$this->assertSame($submission['hash'], end($history)['submission']['hash']);
|
|
$this->login($owner)->getJson(self::BASE.'/'.$record->id)->assertOk()->assertJsonPath('record.canEdit', true);
|
|
}
|
|
|
|
public function test_expiry_is_immediate_at_the_boundary_frozen_and_independent_of_nda_and_scheduler(): void
|
|
{
|
|
config(['private_offers.validity_days' => 7, 'private_offers.validity_policy_reference' => 'SYNTHETIC QA POLICY NOT A FINAL DEADLINE']);
|
|
$this->travelTo(now()->startOfSecond());
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$start = now()->toIso8601String();
|
|
$this->publication($record, $start);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertOk()->assertJsonPath('data.expires_at', app(OfferAvailability::class)->record($record->fresh())['expires_at']);
|
|
config(['private_offers.validity_days' => 365, 'private_offers.nda' => null]);
|
|
$this->travel(7)->days();
|
|
$view = app(OfferAvailability::class)->record($record->fresh());
|
|
$this->assertSame('expired', $view['state']);
|
|
$this->getJson('/api/v1/offers?currency=MZN&min_amount=1')->assertOk()->assertJsonPath('results.total', 0);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
|
|
$before = $record->fresh()->management_version;
|
|
$this->artisan('investmoz:offers-expire')->assertSuccessful();
|
|
$this->assertSame($before, $record->fresh()->management_version);
|
|
$this->artisan('investmoz:offers-expire', ['--confirm' => true])->assertSuccessful();
|
|
$this->artisan('investmoz:offers-expire', ['--confirm' => true])->assertSuccessful();
|
|
$this->assertSame(1, DB::table('audit_events')->where('action', 'offer_expired')->count());
|
|
$this->owner($owner, $record, 'reactivate')->assertConflict();
|
|
$this->travelBack();
|
|
}
|
|
|
|
public function test_no_deadline_is_invented_when_the_policy_is_pending(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$this->publication($record, now()->subYear()->toIso8601String());
|
|
$this->assertSame(['state' => 'available', 'expires_at' => null, 'policy_pending' => true], app(OfferAvailability::class)->record($record->fresh()));
|
|
$this->assertFalse(config('private_offers.expiry_scheduler_enabled'));
|
|
$this->assertNull(config('private_offers.validity_days'));
|
|
}
|
|
|
|
public function test_strangers_reviewers_wrong_scopes_and_auditors_cannot_withdraw_or_reactivate(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
foreach ([$this->actor(), $this->actor('promoter'), $this->actor('validator'), $this->actor('auditor')] as $other) {
|
|
$this->owner($other, $record, 'withdraw')->assertForbidden();
|
|
}
|
|
$this->owner($owner, $record, 'withdraw')->assertOk();
|
|
$this->owner($owner, $record, 'request_reactivation', str_repeat('0', 64))->assertConflict();
|
|
$this->assertSame('withdrawn', app(OfferAvailability::class)->record($record->fresh())['state']);
|
|
}
|
|
|
|
public function test_reports_are_private_idempotent_and_treated_in_the_existing_dashboard_and_tasks(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->publication($record);
|
|
$reporter = $this->actor();
|
|
$key = (string) Str::uuid();
|
|
$input = ['client_key' => $key, 'reason' => 'PRIVATE REPORT CANARY with concrete synthetic facts.', 'confirm' => true];
|
|
$url = self::BASE.'/'.$record->id;
|
|
$this->login($reporter)->postJson($url.'/denuncias', $input)->assertCreated();
|
|
$this->postJson($url.'/denuncias', $input)->assertCreated()->assertJsonPath('replayed', true);
|
|
$this->postJson($url.'/denuncias', [...$input, 'reason' => self::REASON])->assertConflict();
|
|
$this->assertSame(1, DB::table('tasks')->where('task_type', 'offer_report_'.$key)->count());
|
|
$this->assertStringNotContainsString('PRIVATE REPORT CANARY', json_encode($record->fresh()->workflow_context));
|
|
$reviewer = $this->actor('validator');
|
|
$this->login($reviewer)->getJson('/management/editorial')->assertOk()->assertJsonPath('offerOperationsUrl', '/minhas-oportunidades/ofertas?operational=1');
|
|
$this->get(self::BASE.'?operational=1&pending_reports=1')->assertOk();
|
|
$this->getJson($url.'/operacoes')->assertOk()->assertJsonPath('reports.0.reason', $input['reason']);
|
|
$this->getJson($url.'/operacoes')->assertJsonPath('reports.0.source_snapshot.title', 'Synthetic public summary');
|
|
$handle = ['etag' => $this->etag($record), 'version' => 1, 'action' => 'start_review', 'reason' => self::REASON, 'confirm' => true];
|
|
$this->login($owner)->postJson($url.'/denuncias/'.$key, $handle)->assertForbidden();
|
|
$this->login($reviewer)->postJson($url.'/denuncias/'.$key, $handle)->assertOk();
|
|
$this->postJson($url.'/denuncias/'.$key, [...$handle, 'action' => 'resolve_withdrawal'])->assertConflict();
|
|
$this->postJson($url.'/denuncias/'.$key, [...$handle, 'etag' => $this->etag($record), 'version' => 2, 'action' => 'resolve_withdrawal'])->assertOk()->assertJsonPath('availability.state', 'withdrawn');
|
|
$this->assertSame('done', DB::table('tasks')->where('task_type', 'offer_report_'.$key)->value('status'));
|
|
$this->login($owner)->getJson($url.'/operacoes')->assertOk()->assertJsonPath('reports', []);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertNotFound();
|
|
$this->login($reporter)->postJson($url.'/denuncias', $input)->assertCreated()->assertJsonPath('replayed', true);
|
|
}
|
|
|
|
public function test_drafts_cannot_be_reported_by_unrelated_users_and_report_read_requires_current_scope_and_mfa(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$other = $this->actor();
|
|
$this->login($other)->postJson(self::BASE.'/'.$record->id.'/denuncias', ['client_key' => (string) Str::uuid(), 'reason' => self::REASON, 'confirm' => true])->assertNotFound();
|
|
$this->getJson(self::BASE.'/'.$record->id.'/operacoes')->assertForbidden();
|
|
$this->login($this->actor('validator', 'real_estate'))->get(self::BASE.'?operational=1')->assertOk();
|
|
$this->getJson(self::BASE.'/'.$record->id.'/operacoes')->assertForbidden();
|
|
$this->login($this->actor('validator'))->withSession(['portal.mfa_at' => time() - 1000])->get(self::BASE.'?operational=1')->assertForbidden();
|
|
$this->login($this->actor('auditor'))->get(self::BASE.'?operational=1')->assertForbidden();
|
|
}
|
|
|
|
public function test_highlights_are_versioned_administrative_only_and_cannot_set_commercial_flags(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$publisher = $this->actor('publisher');
|
|
$url = self::BASE.'/'.$record->id;
|
|
$plan = ['etag' => $this->etag($record), 'version' => 0, 'label' => 'Synthetic plan', 'status' => 'draft', 'note' => self::REASON, 'confirm' => true, 'is_featured' => true, 'payment_enabled' => true];
|
|
$this->login($owner)->postJson($url.'/destaque-administrativo', $plan)->assertForbidden();
|
|
$this->login($this->actor('validator'))->postJson($url.'/destaque-administrativo', $plan)->assertForbidden();
|
|
$this->login($publisher)->postJson($url.'/destaque-administrativo', $plan)->assertOk()->assertJsonPath('commercial_activation', false);
|
|
$this->postJson($url.'/destaque-administrativo', $plan)->assertConflict();
|
|
$this->postJson($url.'/destaque-administrativo', [...$plan, 'etag' => $this->etag($record), 'version' => 1, 'status' => 'active'])->assertUnprocessable();
|
|
$this->postJson($url.'/destaque-administrativo', [...$plan, 'etag' => $this->etag($record), 'version' => 1, 'status' => 'archived'])->assertOk()->assertJsonPath('highlight.version', 2);
|
|
$this->assertFalse($record->fresh()->is_featured);
|
|
$this->assertNull($record->fresh()->published_at);
|
|
$this->login($owner)->getJson($url.'/operacoes')->assertJsonPath('highlight', null);
|
|
}
|
|
|
|
public function test_a_withdrawn_first_draft_still_requires_its_first_editorial_validation(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->owner($owner, $record, 'withdraw')->assertOk();
|
|
$this->owner($owner, $record, 'request_reactivation')->assertOk()->assertJsonPath('availability.state', 'revalidation');
|
|
$this->owner($owner, $record, 'reactivate')->assertConflict();
|
|
$this->approve($owner, $record);
|
|
$this->owner($owner, $record, 'reactivate')->assertOk();
|
|
$this->assertSame('draft', $record->fresh()->status);
|
|
}
|
|
|
|
public function test_withdrawal_and_revalidation_do_not_restore_previous_nda_grants(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$bytes = "%PDF-1.7\nSYNTHETIC QA NOT A LEGAL NDA\n%%EOF";
|
|
$docId = (string) Str::uuid();
|
|
$sha = hash('sha256', $bytes);
|
|
Storage::disk('local')->put('qa-nda/'.$docId, Crypt::encryptString($bytes));
|
|
DB::table('documents')->insert(['id' => $docId, 'file_name' => 'qa-only.pdf', 'media_type' => 'application/pdf', 'storage_key' => 'qa-nda/'.$docId, 'storage_encryption' => 'laravel',
|
|
'sha256' => $sha, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $sha, 'malware_engine' => 'QA-Test-Double', 'version_no' => 1, 'data_classification' => 'restricted']);
|
|
config(['private_offers.nda' => ['document_id' => $docId, 'version' => 1, 'sha256' => $sha, 'approval_reference' => 'SYNTHETIC QA NOT LEGAL APPROVAL']]);
|
|
$nda = app(PrivateOfferConfidentiality::class)->nda();
|
|
$interested = $this->actor();
|
|
$url = self::BASE.'/'.$record->id;
|
|
$input = ['action' => 'accept', 'confirm' => true, 'version_hash' => $record->fresh()->workflow_context['approval']['hash'], 'nda_sha256' => $sha, 'nda_fingerprint' => $nda['fingerprint']];
|
|
$this->login($interested)->postJson($url.'/confidencialidade', $input)->assertOk();
|
|
$acceptance = $this->getJson($url.'/confidencialidade')->json('acceptance');
|
|
$this->login($owner)->postJson($url.'/confidencialidade', [...$input, 'action' => 'authorize', 'interested_user_id' => $interested->id, 'acceptance_id' => $acceptance['id'], 'reason' => self::REASON])->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertOk();
|
|
$this->owner($owner, $record, 'withdraw')->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertConflict();
|
|
$this->owner($owner, $record, 'request_reactivation')->assertOk();
|
|
$this->approve($owner, $record);
|
|
$this->owner($owner, $record, 'reactivate')->assertOk();
|
|
$this->login($interested)->getJson($url.'/confidencial')->assertForbidden();
|
|
$this->assertSame($acceptance['id'], $record->fresh()->workflow_context['offer_confidentiality']['acceptances'][(string) $interested->id]['id']);
|
|
}
|
|
|
|
public function test_operational_and_report_screens_use_real_compiled_entries_and_private_cache_headers(): void
|
|
{
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$directory = getenv('EDITORIAL_QA_BUILD_DIRECTORY') ?: 'build';
|
|
$manifest = json_decode(file_get_contents(public_path($directory.'/manifest.json')), true, 512, JSON_THROW_ON_ERROR);
|
|
$this->app->instance(Vite::class, (new Vite)->useBuildDirectory($directory)->useHotFile(storage_path('phpunit-offers-no-hot')));
|
|
foreach (['offers/preparation' => self::BASE.'/'.$record->id, 'offers/report' => self::BASE.'/'.$record->id.'/denunciar'] as $page => $url) {
|
|
$key = 'resources/js/pages/'.$page.'.tsx';
|
|
$this->assertArrayHasKey($key, $manifest);
|
|
$response = $this->login($owner)->get($url, ['Accept' => 'text/html'])->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertSee($manifest[$key]['file'], false);
|
|
if ($page === 'offers/report') {
|
|
$response->assertDontSee('PRIVATE CONTACT CANARY')->assertDontSee('PRIVATE DESCRIPTION CANARY');
|
|
}
|
|
}
|
|
$reviewer = $this->actor('validator');
|
|
foreach (['offers/preparation-index' => self::BASE.'?operational=1', 'management/editorial-workflow' => '/management/editorial'] as $page => $url) {
|
|
$key = 'resources/js/pages/'.$page.'.tsx';
|
|
$this->assertArrayHasKey($key, $manifest);
|
|
$this->login($reviewer)->get($url, ['Accept' => 'text/html'])->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertSee($manifest[$key]['file'], false);
|
|
}
|
|
}
|
|
|
|
public function test_an_edited_private_approval_does_not_extend_the_existing_public_deadline(): void
|
|
{
|
|
config(['private_offers.validity_days' => 7, 'private_offers.validity_policy_reference' => 'SYNTHETIC QA FIRST']);
|
|
$owner = $this->actor('promoter');
|
|
$record = $this->draft($owner);
|
|
$this->approve($owner, $record);
|
|
$this->publication($record);
|
|
$deadline = app(OfferAvailability::class)->record($record->fresh())['expires_at'];
|
|
$this->login($owner)->postJson(self::BASE.'/'.$record->id.'/revisao', ['action' => 'request_revision', 'etag' => $this->etag($record), 'reason' => self::REASON])->assertOk();
|
|
$this->postJson(self::BASE.'/'.$record->id.'/guardar', ['title' => 'Changed private title', 'etag' => $this->etag($record), 'reason' => self::REASON])->assertOk();
|
|
config(['private_offers.validity_days' => 30, 'private_offers.validity_policy_reference' => 'SYNTHETIC QA SECOND']);
|
|
$this->approve($owner, $record);
|
|
$this->assertSame($deadline, app(OfferAvailability::class)->record($record->fresh())['expires_at']);
|
|
$context = $record->fresh()->workflow_context;
|
|
unset($context['offer_publication']['editorial_hash']);
|
|
$record->forceFill(['workflow_context' => $context])->save();
|
|
$this->assertSame($deadline, app(OfferAvailability::class)->record($record->fresh())['expires_at']);
|
|
$this->getJson('/api/v1/offers/'.$record->id)->assertOk()->assertJsonPath('data.title', 'Synthetic public summary');
|
|
}
|
|
}
|