actingAs($customer, 'client') ->withHeaders(['x-access-token' => $this->buildClientAccessToken()]); } public function test_confirm_rejects_proof_path_belonging_to_another_customer(): void { Storage::fake('local'); $owner = Customer::factory()->create(); $attacker = Customer::factory()->create(); $ownerPath = UploadedFile::fake()->image('proof.jpg')->store('deposit-proofs/' . $owner->id, 'local'); $transaction = Transaction::create([ 'uuid' => Str::uuid()->toString(), 'public_code' => 'MAN-TEST-' . Str::random(6), 'trans' => 'MANTEST' . time(), 'amount' => 100000, 'customer_id' => $attacker->id, 'type' => 'manual', 'status' => TransactionStatus::PENDING->value, ]); $response = $this->actingAsCustomer($attacker)->postJson('/v1/transaction/manual-deposit/confirm', [ 'uuid' => $transaction->uuid, 'proof_image' => $ownerPath, ]); $response->assertStatus(422)->assertJsonPath('error', true); $this->assertNull($transaction->fresh()->proof_image); } public function test_confirm_rejects_traversal_and_reused_proof_paths(): void { Storage::fake('local'); $customer = Customer::factory()->create(); $other = Customer::factory()->create(); $otherPath = UploadedFile::fake()->image('proof.jpg')->store('deposit-proofs/' . $other->id, 'local'); $usedPath = UploadedFile::fake()->image('old.jpg')->store('deposit-proofs/' . $customer->id, 'local'); $newTransaction = fn () => Transaction::create([ 'uuid' => Str::uuid()->toString(), 'public_code' => 'MAN-TEST-' . Str::random(6), 'trans' => 'MANTEST' . Str::random(8), 'amount' => 100000, 'customer_id' => $customer->id, 'type' => 'manual', 'status' => TransactionStatus::PENDING->value, ]); $newTransaction()->update(['proof_image' => $usedPath, 'status' => TransactionStatus::COMPLETED->value]); foreach (["deposit-proofs/{$customer->id}/../{$otherPath}", $usedPath] as $path) { $transaction = $newTransaction(); $this->actingAsCustomer($customer)->postJson('/v1/transaction/manual-deposit/confirm', [ 'uuid' => $transaction->uuid, 'proof_image' => $path, ])->assertStatus(422); $this->assertNull($transaction->fresh()->proof_image); } } public function test_confirm_without_proof_is_rejected(): void { $customer = Customer::factory()->create(); $transaction = Transaction::create([ 'uuid' => Str::uuid()->toString(), 'public_code' => 'MAN-TEST-' . Str::random(6), 'trans' => 'MANTEST' . time(), 'amount' => 100000, 'customer_id' => $customer->id, 'type' => 'manual', 'status' => TransactionStatus::PENDING->value, ]); $response = $this->actingAsCustomer($customer)->postJson('/v1/transaction/manual-deposit/confirm', [ 'uuid' => $transaction->uuid, ]); $response->assertStatus(422)->assertJsonPath('error', true); } public function test_upload_then_confirm_binds_proof_to_owner(): void { Storage::fake('local'); Mail::fake(); Event::fake([AdminNotificationCreated::class]); $customer = Customer::factory()->create(); Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']); $admin = User::factory()->create(); $admin->assignRole('Full Administrator'); $transaction = Transaction::create([ 'uuid' => Str::uuid()->toString(), 'public_code' => 'MAN-TEST-' . Str::random(6), 'trans' => 'MANTEST' . time(), 'amount' => 100000, 'customer_id' => $customer->id, 'type' => 'manual', 'status' => TransactionStatus::PENDING->value, ]); $upload = $this->actingAsCustomer($customer)->post('/v1/transaction/manual-deposit/upload-proof', [ 'file' => UploadedFile::fake()->image('proof.jpg'), ]); $upload->assertStatus(200); $path = $upload->json('path'); $this->assertStringStartsWith('deposit-proofs/' . $customer->id . '/', $path); Storage::disk('local')->assertExists($path); $confirm = $this->actingAsCustomer($customer)->postJson('/v1/transaction/manual-deposit/confirm', [ 'uuid' => $transaction->uuid, 'proof_image' => $path, ]); $confirm->assertStatus(200)->assertJsonPath('error', false); $this->assertSame($path, $transaction->fresh()->proof_image); $this->assertDatabaseHas('notifications', [ 'recipient_type' => 'admin', 'recipient_id' => $admin->id, 'type' => 'manual_deposit_pending', 'link' => '/transactions', ]); Event::assertDispatched( AdminNotificationCreated::class, fn (AdminNotificationCreated $event) => $event->type === 'manual_deposit_pending', ); } }