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 دقیق (که به APP_URL وابسته است)، از closure * استفاده می‌کنیم که بر اساس مسیر (path) تطبیق می‌دهد. این‌طوری حتی اگر * config('app.url') چیزی غیر از http://localhost برگرداند، fake همچنان کار می‌کند. */ private function fakeApiEndpoints(string $baseUrl = 'http://localhost'): void { Http::fake(function ($request) { $url = $request->url(); // تطبیق بر اساس مسیر نه host — به‌این ترتیب هم localhost و هم // system.ifnex.ir و هر URL دیگری پشتیبانی می‌شود. if (str_ends_with($url, '/up')) { return Http::response('OK', 200); } if (str_contains($url, '/api/v1/countries')) { return Http::response(['data' => []], 200); } if (str_contains($url, '/api/v1/track/')) { return Http::response([], 404); } if (str_ends_with($url, '/api/v1/customer/profile')) { return Http::response(['message' => 'Unauthenticated'], 401); } // fallback برای هر URL دیگر که smoke-test ممکن است صدا بزند return Http::response([], 200); }); } // ═══════════════════ 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 — از closure برای تطبیق مسیر استفاده می‌کنیم Http::fake(function ($request) { $url = $request->url(); if (str_ends_with($url, '/up')) { return Http::response('Server Error', 500); } if (str_contains($url, '/api/v1/countries')) { return Http::response(['data' => []], 200); } if (str_contains($url, '/api/v1/track/')) { return Http::response([], 404); } if (str_ends_with($url, '/api/v1/customer/profile')) { return Http::response(['message' => 'Unauthenticated'], 401); } return Http::response([], 200); }); $this->artisan('ifnex:smoke-test') ->assertFailed(); } #[Test] public function it_fails_when_protected_endpoint_allows_unauthenticated_access() { $this->seedHealthyData(); // شبیه‌سازی باگ امنیتی: endpoint محافظت‌شده، 200 برمی‌گرداند (به‌جای 401) Http::fake(function ($request) { $url = $request->url(); if (str_ends_with($url, '/up')) { return Http::response('OK', 200); } if (str_contains($url, '/api/v1/countries')) { return Http::response(['data' => []], 200); } if (str_contains($url, '/api/v1/track/')) { return Http::response([], 404); } if (str_ends_with($url, '/api/v1/customer/profile')) { return Http::response(['data' => ['id' => 1]], 200); // ← باید 401 باشد! } return Http::response([], 200); }); $this->artisan('ifnex:smoke-test') ->assertFailed(); } #[Test] public function it_accepts_custom_base_url() { $this->seedHealthyData(); $customUrl = 'https://system.ifnex.ir'; // از closure استفاده می‌کنیم تا URL پایه مهم نباشد Http::fake(function ($request) { $url = $request->url(); if (str_ends_with($url, '/up')) { return Http::response('OK', 200); } if (str_contains($url, '/api/v1/countries')) { return Http::response(['data' => []], 200); } if (str_contains($url, '/api/v1/track/')) { return Http::response([], 404); } if (str_ends_with($url, '/api/v1/customer/profile')) { return Http::response(['message' => 'Unauthenticated'], 401); } return Http::response([], 200); }); $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(function ($request) { $url = $request->url(); if (str_ends_with($url, '/up')) { return Http::response('OK', 200); } if (str_contains($url, '/api/v1/countries')) { return Http::response(['data' => []], 200); } if (str_contains($url, '/api/v1/track/')) { return Http::response(['errors' => []], 422); // ← 422 به‌جای 404 } if (str_ends_with($url, '/api/v1/customer/profile')) { return Http::response(['message' => 'Unauthenticated'], 401); } return Http::response([], 200); }); $this->artisan('ifnex:smoke-test') ->assertSuccessful(); } #[Test] public function it_fails_when_tracking_endpoint_returns_500() { $this->seedHealthyData(); // 500 یعنی باگ در controller — نباید رخ دهد Http::fake(function ($request) { $url = $request->url(); if (str_ends_with($url, '/up')) { return Http::response('OK', 200); } if (str_contains($url, '/api/v1/countries')) { return Http::response(['data' => []], 200); } if (str_contains($url, '/api/v1/track/')) { return Http::response('Fatal', 500); } if (str_ends_with($url, '/api/v1/customer/profile')) { return Http::response(['message' => 'Unauthenticated'], 401); } return Http::response([], 200); }); $this->artisan('ifnex:smoke-test') ->assertFailed(); } }