CaiWuXiTong/tests/Feature/Business/CourierAgreementTaskDeletionTest.php

210 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use App\Models\Business\CourierAgreementSignature;
use App\Models\Business\CourierAgreementSignatureEvent;
use App\Support\CourierAgreementSignatureService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Tests\Feature\FeatureTestCase;
class CourierAgreementTaskDeletionTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->withExceptionHandling();
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
Storage::fake('uploads');
Http::preventStrayRequests();
config([
'services.esign.environment' => 'sandbox',
'services.esign.base_url' => 'https://smlopenapi.esign.test',
'services.esign.app_id' => 'sandbox-app-id',
'services.esign.app_secret' => 'sandbox-app-secret',
]);
}
public function test_failed_and_unpushed_tasks_are_soft_deleted_and_files_and_audit_are_retained(): void
{
foreach (['prepared', 'failed', 'unknown'] as $status) {
$task = $this->task(['status' => $status, 'last_error' => 'E 签宝接口返回业务错误(1437328):签署人信息不一致']);
Storage::disk('uploads')->put($task->source_pdf_path, '%PDF-source');
$audit = CourierAgreementSignatureEvent::create([
'company_id' => $this->company->id, 'agreement_signature_id' => $task->id,
'event_key' => 'audit-' . $task->id, 'action' => 'TEST_EVENT',
'esign_sign_flow_id' => 'test-' . $task->id, 'event_timestamp' => now()->timestamp,
'raw_body' => '{}', 'received_at' => now(),
]);
$this->loginAs()->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])
->assertOk()->assertJson(['message' => '签署任务已删除。']);
$this->assertSoftDeleted($task);
$this->assertSame($this->user->id, $task->fresh()->updated_by);
Storage::disk('uploads')->assertExists($task->source_pdf_path);
$this->assertNotNull($audit->fresh());
$this->assertNull(CourierAgreementSignature::find($task->id));
$this->get($this->pageUrl())->assertOk()->assertDontSeeText($task->business_no);
$this->get($this->pageUrl() . '?agreement_scope=all')->assertOk()->assertDontSeeText($task->business_no);
$this->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertNotFound();
}
Http::assertNothingSent();
}
public function test_in_flight_completed_and_ambiguous_tasks_cannot_be_deleted(): void
{
$cases = [
['status' => 'sending'],
['status' => 'signing', 'esign_sign_flow_id' => 'active-flow', 'remote_status' => '1'],
['status' => 'completed'],
['status' => 'archived'],
['status' => 'unknown', 'last_error' => 'Connection timed out'],
['status' => 'failed', 'esign_sign_flow_id' => 'failed-after-flow'],
['completed_at' => now()], ['archived_at' => now()],
['signed_pdf_path' => 'signed.pdf'], ['certificate_path' => 'certificate.pdf'],
['remote_status' => '2'], ['remote_status' => '1'],
];
foreach ($cases as $case) {
$task = $this->task($case);
$this->loginAs()->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertStatus(422);
$this->assertFalse($task->fresh()->trashed());
}
Http::assertNothingSent();
}
public function test_ended_flows_are_checked_remotely_before_deletion(): void
{
Http::fakeSequence()->push(['code' => 0, 'data' => ['signFlowStatus' => '3']])
->push(['code' => 0, 'data' => ['signFlowStatus' => '5']])
->push(['code' => 0, 'data' => ['signFlowStatus' => '7']]);
$checked = 0;
foreach (['revoked' => '3', 'expired' => '5', 'rejected' => '7'] as $status => $remoteStatus) {
$task = $this->task(['status' => $status, 'remote_status' => $remoteStatus, 'esign_sign_flow_id' => 'flow-' . $status]);
$this->loginAs()->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertOk();
$this->assertSoftDeleted($task);
Http::assertSentCount(++$checked);
Http::assertSent(fn ($request) => $request->method() === 'GET' && str_contains($request->url(), $task->esign_sign_flow_id));
}
}
public function test_remote_errors_and_stale_terminal_states_preserve_the_task(): void
{
$sequence = Http::fakeSequence();
foreach (['1', '2', '999', ''] as $remoteStatus) {
$sequence->push(['code' => 0, 'data' => ['signFlowStatus' => $remoteStatus]]);
}
$sequence->push(['code' => 500, 'message' => 'provider unavailable'], 500);
foreach (['1', '2', '999', ''] as $remoteStatus) {
$task = $this->task(['status' => 'revoked', 'remote_status' => '3', 'esign_sign_flow_id' => 'flow-' . uniqid()]);
$this->loginAs()->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertStatus(422);
$this->assertFalse($task->fresh()->trashed());
}
$task = $this->task(['status' => 'revoked', 'remote_status' => '3', 'esign_sign_flow_id' => 'flow-error']);
$this->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])
->assertStatus(422)->assertJsonValidationErrors('signature');
$this->assertFalse($task->fresh()->trashed());
}
public function test_environment_mismatch_and_missing_confirmation_block_deletion(): void
{
$task = $this->task();
$this->loginAs()->deleteJson($this->deleteUrl($task))->assertStatus(422)->assertJsonValidationErrors('delete_confirmed');
$this->assertFalse($task->fresh()->trashed());
$remoteTask = $this->task(['status' => 'revoked', 'remote_status' => '3', 'esign_sign_flow_id' => 'flow-production', 'environment' => 'production']);
$this->deleteJson($this->deleteUrl($remoteTask), ['delete_confirmed' => true])->assertStatus(422);
$this->assertFalse($remoteTask->fresh()->trashed());
Http::assertNothingSent();
}
public function test_deletion_does_not_cross_company_or_management_permissions(): void
{
$foreignTask = $this->task(['company_id' => $this->company->id + 100]);
$this->loginAs()->deleteJson($this->deleteUrl($foreignTask), ['delete_confirmed' => true])->assertNotFound();
$this->assertFalse($foreignTask->fresh()->trashed());
$task = $this->task();
$this->user->roles()->detach();
$this->user->unsetRelation('roles');
$this->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertForbidden();
$this->assertFalse($task->fresh()->trashed());
}
public function test_deleted_request_cannot_be_replayed_or_recreated_with_the_same_idempotency_key(): void
{
$key = 'deleted-request';
$task = $this->task(['idempotency_key' => 'agreement-' . hash('sha256', $this->company->id . '|' . $key)]);
$this->loginAs()->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertOk();
try {
app(CourierAgreementSignatureService::class)->send(['document_type' => 'account-proof', 'idempotency_key' => $key], $this->user->id);
$this->fail('Deleted signing request must not be replayed.');
} catch (ValidationException $exception) {
$this->assertArrayHasKey('signature', $exception->errors());
}
$this->assertSame(1, CourierAgreementSignature::withTrashed()->count());
$this->assertSoftDeleted($task);
Http::assertNothingSent();
}
public function test_retrying_a_failed_push_blocks_concurrent_deletion(): void
{
$key = 'retry-request';
$task = $this->task([
'idempotency_key' => 'agreement-' . hash('sha256', $this->company->id . '|' . $key),
'operator_name' => '测试人员', 'operator_account' => '13800138000',
]);
Storage::disk('uploads')->put($task->source_pdf_path, '%PDF-source');
Http::fake(function () use ($task) {
$this->assertSame('sending', $task->fresh()->status);
$this->loginAs()->deleteJson($this->deleteUrl($task), ['delete_confirmed' => true])->assertStatus(422);
return Http::response(['code' => 500, 'message' => 'provider unavailable'], 500);
});
try {
app(CourierAgreementSignatureService::class)->send(['document_type' => 'account-proof', 'idempotency_key' => $key], $this->user->id);
$this->fail('Fake provider should reject the retry.');
} catch (ValidationException $exception) {
$this->assertArrayHasKey('signature', $exception->errors());
}
Http::assertSentCount(1);
$this->assertFalse($task->fresh()->trashed());
$this->assertSame('failed', $task->fresh()->status);
}
public function test_delete_button_is_only_shown_for_safe_tasks_in_the_active_view(): void
{
$safe = $this->task();
$signing = $this->task(['status' => 'signing', 'esign_sign_flow_id' => 'flow-active', 'remote_status' => '1']);
$this->loginAs()->get($this->pageUrl())->assertOk()
->assertSee($this->deleteUrl($safe), false)->assertDontSee($this->deleteUrl($signing) . '"', false);
$this->get($this->pageUrl() . '?agreement_scope=all')->assertOk()->assertDontSee('data-delete-task-form data-task-number=', false);
$this->get($this->pageUrl() . '?agreement_scope=archived')->assertOk()->assertDontSee('data-delete-task-form data-task-number=', false);
}
private function task(array $overrides = []): CourierAgreementSignature
{
$key = uniqid();
return CourierAgreementSignature::create(array_merge([
'company_id' => $this->company->id, 'business_no' => 'AGR-DELETE-' . $key,
'idempotency_key' => 'delete-' . $key, 'document_type' => 'account-proof',
'template_code' => 'TPL-ACCOUNT-CERT-V2', 'template_version' => '2',
'source_file_name' => '收款账户证明文件.doc', 'status' => 'failed',
'courier_name' => '测试快递', 'company_full_name' => $this->company->name,
'organization_name' => '测试快递有限公司', 'payload' => [], 'environment' => 'sandbox',
'source_pdf_path' => 'delete-tests/' . $key . '/source.pdf', 'source_pdf_sha256' => str_repeat('a', 64),
'created_by' => $this->user->id, 'updated_by' => $this->user->id,
], $overrides));
}
private function deleteUrl(CourierAgreementSignature $task): string
{
return route('business.courier-agreement-signatures.destroy', $task);
}
private function pageUrl(): string
{
return route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing']);
}
}