ifnex/04_Laravel/tests/Feature/Console/IfnexImportFxRatesTest.php
Kazem Alghasi f1d5667fe8 feat(deploy): preflight + smoke-test + UAT checklist (phases 3.9.1–3.11)
Prepare the project for production deployment and final client
acceptance testing. Adds preflight fail-fast command, hardened FX rate
importer, currency seeder, deploy scripts, E2E smoke test, and the
43-item UAT checklist document with sign-off form.

Key changes:

- Phase 3.9.1 — hotfix (AddressBookEntry):
  - Add $attributes defaults (use_count=0, is_default=false) so
    instance values are populated right after create() without
    refresh(). Resolves the red test
    `it_increments_use_count_and_sets_last_used_at`.

- Phase 3.10 — deploy infrastructure (Laravel):
  - Add `ifnex:preflight` command: fail-fast audit of production
    environment (APP_ENV, APP_DEBUG, APP_KEY, IFNEX_API_KEY,
    IFNEX_BRIDGE_API_KEY, ZARINPAL_SANDBOX, DB_CONNECTION, CORS,
    currencies seeded, PHP extensions, storage writable). Exits 1
    if any check fails; supports --skip-db for pre-migrate runs.
  - Add `ifnex:import-fx-rates` command: replacement for
    `ifnex:update-rates`. Fetches USD/AED from Exir.io + EUR/CNY
    from FreeCurrencyAPI. Supports --auto-approve (cron use) and
    --threshold (max change % for auto-approve). Logs critical on
    HTTP failure. URL configurable via EXIR_API_URL env.
  - Add `CurrencySeeder`: creates 5 base currencies (USD/AED/EUR/
    CNY/IRR) with rate=0 (IRR=1). Idempotent — does not overwrite
    real rates already set by admin or cron.
  - Update `DatabaseSeeder`: skip super_admin with password
    'password' in production (still created in local/testing).
  - Fix `bootstrap/app.php`: remove dead `use ApiKeyAuth` import
    (actual class is ApiKeyMiddleware). Latent fatal if api_key
    alias was used.
  - Add `config/ifnex.php` exir block + preflight config.
  - Harden `.env.example`: MySQL default, EXIR_API_URL,
    SANCTUM_STATEFUL_DOMAINS, APP_TIMEZONE, LOG_DAILY_DAYS, fix
    duplicate CURRENCY_API_KEY.

- Phase 3.10 — deploy scripts (Linux):
  - Add `scripts/deploy.sh`: idempotent 13-step deploy (composer
    install → key:generate → preflight → migrate → seed → FX import
    → cache → chown). Fail-fast with set -e.
  - Add `scripts/cron-exir-rates.sh`: daily cron wrapper for
    `ifnex:import-fx-rates --source=all --auto-approve`.

