Introduce a comprehensive address management system allowing users to
save, manage, and reuse sender and receiver addresses during the
ordering process. This feature bridges the WordPress frontend with the
Laravel backend API.
Key changes:
- Backend (Laravel):
- Added `AddressBookEntry` model and migration for storing addresses
with role (sender/receiver), usage tracking, and default status.
- Implemented `AddressBookController` with full CRUD functionality
secured via Sanctum and ownership validation.
- Integrated address usage tracking and upsert logic into the
`CustomerOrderController` to prevent duplicate entries.
- Added 18 new feature tests covering ownership, limits, and
validation.
- Frontend (WordPress):
- Added a new "Address Book" tab in the user dashboard with card-based
UI, filtering, and SweetAlert2 integration.
- Enhanced the order form with dropdown selectors for saved addresses
and "save this address" checkboxes for both sender and receiver.
- Implemented secure AJAX proxies in `user-bridge.php` with nonce
and sanitization.
- Documentation:
- Updated `AGENT.md` and `CLIENT_DELIVERY.md` to reflect phase 3.9
completion and updated the project roadmap.
- Removed obsolete `README-INSTALL.md`.
289 lines
10 KiB
PHP
289 lines
10 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Api;
|
||
|
||
use App\Models\AddressBookEntry;
|
||
use App\Models\User;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Laravel\Sanctum\Sanctum;
|
||
use PHPUnit\Framework\Attributes\Test;
|
||
use Tests\TestCase;
|
||
|
||
class AddressBookControllerTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
private User $user;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
$this->user = User::factory()->create();
|
||
Sanctum::actingAs($this->user);
|
||
}
|
||
|
||
private function validPayload(array $overrides = []): array
|
||
{
|
||
return array_merge([
|
||
'role' => 'receiver',
|
||
'label' => 'خانه',
|
||
'name' => 'علی رضایی',
|
||
'phone' => '09121234567',
|
||
'state' => 'تهران',
|
||
'city' => 'تهران',
|
||
'postal_code' => '1234567890',
|
||
'address' => 'خیابان ولیعصر، پلاک ۱۰، واحد ۲',
|
||
], $overrides);
|
||
}
|
||
|
||
// ═══════════════════ index ═══════════════════
|
||
|
||
#[Test]
|
||
public function it_lists_only_current_user_addresses()
|
||
{
|
||
AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
AddressBookEntry::create([...$this->validPayload(['role' => 'sender', 'name' => 'رضا']), 'user_id' => $this->user->id]);
|
||
|
||
$other = User::factory()->create();
|
||
AddressBookEntry::create([...$this->validPayload(['name' => 'کاربر دیگر']), 'user_id' => $other->id]);
|
||
|
||
$response = $this->getJson('/api/v1/addresses');
|
||
|
||
$response->assertStatus(200)
|
||
->assertJson(['success' => true])
|
||
->assertJsonCount(2, 'data');
|
||
}
|
||
|
||
#[Test]
|
||
public function it_filters_addresses_by_role()
|
||
{
|
||
AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
AddressBookEntry::create([...$this->validPayload(['role' => 'sender', 'name' => 'رضا']), 'user_id' => $this->user->id]);
|
||
|
||
$response = $this->getJson('/api/v1/addresses?role=sender');
|
||
|
||
$response->assertStatus(200)
|
||
->assertJsonCount(1, 'data')
|
||
->assertJsonPath('data.0.role', 'sender');
|
||
}
|
||
|
||
#[Test]
|
||
public function it_requires_authentication()
|
||
{
|
||
// حذف کاربر actAsشده از گارد sanctum برای این تست
|
||
auth('sanctum')->forgetUser();
|
||
|
||
$response = $this->getJson('/api/v1/addresses');
|
||
|
||
$response->assertStatus(401);
|
||
}
|
||
|
||
// ═══════════════════ store ═══════════════════
|
||
|
||
#[Test]
|
||
public function it_creates_address_successfully()
|
||
{
|
||
$response = $this->postJson('/api/v1/addresses', $this->validPayload());
|
||
|
||
$response->assertStatus(201)
|
||
->assertJson(['success' => true])
|
||
->assertJsonPath('data.name', 'علی رضایی')
|
||
->assertJsonPath('data.role', 'receiver');
|
||
|
||
$this->assertDatabaseHas('address_book_entries', [
|
||
'user_id' => $this->user->id,
|
||
'phone' => '09121234567',
|
||
'role' => 'receiver',
|
||
]);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_rejects_invalid_role()
|
||
{
|
||
$response = $this->postJson('/api/v1/addresses', $this->validPayload(['role' => 'hacker']));
|
||
|
||
$response->assertStatus(422)
|
||
->assertJson(['success' => false]);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_rejects_missing_required_fields()
|
||
{
|
||
$response = $this->postJson('/api/v1/addresses', [
|
||
'role' => 'receiver',
|
||
// name, phone, address وجود ندارند
|
||
]);
|
||
|
||
$response->assertStatus(422);
|
||
$this->assertArrayHasKey('name', $response->json('errors'));
|
||
$this->assertArrayHasKey('phone', $response->json('errors'));
|
||
$this->assertArrayHasKey('address', $response->json('errors'));
|
||
}
|
||
|
||
#[Test]
|
||
public function it_upserts_duplicate_instead_of_creating()
|
||
{
|
||
AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
|
||
// همان نقش + همان تلفن + همان آدرس، با شهر متفاوت
|
||
$response = $this->postJson('/api/v1/addresses', $this->validPayload(['city' => 'کرج']));
|
||
|
||
$response->assertStatus(200)
|
||
->assertJson([
|
||
'success' => true,
|
||
'updated_existing' => true,
|
||
]);
|
||
|
||
$this->assertDatabaseCount('address_book_entries', 1);
|
||
$this->assertDatabaseHas('address_book_entries', ['city' => 'کرج']);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_enforces_max_entries_limit()
|
||
{
|
||
for ($i = 0; $i < AddressBookEntry::MAX_ENTRIES_PER_USER; $i++) {
|
||
AddressBookEntry::create([
|
||
...$this->validPayload(['phone' => '091200000' . str_pad((string) $i, 2, '0', STR_PAD_LEFT)]),
|
||
'user_id' => $this->user->id,
|
||
]);
|
||
}
|
||
|
||
$response = $this->postJson('/api/v1/addresses', $this->validPayload(['phone' => '09129999999']));
|
||
|
||
$response->assertStatus(422)
|
||
->assertJson(['success' => false]);
|
||
$this->assertDatabaseCount('address_book_entries', AddressBookEntry::MAX_ENTRIES_PER_USER);
|
||
}
|
||
|
||
// ═══════════════════ update / delete / default ═══════════════════
|
||
|
||
#[Test]
|
||
public function it_updates_own_address()
|
||
{
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
|
||
$response = $this->putJson("/api/v1/addresses/{$entry->id}", $this->validPayload(['city' => 'اصفهان']));
|
||
|
||
$response->assertStatus(200)
|
||
->assertJsonPath('data.city', 'اصفهان');
|
||
}
|
||
|
||
#[Test]
|
||
public function it_cannot_change_entry_role_on_update()
|
||
{
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
|
||
$response = $this->putJson("/api/v1/addresses/{$entry->id}", $this->validPayload(['role' => 'sender']));
|
||
|
||
$response->assertStatus(200);
|
||
$this->assertDatabaseHas('address_book_entries', [
|
||
'id' => $entry->id,
|
||
'role' => 'receiver', // نقش تغییر نکرده
|
||
]);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_prevents_updating_others_address()
|
||
{
|
||
$other = User::factory()->create();
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $other->id]);
|
||
|
||
$response = $this->putJson("/api/v1/addresses/{$entry->id}", $this->validPayload());
|
||
|
||
$response->assertStatus(403);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_deletes_own_address()
|
||
{
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
|
||
$response = $this->deleteJson("/api/v1/addresses/{$entry->id}");
|
||
|
||
$response->assertStatus(200)
|
||
->assertJson(['success' => true]);
|
||
|
||
$this->assertDatabaseMissing('address_book_entries', ['id' => $entry->id]);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_prevents_deleting_others_address()
|
||
{
|
||
$other = User::factory()->create();
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $other->id]);
|
||
|
||
$response = $this->deleteJson("/api/v1/addresses/{$entry->id}");
|
||
|
||
$response->assertStatus(403);
|
||
$this->assertDatabaseHas('address_book_entries', ['id' => $entry->id]);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_sets_default_and_unsets_previous_default_of_same_role()
|
||
{
|
||
$first = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id, 'is_default' => true]);
|
||
$second = AddressBookEntry::create([...$this->validPayload(['phone' => '09129876543']), 'user_id' => $this->user->id]);
|
||
|
||
$response = $this->postJson("/api/v1/addresses/{$second->id}/default");
|
||
|
||
$response->assertStatus(200)
|
||
->assertJson(['success' => true]);
|
||
|
||
$this->assertTrue($second->refresh()->is_default);
|
||
$this->assertFalse($first->refresh()->is_default);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_prevents_setting_default_on_others_address()
|
||
{
|
||
$other = User::factory()->create();
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $other->id]);
|
||
|
||
$response = $this->postJson("/api/v1/addresses/{$entry->id}/default");
|
||
|
||
$response->assertStatus(403);
|
||
}
|
||
|
||
// ═══════════════════ Model behaviour ═══════════════════
|
||
|
||
#[Test]
|
||
public function it_increments_use_count_and_sets_last_used_at()
|
||
{
|
||
$entry = AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
|
||
$this->assertSame(0, $entry->use_count);
|
||
$this->assertNull($entry->last_used_at);
|
||
|
||
$entry->markUsed();
|
||
|
||
$this->assertSame(1, $entry->refresh()->use_count);
|
||
$this->assertNotNull($entry->refresh()->last_used_at);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_finds_duplicate_by_role_phone_address()
|
||
{
|
||
AddressBookEntry::create([...$this->validPayload(), 'user_id' => $this->user->id]);
|
||
|
||
$dup = AddressBookEntry::findDuplicate($this->user->id, 'receiver', '09121234567', 'خیابان ولیعصر، پلاک ۱۰، واحد ۲');
|
||
$this->assertNotNull($dup);
|
||
|
||
// نقش متفاوت → تکراری نیست
|
||
$notDup = AddressBookEntry::findDuplicate($this->user->id, 'sender', '09121234567', 'خیابان ولیعصر، پلاک ۱۰، واحد ۲');
|
||
$this->assertNull($notDup);
|
||
}
|
||
|
||
#[Test]
|
||
public function it_returns_form_fields_with_role_prefix()
|
||
{
|
||
$sender = AddressBookEntry::create([...$this->validPayload(['role' => 'sender']), 'user_id' => $this->user->id]);
|
||
|
||
$fields = $sender->toFormFieldData();
|
||
|
||
$this->assertSame('علی رضایی', $fields['sender_name']);
|
||
$this->assertSame('09121234567', $fields['sender_phone']);
|
||
$this->assertArrayNotHasKey('receiver_name', $fields);
|
||
}
|
||
}
|