ifnex/04_Laravel/tests/Feature/Console/IfnexSmokeTestTest.php
Kazem Alghasi 8d61eb19d5 feat(uat): implement smoke test command and UAT documentation
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
2026-11-01 11:36:25 +03:30

247 lines
8.7 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\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();
}
}