- Phase 3.10.1 — preflight hotfix:
  - Make expected DB connection configurable
    (ifnex.preflight.expected_db_connection) instead of hardcoded
    'mysql'. Prevents cascade test failure when tests would have to
    mutate database.default to mysql in a sqlite test environment
    (Unknown database ':memory:' → broken connection pool → 22
    previously-green tests fail with "There is already an active
    transaction").
  - Rewrite IfnexPreflightTest to use config() rather than mutating
    database.default.

- Phase 3.11 — UAT:
  - Add `ifnex:smoke-test` command: E2E post-deploy automated test
    (25+ checks: DB connection, core tables, currencies with
    non-zero rates, countries count, roles, admin user, system
    settings, storage writable, config/route cache, 4 API
    endpoints: /up, /countries, /track/{invalid}, /customer/profile
    without token).
  - Add 9 feature tests for smoke-test (PASS + 6 FAIL + custom URL
    + 422/500 handling + auth bypass detection).
  - Add `UAT-3.11.md`: 43-item Persian checklist for client
    acceptance testing across 15 feature areas. Includes sign-off
    form for both parties (client + VernaSoft).
  - Update `CLIENT_DELIVERY.md` to record phases 3.10 and 3.11.

Tests:
- Total: 51 → 82 tests, 150 → 220+ assertions, all green.
- New: IfnexPreflightTest (9), IfnexImportFxRatesTest (9),
  CurrencySeederTest (4), IfnexSmokeTestTest (9) = 31 new tests.

Documentation:
- DEPLOYMENT_HANDOFF.md §2.4: add CurrencySeeder + import-fx-rates.
- DEPLOYMENT_HANDOFF.md §2.8: update cron to use cron-exir-rates.sh.
- UAT-3.11.md: new 43-item client acceptance checklist + sign-off.
- CLIENT_DELIVERY.md: record phases 3.10 and 3.11 completion.
2026-10-31 22:18:15 +03:30

244 lines
8.4 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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\ExchangeRateHistory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* تست‌های دستور ifnex:import-fx-rates (فاز ۳.۱۰)
*
* این تست‌ها با Http::fake کار می‌کنند تا بدون اتصال واقعی به Exir.io
* یا FreeCurrencyAPI قابل اجرا باشند.
*/
class IfnexImportFxRatesTest extends TestCase
{
use RefreshDatabase;
private function seedCurrencies(): void
{
foreach (['IRR', 'USD', 'AED', 'EUR', 'CNY'] as $code) {
Currency::create([
'code' => $code,
'name' => $code,
'symbol' => $code,
'rate_to_rial' => $code === 'IRR' ? 1 : 0, // غیر IRR صفر → اولین import باید بنویسد
'is_active' => true,
]);
}
}
// ═══════════════════ موفقیت ‌ها ═══════════════════
#[Test]
public function it_fetches_usd_and_aed_from_exir_with_auto_approve()
{
$this->seedCurrencies();
Http::fake([
'https://api.exir.io/*' => Http::response([
'usdt-rls' => ['price' => 60000.0], // 1 دلار = ۶۰,۰۰۰ تومان
'usdt-aed' => ['price' => 0.27], // 1 درهم = 0.27 تتر → 0.27 × 60000 = 16200 ریال
], 200),
]);
$this->artisan('ifnex:import-fx-rates', ['--source' => 'exir', '--auto-approve' => true])
->assertSuccessful();
$this->assertDatabaseHas('currencies', ['code' => 'USD', 'rate_to_rial' => 60000.000]);
$this->assertDatabaseHas('currencies', ['code' => 'AED', 'rate_to_rial' => 16200.000]);
// باید رکورد approved در تاریخچه ساخته شود
$this->assertDatabaseHas('exchange_rate_history', [
'currency_from' => 'USD',
'status' => 'approved',
'api_source' => 'exir.io',
]);
}
#[Test]
public function it_creates_pending_when_change_exceeds_auto_approve_threshold()
{
$this->seedCurrencies();
// USD قبلاً ۶۰,۰۰۰ بوده؛ حالا ۸۰,۰۰۰ → تغییر ۳۳٪ > threshold 5٪
Currency::where('code', 'USD')->update(['rate_to_rial' => 60000]);
Currency::where('code', 'AED')->update(['rate_to_rial' => 16200]);
Http::fake([
'https://api.exir.io/*' => Http::response([
'usdt-rls' => ['price' => 80000.0], // ۳۳٪ تغییر
'usdt-aed' => ['price' => 0.30],
], 200),
]);
$this->artisan('ifnex:import-fx-rates', [
'--source' => 'exir',
'--auto-approve' => true,
'--threshold' => 5,
])->assertSuccessful();
// باید pending بسازد چون تغییر > threshold
$this->assertDatabaseHas('exchange_rate_history', [
'currency_from' => 'USD',
'status' => 'pending',
]);
// نرخ currencies نباید تغییر کرده باشد (چون pending است)
$this->assertSame('60000.0000', Currency::where('code', 'USD')->value('rate_to_rial'));
}
#[Test]
public function it_creates_pending_by_default_without_auto_approve()
{
$this->seedCurrencies();
Currency::where('code', 'USD')->update(['rate_to_rial' => 60000]);
Http::fake([
'https://api.exir.io/*' => Http::response([
'usdt-rls' => ['price' => 90000.0], // ۵۰٪ تغییر
], 200),
]);
$this->artisan('ifnex:import-fx-rates', ['--source' => 'exir'])
->assertSuccessful();
// بدون --auto-approve همیشه pending می‌سازد
$this->assertDatabaseHas('exchange_rate_history', [
'currency_from' => 'USD',
'status' => 'pending',
]);
// نرخ currencies نباید تغییر کند
$this->assertSame('60000.0000', Currency::where('code', 'USD')->value('rate_to_rial'));
}
#[Test]
public function it_skips_when_change_is_below_threshold()
{
$this->seedCurrencies();
Currency::where('code', 'USD')->update(['rate_to_rial' => 60000]);
// تغییر فقط ۱٪ < 0.5٪ threshold؟ نه — threshold پیش‌فرض 0.5٪، پس ۱٪ باید رکورد بسازد
// برای تست skip، تغییر بسیار کوچک می‌زنیم
Http::fake([
'https://api.exir.io/*' => Http::response([
'usdt-rls' => ['price' => 60100.0], // ۰.۱۷٪ تغییر < 0.5٪
], 200),
]);
$this->artisan('ifnex:import-fx-rates', ['--source' => 'exir'])
->assertSuccessful();
// نباید هیچ رکوردی در تاریخچه بسازد
$this->assertDatabaseCount('exchange_rate_history', 0);
// نرخ currencies هم نباید تغییر کند
$this->assertSame('60000.0000', Currency::where('code', 'USD')->value('rate_to_rial'));
}
#[Test]
public function it_handles_exir_http_failure_gracefully()
{
$this->seedCurrencies();
Http::fake([
'https://api.exir.io/*' => Http::response([], 500),
]);
$result = $this->artisan('ifnex:import-fx-rates', ['--source' => 'exir']);
// نباید کد موفق بدهد چون هیچ نرخی دریافت نشد
$result->assertFailed();
// هیچ رکوردی نباید ساخته شود
$this->assertDatabaseCount('exchange_rate_history', 0);
}
#[Test]
public function it_skips_freecurrencyapi_when_api_key_is_missing()
{
$this->seedCurrencies();
config([
'ifnex.currency.api_key' => '', // بدون کلید
]);
Http::fake([
'https://api.freecurrencyapi.com/*' => Http::response([
'data' => ['IRR' => 60000, 'EUR' => 0.95, 'CNY' => 7.2],
], 200),
]);
$this->artisan('ifnex:import-fx-rates', ['--source' => 'freecurrencyapi'])
->assertFailed(); // چون هیچ نرخی دریافت نشد
// نباید رکوردی ساخته شود
$this->assertDatabaseCount('exchange_rate_history', 0);
}
#[Test]
public function it_fetches_eur_and_cny_from_freecurrencyapi()
{
$this->seedCurrencies();
config([
'ifnex.currency.api_key' => 'test-key-123',
]);
Http::fake([
'https://api.freecurrencyapi.com/*' => Http::response([
'data' => [
'IRR' => 60000.0, // 1 USD = 60,000 IRR
'EUR' => 0.95, // 1 USD = 0.95 EUR → 1 EUR = 0.95 × 60000 = 57000 IRR
'CNY' => 7.2, // 1 USD = 7.2 CNY → 1 CNY = 7.2 × 60000 = 432000 IRR
],
], 200),
]);
$this->artisan('ifnex:import-fx-rates', [
'--source' => 'freecurrencyapi',
'--auto-approve' => true,
])->assertSuccessful();
// باید در currencies نوشته شود
$this->assertDatabaseHas('currencies', ['code' => 'EUR']);
$this->assertDatabaseHas('currencies', ['code' => 'CNY']);
$this->assertDatabaseHas('exchange_rate_history', [
'currency_from' => 'EUR',
'status' => 'approved',
'api_source' => 'freecurrencyapi.com',
]);
}
#[Test]
public function it_warns_when_currencies_table_is_empty()
{
// جدول خالی است — نباید crash کند، باید هشدار دهد
Http::fake([
'https://api.exir.io/*' => Http::response([
'usdt-rls' => ['price' => 60000.0],
], 200),
]);
$this->artisan('ifnex:import-fx-rates', ['--source' => 'exir'])
->assertSuccessful();
// چون ارز USD وجود ندارد، نرخ نوشته نمی‌شود
$this->assertDatabaseCount('exchange_rate_history', 0);
$this->assertDatabaseCount('currencies', 0);
}
#[Test]
public function it_rejects_invalid_source_option()
{
$this->artisan('ifnex:import-fx-rates', ['--source' => 'invalid'])
->assertFailed();
}
}