Add `ifnex:smoke-test` console command to perform automated end-to-end checks post-deployment, including database connectivity, essential data presence, and API endpoint availability. - Implement `IfnexSmokeTest` command for automated system verification - Add feature tests for the smoke test command - Create `UAT-3.11.md` containing the client acceptance checklist - Update `CLIENT_DELIVERY.md` to reflect phase 3.10 and 3.11 completion - Register the new command in `bootstrap/app.php` - Add project documentation directory
247 lines
8.7 KiB
PHP
247 lines
8.7 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Console;
|
||
|
||
use App\Models\Currency;
|
||
use App\Models\User;
|
||
use Database\Seeders\CountriesTableSeeder;
|
||
use Database\Seeders\CurrencySeeder;
|
||
use Database\Seeders\RoleAndPermissionSeeder;
|
||
use Database\Seeders\SystemSettingSeeder;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Illuminate\Support\Facades\Http;
|
||
use PHPUnit\Framework\Attributes\Test;
|
||
use Spatie\Permission\Models\Role;
|
||
use Tests\TestCase;
|
||
|
||
/**
|
||
* تستهای دستور ifnex:smoke-test (فاز ۳.۱۱)
|
||
*
|
||
* این تستها با Http::fake کار میکنند تا بدون اتصال واقعی به HTTP، رفتار
|
||
* smoke-test را بررسی کنند. حالتهای اصلی:
|
||
* - تمام سبز وقتی همهچیز سالم است
|
||
* - شکست وقتی جدول currencies خالی است
|
||
* - شکست وقتی نقشها موجود نیستند
|
||
* - شکست وقتی هیچ ادمینی وجود ندارد
|
||
*/
|
||
class IfnexSmokeTestTest extends TestCase
|
||
{
|
||
use RefreshDatabase;
|
||
|
||
/**
|
||
* راهاندازی دادههای سالم برای تستهای PASS.
|
||
* شامل: ارزها، کشورها، نقشها، تنظیمات سیستم، یک کاربر ادمین.
|
||
*/
|
||
private function seedHealthyData(): void
|
||
{
|
||
$this->seed(CurrencySeeder::class);
|
||
$this->seed(CountriesTableSeeder::class);
|
||
$this->seed(RoleAndPermissionSeeder::class);
|
||
$this->seed(SystemSettingSeeder::class);
|
||
|
||
// نرخهای غیر صفر برای ارزها (شبیهسازی import-fx-rates اجرا شده)
|
||
Currency::where('code', 'USD')->update(['rate_to_rial' => 60000]);
|
||
Currency::where('code', 'AED')->update(['rate_to_rial' => 16200]);
|
||
Currency::where('code', 'EUR')->update(['rate_to_rial' => 65000]);
|
||
Currency::where('code', 'CNY')->update(['rate_to_rial' => 8000]);
|
||
|
||
// یک کاربر ادمین
|
||
$admin = User::factory()->create(['email' => 'admin@ifnex.ir']);
|
||
$admin->assignRole('super_admin');
|
||
}
|
||
|
||
/**
|
||
* HTTP fake برای endpointهای API که smoke-test صدا میزند.
|
||
* URL پایه بهصورت localhost در نظر گرفته میشود (APP_URL پیشفرض تست).
|
||
*/
|
||
private function fakeApiEndpoints(string $baseUrl = 'http://localhost'): void
|
||
{
|
||
Http::fake([
|
||
"{$baseUrl}/up" => Http::response('OK', 200),
|
||
"{$baseUrl}/api/v1/countries" => Http::response(['data' => []], 200),
|
||
"{$baseUrl}/api/v1/track/INVALID-AWB-TEST" => Http::response([], 404),
|
||
"{$baseUrl}/api/v1/customer/profile" => Http::response(['message' => 'Unauthenticated'], 401),
|
||
]);
|
||
}
|
||
|
||
// ═══════════════════ PASS scenarios ═══════════════════
|
||
|
||
#[Test]
|
||
public function it_passes_when_everything_is_healthy()
|
||
{
|
||
$this->seedHealthyData();
|
||
$this->fakeApiEndpoints();
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertSuccessful();
|
||
}
|
||
|
||
// ═══════════════════ FAIL scenarios ═══════════════════
|
||
|
||
#[Test]
|
||
public function it_fails_when_currencies_table_is_empty()
|
||
{
|
||
// ارزها ساخته نمیشوند — جدول خالی
|
||
$this->seed(CountriesTableSeeder::class);
|
||
$this->seed(RoleAndPermissionSeeder::class);
|
||
$this->seed(SystemSettingSeeder::class);
|
||
|
||
$admin = User::factory()->create();
|
||
$admin->assignRole('super_admin');
|
||
|
||
$this->fakeApiEndpoints();
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_currency_rate_is_zero()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
// شبیهسازی نرخ صفر — import-fx-rates هنوز اجرا نشده
|
||
Currency::where('code', 'USD')->update(['rate_to_rial' => 0]);
|
||
|
||
$this->fakeApiEndpoints();
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_no_admin_user_exists()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
// حذف همهٔ کاربران ادمین
|
||
User::query()->delete();
|
||
|
||
$this->fakeApiEndpoints();
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_countries_not_seeded()
|
||
{
|
||
$this->seed(CurrencySeeder::class);
|
||
$this->seed(RoleAndPermissionSeeder::class);
|
||
$this->seed(SystemSettingSeeder::class);
|
||
|
||
// کشورها ساخته نمیشوند
|
||
|
||
$admin = User::factory()->create();
|
||
$admin->assignRole('super_admin');
|
||
|
||
$this->fakeApiEndpoints();
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_roles_not_seeded()
|
||
{
|
||
// بدون RoleAndPermissionSeeder — نقشها موجود نیستند
|
||
$this->seed(CurrencySeeder::class);
|
||
$this->seed(CountriesTableSeeder::class);
|
||
$this->seed(SystemSettingSeeder::class);
|
||
|
||
// کاربر بدون assignRole (چون نقش وجود ندارد)
|
||
User::factory()->create();
|
||
|
||
$this->fakeApiEndpoints();
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_health_endpoint_returns_500()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
// شبیهسازی شکست /up
|
||
Http::fake([
|
||
'http://localhost/up' => Http::response('Server Error', 500),
|
||
'http://localhost/api/v1/countries' => Http::response(['data' => []], 200),
|
||
'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response([], 404),
|
||
'http://localhost/api/v1/customer/profile' => Http::response(['message' => 'Unauthenticated'], 401),
|
||
]);
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_protected_endpoint_allows_unauthenticated_access()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
// شبیهسازی باگ امنیتی: endpoint محافظتشده، 200 برمیگرداند (بهجای 401)
|
||
Http::fake([
|
||
'http://localhost/up' => Http::response('OK', 200),
|
||
'http://localhost/api/v1/countries' => Http::response(['data' => []], 200),
|
||
'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response([], 404),
|
||
'http://localhost/api/v1/customer/profile' => Http::response(['data' => ['id' => 1]], 200), // ← باید 401 باشد!
|
||
]);
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_accepts_custom_base_url()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
$customUrl = 'https://system.ifnex.ir';
|
||
|
||
Http::fake([
|
||
"{$customUrl}/up" => Http::response('OK', 200),
|
||
"{$customUrl}/api/v1/countries" => Http::response(['data' => []], 200),
|
||
"{$customUrl}/api/v1/track/INVALID-AWB-TEST" => Http::response([], 404),
|
||
"{$customUrl}/api/v1/customer/profile" => Http::response(['message' => 'Unauthenticated'], 401),
|
||
]);
|
||
|
||
$this->artisan('ifnex:smoke-test', ['--url' => $customUrl])
|
||
->assertSuccessful();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_accepts_422_for_tracking_endpoint()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
// ممکن است controller برای AWB نامعتبر، 422 برگرداند بهجای 404 — این هم قابلقبول است
|
||
Http::fake([
|
||
'http://localhost/up' => Http::response('OK', 200),
|
||
'http://localhost/api/v1/countries' => Http::response(['data' => []], 200),
|
||
'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response(['errors' => []], 422),
|
||
'http://localhost/api/v1/customer/profile' => Http::response(['message' => 'Unauthenticated'], 401),
|
||
]);
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertSuccessful();
|
||
}
|
||
|
||
#[Test]
|
||
public function it_fails_when_tracking_endpoint_returns_500()
|
||
{
|
||
$this->seedHealthyData();
|
||
|
||
// 500 یعنی باگ در controller — نباید رخ دهد
|
||
Http::fake([
|
||
'http://localhost/up' => Http::response('OK', 200),
|
||
'http://localhost/api/v1/countries' => Http::response(['data' => []], 200),
|
||
'http://localhost/api/v1/track/INVALID-AWB-TEST' => Http::response('Fatal', 500),
|
||
'http://localhost/api/v1/customer/profile' => Http::response(['message' => 'Unauthenticated'], 401),
|
||
]);
|
||
|
||
$this->artisan('ifnex:smoke-test')
|
||
->assertFailed();
|
||
}
|
||
}
|