feat: 增强钱包 API URL 验证与配置
- 在 AdminIntegrationSiteStoreRequest 和 AdminIntegrationSiteUpdateRequest 中引入 WalletApiUrlRule,确保 wallet_api_url 字段符合 HTTPS 公开域名要求。 - 更新 HttpMainSiteWalletBalanceClient 和 HttpMainSiteWalletGateway,使用 WalletApiUrlSanitizer 进行 URL 规范化与验证,防止 SSRF 攻击。 - 新增测试用例,验证 wallet_api_url 的有效性,确保系统安全性与稳定性。 - 更新 .env.example 文件,添加 LOTTERY_RISK_POOL_USE_REDIS_LUA 配置项以支持 Redis Lua 原子扣减功能。 - 修改 package-lock.json 中的项目名称,确保一致性。 - 在 API 路由中新增 integration/runtime-origins 路由,提供运行时白名单功能。
This commit is contained in:
@@ -123,6 +123,8 @@ FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=redis
|
||||
# 缓存存储:file / database / redis 等;与 Redis 赔付池等能力对接前可用 database
|
||||
CACHE_STORE=redis
|
||||
# 号码赔付池是否使用 Redis Lua 原子扣减;本地无 Redis Lua 需求时可 false
|
||||
LOTTERY_RISK_POOL_USE_REDIS_LUA=false
|
||||
# 缓存键全局前缀;多环境共 Redis 时可用于隔离,一般可留空使用框架默认
|
||||
# CACHE_PREFIX=
|
||||
|
||||
@@ -219,6 +221,7 @@ LOTTERY_JWT_REQUIRE_IAT=true
|
||||
# 管理端登录:Sanctum PAT 有效天数(签发时刻起),至少 1;到期需重新登录
|
||||
ADMIN_API_TOKEN_TTL_DAYS=7
|
||||
|
||||
# Legacy 主站兜底配置:正式接入站点以后台 admin_sites 为准;这里仅用于本地默认站/历史回退。
|
||||
# 主站站点根 URL(SSO、跳转等)
|
||||
MAIN_SITE_BASE_URL=
|
||||
# 主站 JWT 验签密钥(与主站约定,勿泄露)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Integration;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AdminSite;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 玩家端 iframe 运行时白名单。
|
||||
*
|
||||
* 只公开启用站点的 origin,不包含任何密钥或钱包地址。
|
||||
*/
|
||||
final class IntegrationRuntimeOriginsController extends Controller
|
||||
{
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
$origins = AdminSite::query()
|
||||
->where('status', 1)
|
||||
->pluck('iframe_allowed_origins')
|
||||
->flatMap(static function (mixed $value): array {
|
||||
if (is_string($value)) {
|
||||
$decoded = json_decode($value, true);
|
||||
$value = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
array_map(
|
||||
static fn (mixed $origin): string => is_string($origin) ? trim($origin) : '',
|
||||
$value,
|
||||
),
|
||||
static fn (string $origin): bool => $origin !== '',
|
||||
));
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return ApiResponse::success([
|
||||
'iframe_allowed_origins' => $origins,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Rules\WalletApiUrlRule;
|
||||
|
||||
final class AdminIntegrationSiteStoreRequest extends FormRequest
|
||||
{
|
||||
@@ -20,7 +21,7 @@ final class AdminIntegrationSiteStoreRequest extends FormRequest
|
||||
'name' => ['required', 'string', 'max:128'],
|
||||
'currency_code' => ['sometimes', 'string', 'max:16'],
|
||||
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||
'wallet_api_url' => ['nullable', 'string', 'max:512'],
|
||||
'wallet_api_url' => ['nullable', 'string', 'max:512', new WalletApiUrlRule()],
|
||||
'wallet_debit_path' => ['sometimes', 'string', 'max:128'],
|
||||
'wallet_credit_path' => ['sometimes', 'string', 'max:128'],
|
||||
'wallet_balance_path' => ['sometimes', 'string', 'max:128'],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use App\Rules\WalletApiUrlRule;
|
||||
|
||||
final class AdminIntegrationSiteUpdateRequest extends FormRequest
|
||||
{
|
||||
@@ -18,7 +19,7 @@ final class AdminIntegrationSiteUpdateRequest extends FormRequest
|
||||
'name' => ['required', 'string', 'max:128'],
|
||||
'currency_code' => ['sometimes', 'string', 'max:16'],
|
||||
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||
'wallet_api_url' => ['nullable', 'string', 'max:512'],
|
||||
'wallet_api_url' => ['nullable', 'string', 'max:512', new WalletApiUrlRule()],
|
||||
'wallet_debit_path' => ['sometimes', 'string', 'max:128'],
|
||||
'wallet_credit_path' => ['sometimes', 'string', 'max:128'],
|
||||
'wallet_balance_path' => ['sometimes', 'string', 'max:128'],
|
||||
|
||||
20
app/Rules/WalletApiUrlRule.php
Normal file
20
app/Rules/WalletApiUrlRule.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use App\Support\Integration\WalletApiUrlSanitizer;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
final class WalletApiUrlRule implements Rule
|
||||
{
|
||||
public function passes($attribute, $value): bool
|
||||
{
|
||||
return WalletApiUrlSanitizer::normalizeAndValidate($value) !== null;
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
{
|
||||
return 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Services\Wallet;
|
||||
use App\Models\Player;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||||
use App\Support\Integration\WalletApiUrlSanitizer;
|
||||
|
||||
/**
|
||||
* 查询主站钱包余额(供玩家端余额接口填充 main_balance)。
|
||||
@@ -51,7 +52,19 @@ final class HttpMainSiteWalletBalanceClient
|
||||
);
|
||||
}
|
||||
|
||||
$base = rtrim((string) $config->walletApiUrl, '/');
|
||||
$base = WalletApiUrlSanitizer::normalizeAndValidate($config->walletApiUrl);
|
||||
if ($base === null) {
|
||||
return new MainSiteWalletBalanceProbeResult(
|
||||
success: false,
|
||||
mainBalanceMinor: null,
|
||||
currencyCode: $currencyCode,
|
||||
requestUrl: '',
|
||||
httpStatus: null,
|
||||
message: 'wallet_api_url 无效(拒绝以防 SSRF)',
|
||||
responseBody: null,
|
||||
);
|
||||
}
|
||||
|
||||
$path = $config->walletBalancePath;
|
||||
$url = $base.'/'.ltrim($path, '/');
|
||||
$timeout = $config->walletTimeoutSeconds;
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Player;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||||
use App\Support\Integration\WalletApiUrlSanitizer;
|
||||
|
||||
/**
|
||||
* 通过 HTTP 调用主站钱包 API(路径见 config lottery.main_site.wallet_*_path)。
|
||||
@@ -61,7 +62,7 @@ final class HttpMainSiteWalletGateway implements MainSiteWalletGateway
|
||||
\App\Services\Integration\PartnerSiteConfig $config,
|
||||
): MainSiteWalletResult {
|
||||
if (! $config->hasWalletApi()) {
|
||||
return MainSiteWalletResult::success(null, ['stub' => true, 'reason' => 'wallet_api_not_configured'], [
|
||||
$requestSnapshot = [
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'player_id' => $player->id,
|
||||
@@ -69,10 +70,44 @@ final class HttpMainSiteWalletGateway implements MainSiteWalletGateway
|
||||
'amount_minor' => $amountMinor,
|
||||
'idempotent_key' => $idempotentKey,
|
||||
'_meta' => ['stub' => true],
|
||||
]);
|
||||
];
|
||||
|
||||
// 生产环境不允许把“主站未配置钱包 API”当作成功,从而避免资金链路失真。
|
||||
if (app()->environment(['production'])) {
|
||||
return MainSiteWalletResult::failure(
|
||||
'wallet_api_not_configured',
|
||||
['stub' => false, 'reason' => 'wallet_api_not_configured'],
|
||||
false,
|
||||
$requestSnapshot,
|
||||
);
|
||||
}
|
||||
|
||||
// 本地/测试环境允许 stub 成功,方便联调与自动化用例覆盖资金链路流程。
|
||||
return MainSiteWalletResult::success(
|
||||
null,
|
||||
['stub' => true, 'reason' => 'wallet_api_not_configured'],
|
||||
$requestSnapshot,
|
||||
);
|
||||
}
|
||||
|
||||
$base = WalletApiUrlSanitizer::normalizeAndValidate($config->walletApiUrl);
|
||||
if ($base === null) {
|
||||
return MainSiteWalletResult::failure(
|
||||
'wallet_api_url_invalid',
|
||||
['reason' => 'invalid_base_url'],
|
||||
false,
|
||||
[
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'player_id' => $player->id,
|
||||
'currency_code' => $currencyCode,
|
||||
'amount_minor' => $amountMinor,
|
||||
'idempotent_key' => $idempotentKey,
|
||||
'_meta' => ['stub' => true, 'operation' => 'invalid_wallet_api_url'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
$base = rtrim((string) $config->walletApiUrl, '/');
|
||||
$url = $base.'/'.ltrim($path, '/');
|
||||
$timeout = $config->walletTimeoutSeconds;
|
||||
$apiKey = $config->walletApiKey;
|
||||
|
||||
195
app/Support/Integration/WalletApiUrlSanitizer.php
Normal file
195
app/Support/Integration/WalletApiUrlSanitizer.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Integration;
|
||||
|
||||
/**
|
||||
* 防 SSRF:对主站钱包基地址做严格归一化校验。
|
||||
*
|
||||
* 规则(保守):
|
||||
* - 仅允许 https
|
||||
* - 不允许 user/pass、query、fragment
|
||||
* - 不允许除 / 以外的 path(即仅允许根地址)
|
||||
* - 拒绝 localhost 与私网/保留网段(IP 字面量层面)
|
||||
*
|
||||
* 说明:对 hostname 不做 DNS 解析(避免引入不确定性),但会拦截 localhost 及明显内网标识。
|
||||
*/
|
||||
final class WalletApiUrlSanitizer
|
||||
{
|
||||
public static function normalizeAndValidate(?string $raw): ?string
|
||||
{
|
||||
if (! is_string($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 允许尾部 /,归一化后移除
|
||||
$raw = rtrim($raw, " \t\n\r\0\x0B/");
|
||||
|
||||
$parts = parse_url($raw);
|
||||
if (! is_array($parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
|
||||
if ($scheme !== 'https') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($parts['user']) || isset($parts['pass'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! isset($parts['host']) || ! is_string($parts['host']) || $parts['host'] === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($parts['query']) || isset($parts['fragment'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = (string) ($parts['path'] ?? '');
|
||||
if ($path !== '' && $path !== '/') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = strtolower(trim((string) $parts['host']));
|
||||
if ($host === '' || str_contains($host, ' ') || str_contains($host, "\t") || str_contains($host, "\n")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 明确拦截 localhost / 本地常见名
|
||||
if ($host === 'localhost' || $host === 'local' || $host === 'localdomain') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 拦截 IP 字面量私网
|
||||
$isIp = filter_var($host, FILTER_VALIDATE_IP) !== false;
|
||||
if ($isIp) {
|
||||
if (self::ipIsPrivateOrReserved($host)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 端口校验(允许不写 port)
|
||||
if (isset($parts['port'])) {
|
||||
$port = (int) $parts['port'];
|
||||
if ($port < 1 || $port > 65535) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = 'https://'.$host;
|
||||
if (isset($parts['port'])) {
|
||||
$normalized .= ':'.(string) (int) $parts['port'];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private static function ipIsPrivateOrReserved(string $ip): bool
|
||||
{
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$v = ip2long($ip);
|
||||
if ($v === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// PHP 在 macOS 上 ip2long 可能为有符号,强转为 int64 统一处理
|
||||
$v = (int) $v;
|
||||
|
||||
return self::ipInRangesV4((int) $v, [
|
||||
// 0.0.0.0/8
|
||||
['base' => ip2long('0.0.0.0'), 'mask' => 0xFF000000],
|
||||
// 10.0.0.0/8
|
||||
['base' => ip2long('10.0.0.0'), 'mask' => 0xFF000000],
|
||||
// 127.0.0.0/8
|
||||
['base' => ip2long('127.0.0.0'), 'mask' => 0xFF000000],
|
||||
// 169.254.0.0/16
|
||||
['base' => ip2long('169.254.0.0'), 'mask' => 0xFFFF0000],
|
||||
// 172.16.0.0/12
|
||||
['base' => ip2long('172.16.0.0'), 'mask' => 0xFFF00000],
|
||||
// 192.168.0.0/16
|
||||
['base' => ip2long('192.168.0.0'), 'mask' => 0xFFFF0000],
|
||||
// 100.64.0.0/10 (CGNAT)
|
||||
['base' => ip2long('100.64.0.0'), 'mask' => 0xFFC00000],
|
||||
// 192.0.0.0/24 (IETF Protocol Assignments)
|
||||
['base' => ip2long('192.0.0.0'), 'mask' => 0xFFFFFF00],
|
||||
// 198.18.0.0/15 (benchmarking)
|
||||
['base' => ip2long('198.18.0.0'), 'mask' => 0xFFFE0000],
|
||||
// 224.0.0.0/4 (multicast)
|
||||
['base' => ip2long('224.0.0.0'), 'mask' => 0xF0000000],
|
||||
// 240.0.0.0/4 (reserved for future use)
|
||||
['base' => ip2long('240.0.0.0'), 'mask' => 0xF0000000],
|
||||
]);
|
||||
}
|
||||
|
||||
// IPv6:仅做关键保守段拦截(避免复杂数值比较引入 bug)
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$bin = inet_pton($ip);
|
||||
if ($bin === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv6 ::1 (loopback)
|
||||
if (substr($bin, 0, 15) === str_repeat("\0", 15) && $bin[15] === "\1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv6 unspecified ::
|
||||
if ($bin === str_repeat("\0", 16)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$b0 = ord($bin[0]);
|
||||
$b1 = ord($bin[1]);
|
||||
|
||||
// ff00::/8 multicast
|
||||
if ($b0 === 0xFF) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fc00::/7 unique local => fc or fd
|
||||
if ($b0 === 0xFC || $b0 === 0xFD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fe80::/10 link-local => fe + (second byte & 0xC0) == 0x80
|
||||
if ($b0 === 0xFE && (($b1 & 0xC0) === 0x80)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4-mapped ::ffff:0:0/96 => 检查最后 4 字节映射的 IPv4 是否为私网
|
||||
if (substr($bin, 0, 10) === str_repeat("\0", 10) && substr($bin, 10, 2) === "\xFF\xFF") {
|
||||
$v4bin = substr($bin, 12, 4);
|
||||
$v4 = inet_ntop($v4bin);
|
||||
// inet_ntop 对 v4bin 有时返回 false,这里保守返回 true
|
||||
if ($v4 === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return self::ipIsPrivateOrReserved($v4);
|
||||
}
|
||||
}
|
||||
|
||||
// 非法 IP:保守拒绝
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function ipInRangesV4(int $v, array $ranges): bool
|
||||
{
|
||||
foreach ($ranges as $r) {
|
||||
$base = (int) $r['base'];
|
||||
$mask = (int) $r['mask'];
|
||||
if (($v & $mask) === ($base & $mask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "llotterLaravel",
|
||||
"name": "lotterLaravel",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\Api\V1\Jackpot\JackpotSummaryController;
|
||||
use App\Http\Controllers\Api\V1\Play\PlayEffectiveCatalogController;
|
||||
use App\Http\Controllers\Api\V1\Player\PingController as PlayerPingController;
|
||||
use App\Http\Controllers\Api\V1\Setting\SettingIndexController;
|
||||
use App\Http\Controllers\Api\V1\Integration\IntegrationRuntimeOriginsController;
|
||||
|
||||
/**
|
||||
* 公开路由(无需登录)。
|
||||
@@ -43,3 +44,7 @@ Route::prefix('player')
|
||||
|
||||
// 系统公共配置(如前端规则等)
|
||||
Route::get('settings', SettingIndexController::class)->name('api.v1.settings.index');
|
||||
|
||||
// iframe 运行时白名单(只公开启用接入站点的 origin,不公开密钥)
|
||||
Route::get('integration/runtime-origins', IntegrationRuntimeOriginsController::class)
|
||||
->name('api.v1.integration.runtime-origins');
|
||||
|
||||
@@ -323,3 +323,42 @@ test('player list is filtered by admin site binding', function (): void {
|
||||
$siteCodes = collect($response->json('data.items'))->pluck('site_code')->unique()->values()->all();
|
||||
expect($siteCodes)->toBe(['site-a']);
|
||||
});
|
||||
|
||||
test('wallet_api_url rejects non-https', function (): void {
|
||||
$token = integrationAdminToken();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'bad-https-1',
|
||||
'name' => 'Bad HTTPS 1',
|
||||
'wallet_api_url' => 'http://wallet.bad.test',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。');
|
||||
});
|
||||
|
||||
test('wallet_api_url rejects localhost', function (): void {
|
||||
$token = integrationAdminToken();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'bad-https-2',
|
||||
'name' => 'Bad HTTPS 2',
|
||||
'wallet_api_url' => 'https://localhost:8080',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。');
|
||||
});
|
||||
|
||||
test('wallet_api_url rejects private ip with path', function (): void {
|
||||
$token = integrationAdminToken();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'bad-https-3',
|
||||
'name' => 'Bad HTTPS 3',
|
||||
'wallet_api_url' => 'https://127.0.0.1/wallet',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。');
|
||||
});
|
||||
|
||||
38
tests/Feature/PublicIntegrationRuntimeOriginsTest.php
Normal file
38
tests/Feature/PublicIntegrationRuntimeOriginsTest.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('public runtime origins returns enabled integration site iframe origins only', function (): void {
|
||||
AdminSite::query()->create([
|
||||
'code' => 'enabled-a',
|
||||
'name' => 'Enabled A',
|
||||
'currency_code' => 'NPR',
|
||||
'status' => 1,
|
||||
'iframe_allowed_origins' => [
|
||||
'https://main.example.test',
|
||||
'https://main.example.test',
|
||||
' https://shell.example.test ',
|
||||
],
|
||||
]);
|
||||
|
||||
AdminSite::query()->create([
|
||||
'code' => 'disabled-a',
|
||||
'name' => 'Disabled A',
|
||||
'currency_code' => 'NPR',
|
||||
'status' => 0,
|
||||
'iframe_allowed_origins' => [
|
||||
'https://disabled.example.test',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/integration/runtime-origins')
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.iframe_allowed_origins', [
|
||||
'https://main.example.test',
|
||||
'https://shell.example.test',
|
||||
]);
|
||||
});
|
||||
@@ -59,13 +59,13 @@ test('wallet balance rejects illegal currency query', function () {
|
||||
|
||||
test('wallet balance returns main_balance when main site wallet api is configured', function () {
|
||||
config([
|
||||
'lottery.main_site.wallet_api_url' => 'http://fake-main.test',
|
||||
'lottery.main_site.wallet_api_url' => 'https://fake-main.test',
|
||||
'lottery.main_site.wallet_balance_path' => '/wallet/balance',
|
||||
]);
|
||||
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'http://fake-main.test/wallet/balance*' => Http::response([
|
||||
'https://fake-main.test/wallet/balance*' => Http::response([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'main_balance' => 499_900,
|
||||
|
||||
@@ -111,7 +111,7 @@ test('transfer in main site explicit failure returns 1009 and marks order failed
|
||||
Http::fake([
|
||||
'reject-debit.test/*' => Http::response(['success' => false, 'message' => 'main_insufficient'], 200),
|
||||
]);
|
||||
config(['lottery.main_site.wallet_api_url' => 'http://reject-debit.test']);
|
||||
config(['lottery.main_site.wallet_api_url' => 'https://reject-debit.test']);
|
||||
config(['lottery.main_site.wallet_debit_path' => 'debit']);
|
||||
|
||||
$player = Player::query()->create([
|
||||
@@ -143,7 +143,7 @@ test('transfer in main site timeout returns 1002 and pending_reconcile', functio
|
||||
Http::fake([
|
||||
'timeout-debit.test/*' => Http::response([], 504),
|
||||
]);
|
||||
config(['lottery.main_site.wallet_api_url' => 'http://timeout-debit.test']);
|
||||
config(['lottery.main_site.wallet_api_url' => 'https://timeout-debit.test']);
|
||||
config(['lottery.main_site.wallet_debit_path' => 'debit']);
|
||||
|
||||
$player = Player::query()->create([
|
||||
@@ -210,7 +210,7 @@ test('transfer out main site failure refunds lottery and returns 1009', function
|
||||
Http::fake([
|
||||
'reject-credit.test/*' => Http::response(['success' => false, 'message' => 'credit_denied'], 200),
|
||||
]);
|
||||
config(['lottery.main_site.wallet_api_url' => 'http://reject-credit.test']);
|
||||
config(['lottery.main_site.wallet_api_url' => 'https://reject-credit.test']);
|
||||
config(['lottery.main_site.wallet_credit_path' => 'credit']);
|
||||
|
||||
$player = Player::query()->create([
|
||||
@@ -251,7 +251,7 @@ test('transfer out main site timeout returns 1002 and pending_reconcile on order
|
||||
Http::fake([
|
||||
'timeout-credit.test/*' => Http::response([], 504),
|
||||
]);
|
||||
config(['lottery.main_site.wallet_api_url' => 'http://timeout-credit.test']);
|
||||
config(['lottery.main_site.wallet_api_url' => 'https://timeout-credit.test']);
|
||||
config(['lottery.main_site.wallet_credit_path' => 'credit']);
|
||||
|
||||
$player = Player::query()->create([
|
||||
|
||||
@@ -256,7 +256,7 @@ test('transfer in http 504 marks order pending reconcile', function () {
|
||||
Http::fake([
|
||||
'fake-main.test/*' => Http::response([], 504),
|
||||
]);
|
||||
config(['lottery.main_site.wallet_api_url' => 'http://fake-main.test']);
|
||||
config(['lottery.main_site.wallet_api_url' => 'https://fake-main.test']);
|
||||
config(['lottery.main_site.wallet_debit_path' => 'debit']);
|
||||
|
||||
$player = Player::query()->create([
|
||||
|
||||
Reference in New Issue
Block a user