feat: update allowed frontend hosts and add clinic-pro.ir domain
This commit is contained in:
@@ -34,7 +34,7 @@ APP_BASE_URL=https://api.nobat724.com
|
||||
# ddev exec php docker/gen-cors-env.php (یا روی سرور: php docker/gen-cors-env.php)
|
||||
# و خروجی ALLOWED_FRONTEND_HOSTS را اینجا/در Coolify جایگزین کن.
|
||||
# ⚠️ دیگر CORS_ALLOW_ORIGIN را ست نکن (استفاده نمیشود).
|
||||
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,ardabil-nobat.ir,bandar-nobat.ir,behbahan-nobat.ir,birjand-nobat.ir,bojnord-nobat.ir,bushehr-nobat.ir,dehdasht-nobat.ir,esf-nobat.ir,golestan-nobat.ir,hamadan-nobat.ir,ilam-nobat.ir,karaj-nobat.ir,kerman-nobat.ir,kermanshah-nobat.ir,lorestan-nobat.ir,mashhad-nobat.ir,nobat724.com,qazvin-nobat.ir,qom-nobat.ir,rasht-nobat.ir,sanandaj-nobat.ir,sari-nobat.ir,semnan-nobat.ir,shiraz-nobat.ir,shkord-nobat.ir,tabriz-nobat.ir,tehran-nobat.ir,urmia-nobat.ir,yasuj-nobat.ir,yazd-nobat.ir,zahedan-nobat.ir,zanjan-nobat.ir
|
||||
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,ardabil-nobat.ir,bandar-nobat.ir,behbahan-nobat.ir,birjand-nobat.ir,bojnord-nobat.ir,bushehr-nobat.ir,clinic-pro.ir,dehdasht-nobat.ir,esf-nobat.ir,golestan-nobat.ir,hamadan-nobat.ir,ilam-nobat.ir,karaj-nobat.ir,kerman-nobat.ir,kermanshah-nobat.ir,lorestan-nobat.ir,mashhad-nobat.ir,nobat724.com,qazvin-nobat.ir,qom-nobat.ir,rasht-nobat.ir,sanandaj-nobat.ir,sari-nobat.ir,semnan-nobat.ir,shiraz-nobat.ir,shkord-nobat.ir,tabriz-nobat.ir,tehran-nobat.ir,urmia-nobat.ir,yasuj-nobat.ir,yazd-nobat.ir,zahedan-nobat.ir,zanjan-nobat.ir
|
||||
|
||||
# ── Secrets (REQUIRED — set before the first deploy) ──
|
||||
APP_SECRET= # php -r "echo bin2hex(random_bytes(32));"
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
|
||||
# (CorsRegexEnvProcessor → nelmio). CORS_ALLOW_ORIGIN حذف شده — دیگر ست نکن.
|
||||
# افزودن/حذف دامنه: docker/frontend-domains.json را ویرایش کن، سپس گرفتن خروجی جدید:
|
||||
# ddev exec php docker/gen-cors-env.php (یا روی سرور: php docker/gen-cors-env.php)
|
||||
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,ardabil-nobat.ir,bandar-nobat.ir,behbahan-nobat.ir,birjand-nobat.ir,bojnord-nobat.ir,bushehr-nobat.ir,dehdasht-nobat.ir,esf-nobat.ir,golestan-nobat.ir,hamadan-nobat.ir,ilam-nobat.ir,karaj-nobat.ir,kerman-nobat.ir,kermanshah-nobat.ir,lorestan-nobat.ir,mashhad-nobat.ir,nobat724.com,qazvin-nobat.ir,qom-nobat.ir,rasht-nobat.ir,sanandaj-nobat.ir,sari-nobat.ir,semnan-nobat.ir,shiraz-nobat.ir,shkord-nobat.ir,tabriz-nobat.ir,tehran-nobat.ir,urmia-nobat.ir,yasuj-nobat.ir,yazd-nobat.ir,zahedan-nobat.ir,zanjan-nobat.ir
|
||||
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,ardabil-nobat.ir,bandar-nobat.ir,behbahan-nobat.ir,birjand-nobat.ir,bojnord-nobat.ir,bushehr-nobat.ir,clinic-pro.ir,dehdasht-nobat.ir,esf-nobat.ir,golestan-nobat.ir,hamadan-nobat.ir,ilam-nobat.ir,karaj-nobat.ir,kerman-nobat.ir,kermanshah-nobat.ir,lorestan-nobat.ir,mashhad-nobat.ir,nobat724.com,qazvin-nobat.ir,qom-nobat.ir,rasht-nobat.ir,sanandaj-nobat.ir,sari-nobat.ir,semnan-nobat.ir,shiraz-nobat.ir,shkord-nobat.ir,tabriz-nobat.ir,tehran-nobat.ir,urmia-nobat.ir,yasuj-nobat.ir,yazd-nobat.ir,zahedan-nobat.ir,zanjan-nobat.ir
|
||||
|
||||
# ── api.ir identity inquiry (Shahkar + IbanMatch) ──
|
||||
# Empty token => fail-closed (representative verification is rejected).
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"symfony/asset": "7.4.*",
|
||||
"symfony/cache": "7.4.*",
|
||||
"symfony/console": "7.4.*",
|
||||
"symfony/doctrine-messenger": "7.4.*",
|
||||
"symfony/dotenv": "7.4.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "7.4.*",
|
||||
|
||||
Generated
+77
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "af02756595d9f158a8adcfe3a7d22a68",
|
||||
"content-hash": "a224d59e9e6ce93b3f3f78ee13b06bf5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "altcha-org/altcha",
|
||||
@@ -2917,6 +2917,82 @@
|
||||
],
|
||||
"time": "2026-06-11T07:31:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/doctrine-messenger",
|
||||
"version": "v7.4.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/doctrine-messenger.git",
|
||||
"reference": "c29901ea81dff6557ae3c78c0bad1d559500c2d8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/c29901ea81dff6557ae3c78c0bad1d559500c2d8",
|
||||
"reference": "c29901ea81dff6557ae3c78c0bad1d559500c2d8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/dbal": "^3.6|^4",
|
||||
"php": ">=8.2",
|
||||
"symfony/messenger": "^7.2|^8.0",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/persistence": "<1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/persistence": "^1.3|^2|^3",
|
||||
"symfony/property-access": "^6.4|^7.0|^8.0",
|
||||
"symfony/serializer": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "symfony-messenger-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Doctrine Messenger Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/doctrine-messenger/tree/v7.4.14"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-11T07:31:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/dotenv",
|
||||
"version": "v7.4.14",
|
||||
|
||||
@@ -6,3 +6,9 @@ framework:
|
||||
altcha.pool:
|
||||
adapter: cache.adapter.redis
|
||||
default_lifetime: 600
|
||||
# Pool اختصاصی probe سلامت redis؛ در HealthController با NullLogger اجرا میشود
|
||||
# تا افتادن موقت redis (DNS flap) لاگ warning تکراری در app_log تولید نکند —
|
||||
# وضعیت redis همانجا در پاسخ /health و کد 503 گزارش میشود.
|
||||
health.pool:
|
||||
adapter: cache.adapter.redis
|
||||
default_lifetime: 10
|
||||
|
||||
@@ -20,7 +20,7 @@ framework:
|
||||
max_retries: 0
|
||||
|
||||
routing:
|
||||
'App\Shared\Message\SendSmsMessage': async
|
||||
'App\Sms\Message\SendSmsMessage': async
|
||||
'App\Appointment\Message\ExpireAppointmentsMessage': scheduler_default
|
||||
'App\Shared\Logging\Message\PruneLogsMessage': scheduler_default
|
||||
|
||||
|
||||
@@ -61,6 +61,10 @@ services:
|
||||
$expireSeconds: '%env(int:ALTCHA_EXPIRE_SECONDS)%'
|
||||
$altchaPool: '@altcha.pool'
|
||||
|
||||
App\Shared\Controller\HealthController:
|
||||
arguments:
|
||||
$healthPool: '@health.pool'
|
||||
|
||||
# Persist warning+ logs to the app_log table while keeping stderr output.
|
||||
App\Shared\Logging\DbLogger:
|
||||
decorates: 'logger'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"_comment": "Frontend domains served by this backend. Add an entry per city, then run: ddev exec php docker/gen-cors-env.php — paste the output into Coolify env (CORS_ALLOW_ORIGIN + ALLOWED_FRONTEND_HOSTS), add the same domain to the app service in Coolify UI, and redeploy.",
|
||||
"domains": [
|
||||
{ "domain": "nobat724.com", "label": "اصلی" },
|
||||
{ "domain": "clinic-pro.ir", "label": "پنل ادمین/بکاند" },
|
||||
{ "domain": "ahvaz-nobat.ir", "label": "اهواز" },
|
||||
{ "domain": "arak-nobat.ir", "label": "اراک" },
|
||||
{ "domain": "ardabil-nobat.ir", "label": "اردبیل" },
|
||||
|
||||
+3
-2
@@ -17,7 +17,8 @@
|
||||
- شماره فرستنده تنظیم نمیشود؛ کاوهنگار از خط پیشفرض حساب استفاده میکند.
|
||||
- endpoint `GET /api/v1/admin/settings` یک فیلد read-only به نام `sms_api_key_configured` (boolean) برمیگرداند.
|
||||
- `PATCH /api/v1/admin/settings` کلیدهای `sms_provider`، `kavenegar_api_key`، `kavenegar_sender`، `rangineh_api_key`، `rangineh_sender` را نمیپذیرد (از `ALLOWED_KEYS` حذف شدهاند).
|
||||
- **ترابری (transport) کاوهنگار:** همهی فراخوانیهای `KavehNegarProvider` به کاوهنگار بهصورت **GET با query string** ارسال میشوند (مطابق مستند رسمی) — هیچ درخواست `POST`/`body` ساخته نمیشود. GET بدنه ندارد پس curl هدر `Expect: 100-continue` نمیفرستد؛ این جلوی خطاهای `431 Request Header Fields Too Large` و `Idle timeout` را که در prod دیده میشد میگیرد. هر درخواست `timeout=15s`، `max_duration=30s`، بایپس proxy محیطی (`proxy=null`) و تا **۳ بار retry** با backoff برای خطاهای گذرای شبکه دارد.
|
||||
- **ترابری (transport) کاوهنگار:** همهی فراخوانیهای `KavehNegarProvider` به کاوهنگار بهصورت **GET با query string** ارسال میشوند (مطابق مستند رسمی) — هیچ درخواست `POST`/`body` ساخته نمیشود. GET بدنه ندارد پس curl هدر `Expect: 100-continue` نمیفرستد؛ این جلوی `Idle timeout` را میگیرد. هر درخواست `timeout=15s`، `max_duration=30s`، بایپس proxy محیطی (`proxy=null`) و تا **۳ بار retry** با backoff فقط برای خطاهای **گذرای شبکه** (`TransportException`) دارد؛ خطاهای HTTP کاوهنگار (`4xx`/`5xx`، مثل `431`) دائماند و retry نمیشوند.
|
||||
- **سقف طول توکن (`431` guard):** مقدار طولانی در توکنها (مثل نام کلینیک فارسی که هر کاراکترش ۹ بایت URL-encode میشود) query string را بزرگ میکند و کاوهنگار `431 Request Header Fields Too Large` برمیگرداند. provider هر مقدار توکن را به `MAX_TOKEN_LEN = 60` کاراکتر cap میکند. اگر با این حال کاوهنگار خطای HTTP بدهد، بهجای `error` پرسروصدا فقط یک `warning` کوتاه با کد وضعیت لاگ میشود (چون خطای دائم است، نه transient).
|
||||
- **قیمت هر پیامک** از کلید تنظیمات `sms_price_rials` خوانده میشود (قابل ویرایش در `/admin/settings` → بخش پیامک، و از طریق `PATCH /api/v1/admin/settings`). اگر تنظیم نشده باشد، مقدار پیشفرض `SmsWalletController::SMS_PRICE_RIALS = 500` ریال بهعنوان fallback استفاده میشود. `GET /api/v1/sms/wallet/balance` این مقدار را در `sms_price_rials` و تعداد تخمینی پیامک را در `estimated_sms_count` برمیگرداند.
|
||||
|
||||
---
|
||||
@@ -484,7 +485,7 @@ Updated template with `status: "rejected"`.
|
||||
**همهی پیامکهای سیستمی از طریق Kavenegar VerifyLookup (`verify/lookup.json`) ارسال میشوند** (نه متنآزاد `sms/send`). هر تمپلت دو فیلد اضافه دارد:
|
||||
|
||||
- `kavenegar_template`: نام تمپلت مصوب در پنل کاوهنگار. **متن واقعیِ ارسالی از همین تمپلت پنل میآید، نه از `body` دیتابیس**؛ `body` فقط برای پیشنمایش ادمین و رندرِ رکورد `SmsLog` استفاده میشود و باید دستی با تمپلت پنل همراستا نگه داشته شود.
|
||||
- `token_map`: نگاشت متغیر منطقی → جایگاه کاوهنگار. **قانون کاوهنگار:** هر الگو **حتماً باید `%token` داشته باشد** (token1 اجباری است)؛ پس token_map هر تگ یک مقدار به `token` میدهد. قانون فاصله: `token`/`token2`/`token3` **فاصله نمیپذیرند** — provider (`KavehNegarProvider`) فاصلههای مقدارِ این جایگاهها را خودکار با نیمفاصله (ZWNJ) جایگزین میکند؛ `token10`/`token20` فاصله را بدون تغییر میپذیرند. پس ترجیحاً مقادیر بدونفاصله (کد/تاریخ/ساعت/username/لینک) در `token`/`token2`/`token3` و مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک) در `token10`/`token20`.
|
||||
- `token_map`: نگاشت متغیر منطقی → جایگاه کاوهنگار. **قانون کاوهنگار:** هر الگو **حتماً باید `%token` داشته باشد** (token1 اجباری است)؛ پس token_map هر تگ یک مقدار به `token` میدهد. قانون فاصله: `token`/`token2`/`token3` **فاصله نمیپذیرند** — provider (`KavehNegarProvider`) فاصلههای مقدارِ این جایگاهها را خودکار با نیمفاصله (ZWNJ) جایگزین میکند؛ `token10`/`token20` فاصله را بدون تغییر میپذیرند. پس ترجیحاً مقادیر بدونفاصله (کد/تاریخ/ساعت/username/لینک) در `token`/`token2`/`token3` و مقادیر دارای فاصله (نام دکتر/بیمار/کلینیک) در `token10`/`token20`. مقدار هر جایگاه به `MAX_TOKEN_LEN = 60` کاراکتر cap میشود تا از خطای `431` جلوگیری شود.
|
||||
|
||||
`SmsService::dispatchTemplate(tag, mobile, vars)` نقطهی واحدِ ارسال است: `kavenegar_template` و `token_map` را از رکورد DB (یا `SmsMessageTemplate::DEFAULTS`) میخواند، `vars` را به جایگاهها نگاشت میکند و با VerifyLookup میفرستد. اگر `kavenegar_template` تعریف نشده باشد → fallback به ارسال متنآزاد با `body` رندرشده.
|
||||
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
namespace App\Shared\Controller;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
class HealthController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly CacheInterface $cache,
|
||||
) {}
|
||||
private readonly CacheItemPoolInterface $healthPool,
|
||||
) {
|
||||
// probe سلامت نباید هنگام افتادن redis لاگ warning تولید کند؛ logger این pool را خنثی میکنیم.
|
||||
if ($this->healthPool instanceof LoggerAwareInterface) {
|
||||
$this->healthPool->setLogger(new NullLogger());
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/health', methods: ['GET'])]
|
||||
public function __invoke(): JsonResponse
|
||||
@@ -29,7 +36,7 @@ class HealthController
|
||||
}
|
||||
|
||||
try {
|
||||
$item = $this->cache->getItem('health_check');
|
||||
$this->healthPool->getItem('health_check');
|
||||
$checks['redis'] = 'ok';
|
||||
} catch (\Throwable) {
|
||||
$checks['redis'] = 'error';
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\Message;
|
||||
|
||||
class SendSmsMessage
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $mobile,
|
||||
public readonly string $message,
|
||||
public readonly ?int $smsLogId = null,
|
||||
) {}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Sms\Provider;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
@@ -57,6 +58,9 @@ class KavehNegarProvider implements SmsProviderInterface
|
||||
'sender' => $this->sender(),
|
||||
]);
|
||||
return ($data['return']['status'] ?? 0) === 200;
|
||||
} catch (HttpExceptionInterface $e) {
|
||||
$this->logger->warning(sprintf('SMS send rejected (kavenegar): HTTP %d', $e->getResponse()->getStatusCode()), ['mobile' => $mobile]);
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(sprintf('SMS send failed (kavenegar): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'mobile' => $mobile]);
|
||||
return false;
|
||||
@@ -66,12 +70,20 @@ class KavehNegarProvider implements SmsProviderInterface
|
||||
// token/token2/token3 مقدارِ دارای فاصله را رد میکنند؛ فاصله را با نیمفاصله (ZWNJ) جایگزین میکنیم.
|
||||
private const NO_SPACE_SLOTS = ['token', 'token2', 'token3'];
|
||||
|
||||
// کاوهنگار روی طول توکن سقف دارد؛ مقدار بلند (مثلاً نام کلینیک فارسی که هر کاراکترش
|
||||
// ۹ بایت URL-encode میشود) درخواست را بزرگ میکند و پاسخ 431 میگیریم. مقدار را cap میکنیم.
|
||||
private const MAX_TOKEN_LEN = 60;
|
||||
|
||||
private function forSlot(string $slot, string $value): string
|
||||
{
|
||||
if (!in_array($slot, self::NO_SPACE_SLOTS, true)) {
|
||||
return $value;
|
||||
$value = trim($value);
|
||||
if (in_array($slot, self::NO_SPACE_SLOTS, true)) {
|
||||
$value = preg_replace('/\s+/u', "\u{200C}", $value) ?? $value;
|
||||
}
|
||||
return preg_replace('/\s+/u', "\u{200C}", $value) ?? $value;
|
||||
if (mb_strlen($value, 'UTF-8') > self::MAX_TOKEN_LEN) {
|
||||
$value = mb_substr($value, 0, self::MAX_TOKEN_LEN, 'UTF-8');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function sendTemplate(string $mobile, string $templateCode, array $vars): bool
|
||||
@@ -95,6 +107,11 @@ class KavehNegarProvider implements SmsProviderInterface
|
||||
}
|
||||
$data = $this->get('/verify/lookup.json', $params);
|
||||
return ($data['return']['status'] ?? 0) === 200;
|
||||
} catch (HttpExceptionInterface $e) {
|
||||
// 4xx/5xx از کاوهنگار (مثل 431): خطای دائم درخواست است نه transient؛
|
||||
// warning کوتاه با کد وضعیت، بدون dump کامل exception.
|
||||
$this->logger->warning(sprintf('SMS sendTemplate rejected (kavenegar): HTTP %d', $e->getResponse()->getStatusCode()), ['mobile' => $mobile, 'template' => $templateCode]);
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error(sprintf('SMS sendTemplate failed (kavenegar): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'mobile' => $mobile, 'template' => $templateCode]);
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Shared;
|
||||
|
||||
use App\Shared\Controller\HealthController;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* probe سلامت redis نباید هنگام افتادن redis لاگ warning در app_log بریزد:
|
||||
* HealthController باید logger این pool را با NullLogger خنثی کند و صرفاً وضعیت را برگرداند.
|
||||
*/
|
||||
class HealthControllerTest extends TestCase
|
||||
{
|
||||
private function em(): EntityManagerInterface
|
||||
{
|
||||
$conn = $this->createMock(Connection::class);
|
||||
$conn->method('executeQuery'); // SELECT 1 → موفق
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$em->method('getConnection')->willReturn($conn);
|
||||
|
||||
return $em;
|
||||
}
|
||||
|
||||
public function testSilencesLoggerOnPool(): void
|
||||
{
|
||||
$pool = new class implements CacheItemPoolInterface, LoggerAwareInterface {
|
||||
public ?LoggerInterface $logger = null;
|
||||
public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; }
|
||||
public function getItem($key): CacheItemInterface { throw new \RuntimeException('redis down'); }
|
||||
public function getItems(array $keys = []): iterable { return []; }
|
||||
public function hasItem($key): bool { return false; }
|
||||
public function clear(): bool { return true; }
|
||||
public function deleteItem($key): bool { return true; }
|
||||
public function deleteItems(array $keys): bool { return true; }
|
||||
public function save(CacheItemInterface $item): bool { return true; }
|
||||
public function saveDeferred(CacheItemInterface $item): bool { return true; }
|
||||
public function commit(): bool { return true; }
|
||||
};
|
||||
|
||||
$controller = new HealthController($this->em(), $pool);
|
||||
|
||||
// logger فوراً در constructor خنثی میشود
|
||||
$this->assertInstanceOf(NullLogger::class, $pool->logger);
|
||||
|
||||
// redis افتاده → degraded + 503، بدون throw
|
||||
$response = $controller();
|
||||
$this->assertSame(503, $response->getStatusCode());
|
||||
$body = json_decode($response->getContent(), true);
|
||||
$this->assertSame('degraded', $body['status']);
|
||||
$this->assertSame('error', $body['checks']['redis']);
|
||||
$this->assertSame('ok', $body['checks']['database']);
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,25 @@ class KavehNegarProviderTest extends TestCase
|
||||
$this->assertSame('GET', $this->captured['method']);
|
||||
$this->assertStringContainsString('/TESTKEY/sms/send.json', $this->captured['url']);
|
||||
}
|
||||
|
||||
/** توکن بلند (نام فارسی) باید cap شود تا URL بزرگ نشود و کاوهنگار 431 ندهد. */
|
||||
public function testLongTokenIsCapped(): void
|
||||
{
|
||||
$long = str_repeat('ک', 200); // 200 کاراکتر فارسی
|
||||
$ok = $this->provider()->sendTemplate('09120671756', 'clinicpro-welcome', ['token' => $long]);
|
||||
|
||||
$this->assertTrue($ok);
|
||||
parse_str(parse_url($this->captured['url'], PHP_URL_QUERY), $q);
|
||||
$this->assertLessThanOrEqual(60, mb_strlen($q['token'], 'UTF-8'));
|
||||
}
|
||||
|
||||
/** خطای 4xx کاوهنگار (مثل 431) باید false برگرداند و throw نکند. */
|
||||
public function testHttpErrorReturnsFalseWithoutThrowing(): void
|
||||
{
|
||||
$client = new MockHttpClient(fn (): MockResponse => new MockResponse('too large', ['http_code' => 431]));
|
||||
$provider = new KavehNegarProvider($client, new NullLogger(), 'TESTKEY', '10004346');
|
||||
|
||||
$this->assertFalse($provider->sendTemplate('09120671756', 'clinicpro-welcome', ['token' => 'x']));
|
||||
$this->assertFalse($provider->send('09120671756', 'hello'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user