doge 1 день назад
Родитель
Сommit
d4bb620a35
1 измененных файлов с 153 добавлено и 0 удалено
  1. 153 0
      tests/Regression/jisu_lottery_diagnostics.php

+ 153 - 0
tests/Regression/jisu_lottery_diagnostics.php

@@ -0,0 +1,153 @@
+<?php
+
+namespace app\common\service {
+    function env($key, $default = null) { return $GLOBALS['jisu_diagnostic_env'][$key] ?? $default; }
+    function httpRequest($url, $params, $headers, $proxy) {
+        $GLOBALS['jisu_diagnostic_http_calls'][] = [$url, $proxy];
+        return ['errorCode' => 0, 'result' => ['data' => [
+            'drawIssue' => '12345', 'drawTime' => '2026-10-25 12:00:00', 'preDrawCode' => '1,2,3', 'preDrawIssue' => '12344',
+        ]]];
+    }
+    final class WsClientService {
+        public function connect() { ++$GLOBALS['jisu_diagnostic_ws_calls']; return true; }
+        public function close() { ++$GLOBALS['jisu_diagnostic_ws_closes']; }
+    }
+}
+
+namespace {
+    error_reporting(E_ALL & ~E_DEPRECATED);
+    $repo = getenv('JISU_SITE_REPO') ?: dirname(__DIR__, 3) . '/melbet_sport-api';
+    $stage = getenv('JISU_SITE_STAGE') ?: $repo;
+    $loader = require $repo . '/vendor/autoload.php';
+    $loader->setPsr4('app\\', [$stage . '/app/', $repo . '/app/']);
+    $app = new \think\App($repo . '/');
+    \think\Container::setInstance($app);
+    $db = new \think\DbManager();
+    $db->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [
+        'type' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'fields_strict' => true, 'debug' => false,
+    ]], 'auto_timestamp' => false]);
+    $app->instance(\think\DbManager::class, $db);
+    $app->instance('log', new \Psr\Log\NullLogger());
+    $GLOBALS['jisu_diagnostic_env'] = ['app.http_proxy' => 'http://user:proxy-secret@proxy.test:8080'];
+    $GLOBALS['jisu_diagnostic_http_calls'] = [];
+    $GLOBALS['jisu_diagnostic_ws_calls'] = $GLOBALS['jisu_diagnostic_ws_closes'] = 0;
+    $db->execute('CREATE TABLE bot_jisu_lottery (id INTEGER PRIMARY KEY,type INTEGER,issue INTEGER,status INTEGER,open_time INTEGER,next_open_time INTEGER,updated_at TEXT)');
+    foreach (['bot_wallets', 'bot_lhc_order', 'bot_balance_logs'] as $table) {
+        $db->execute('CREATE TABLE ' . $table . ' (id INTEGER PRIMARY KEY,amount TEXT)');
+        \think\facade\Db::table($table)->insert(['id' => 1, 'amount' => '123.45']);
+    }
+    $now = time();
+    $old = $now - 60 * 86400;
+    \think\facade\Db::table('bot_jisu_lottery')->insertAll([
+        ['id' => 1, 'type' => 1, 'issue' => 34040000, 'status' => 3, 'open_time' => $old, 'next_open_time' => $old + 75, 'updated_at' => '2026-08-24 18:36:00'],
+        ['id' => 2, 'type' => 1, 'issue' => 34038100, 'status' => 1, 'open_time' => $now - 60, 'next_open_time' => $now + 15, 'updated_at' => '2026-10-25 12:00:00'],
+        ['id' => 3, 'type' => 2, 'issue' => 34040000, 'status' => 3, 'open_time' => $old, 'next_open_time' => $old + 75, 'updated_at' => '2026-08-24 18:36:00'],
+        ['id' => 4, 'type' => 4, 'issue' => 12345, 'status' => 1, 'open_time' => $now + 3600, 'next_open_time' => null, 'updated_at' => '2026-10-25 12:00:00'],
+    ]);
+    $tables = ['bot_jisu_lottery', 'bot_wallets', 'bot_lhc_order', 'bot_balance_logs'];
+    foreach ($tables as $table) {
+        foreach (['INSERT', 'UPDATE', 'DELETE'] as $action) {
+            $db->execute('CREATE TRIGGER forbid_' . $table . '_' . $action . ' BEFORE ' . $action . ' ON ' . $table . " BEGIN SELECT RAISE(ABORT,'diagnosis attempted a write'); END");
+        }
+    }
+    function snapshot(): array {
+        global $tables;
+        $result = [];
+        foreach ($tables as $table) $result[$table] = \think\facade\Db::table($table)->order('id')->select()->toArray();
+        return $result;
+    }
+    $checks = 0;
+    function check($actual, $expected, string $message): void {
+        global $checks; ++$checks;
+        if ($actual !== $expected) throw new \RuntimeException($message . ': actual=' . json_encode($actual) . ' expected=' . json_encode($expected));
+    }
+    function contains(string $text, string $part): void { check(str_contains($text, $part), true, 'Missing output: ' . $part); }
+    $before = snapshot();
+    $output = new \think\console\Output('buffer');
+    $service = new \app\common\service\JisuLotteryDiagnostics();
+    check($service->run($output), 0, 'Database-only diagnostics complete');
+    $text = $output->fetch();
+    contains($text, 'pending=1 overdue=1');
+    contains($text, 'api_last_drawn: id=1 issue=34040000 status=3');
+    contains($text, 'last_inserted: id=2 issue=34038100 status=1');
+    contains($text, '最新插入期号小于最大期号');
+    contains($text, '没有未开奖期次');
+    contains($text, '固定期号 34038100');
+    contains($text, 'next_open_time 是原始时间戳');
+    contains($text, '此类型没有任何期次数据');
+    contains($text, '检查结束');
+    check($GLOBALS['jisu_diagnostic_http_calls'], [], 'Database diagnostics never contact upstream');
+    check($GLOBALS['jisu_diagnostic_ws_calls'], 0, 'Database diagnostics never connect WebSocket');
+    check(snapshot(), $before, 'Database-only diagnostics preserve all rows');
+
+    $output = new \think\console\Output('buffer');
+    check($service->run($output, true), 0, 'Default network probes use read-only adapters');
+    $text = $output->fetch();
+    foreach ([4, 5, 7] as $type) contains($text, '[SG ' . $type . ' OK]');
+    contains($text, '[WS OK]');
+    check(count($GLOBALS['jisu_diagnostic_http_calls']), 3, 'Every SG feed is probed once');
+    check($GLOBALS['jisu_diagnostic_ws_calls'], 1, 'WebSocket handshake tested once');
+    check($GLOBALS['jisu_diagnostic_ws_closes'], 1, 'WebSocket client always closed');
+    check(str_contains($text, 'proxy-secret'), false, 'Proxy credentials are not printed');
+    check(snapshot(), $before, 'Network probes do not insert or settle any data');
+
+    $calls = 0; $wsCalls = 0;
+    $broken = new \app\common\service\JisuLotteryDiagnostics(
+        static function () use (&$calls) {
+            ++$calls;
+            if ($calls === 1) return null;
+            if ($calls === 2) return ['code' => 500, 'msg' => 'request failed http://user:proxy-secret@proxy.test:8080'];
+            return ['errorCode' => 0, 'result' => ['data' => ['drawIssue' => 12345]]];
+        },
+        static function () use (&$wsCalls) { ++$wsCalls; throw new \RuntimeException('certificate verify failed ssl://user:ws-secret@host.test/?token=secret-token'); }
+    );
+    $output = new \think\console\Output('buffer');
+    check($broken->run($output, true), 1, 'Network failures produce a nonzero diagnostic result');
+    $text = $output->fetch();
+    foreach ([4, 5, 7] as $type) contains($text, '[SG ' . $type . ' FAIL]');
+    contains($text, '返回空内容或非 JSON');
+    contains($text, 'code=500');
+    contains($text, '缺少字段:drawTime,preDrawCode,preDrawIssue');
+    contains($text, 'certificate verify failed');
+    check($calls, 3, 'Upstream failure does not skip other feed probes');
+    check($wsCalls, 1, 'Upstream failure does not skip WebSocket diagnostics');
+    foreach (['proxy-secret', 'ws-secret', 'secret-token'] as $secret) check(str_contains($text, $secret), false, 'Secret is redacted');
+    check(snapshot(), $before, 'Failures never change lottery, orders, wallet or ledger');
+
+    $malformed = new \app\common\service\JisuLotteryDiagnostics(
+        static fn() => ['errorCode' => 0, 'result' => ['data' => [
+            'drawIssue' => [], 'drawTime' => null, 'preDrawCode' => '1,2,3', 'preDrawIssue' => '12344',
+        ]]],
+        static fn() => false
+    );
+    $output = new \think\console\Output('buffer');
+    check($malformed->run($output, true), 1, 'Malformed upstream fields and false WebSocket result are diagnosed');
+    $text = $output->fetch();
+    contains($text, '字段内容异常:drawIssue,drawTime');
+    contains($text, '[WS FAIL]');
+    check(snapshot(), $before, 'Malformed responses do not change data');
+
+    final class GuardedJisuCommand extends \app\api\command\JisuLottery {
+        public function init() { throw new \LogicException('Read-only mode entered mutation initialization'); }
+        public function sgKaiJiang($type) { throw new \LogicException('Read-only mode entered draw/settlement'); }
+    }
+    foreach ([['--diagnose'], ['--diagnose', '--probe-network']] as $args) {
+        $command = new GuardedJisuCommand();
+        $input = new \think\console\Input($args); $input->setInteractive(false);
+        $output = new \think\console\Output('buffer');
+        check($command->run($input, $output), 0, 'Real CLI option routing bypasses normal execution');
+        contains($output->fetch(), '[DIAG] 只读诊断');
+        check(snapshot(), $before, 'Real command diagnostic branch is read-only');
+    }
+    $command = new GuardedJisuCommand();
+    $input = new \think\console\Input(['--probe-network']); $input->setInteractive(false);
+    $output = new \think\console\Output('buffer');
+    check($command->run($input, $output), 1, 'Network flag alone cannot start the mutating worker');
+    contains($output->fetch(), '请同时传入 --diagnose');
+
+    $db->execute('DROP TABLE bot_jisu_lottery');
+    $output = new \think\console\Output('buffer');
+    check($service->run($output), 1, 'Database errors are reported as failures');
+    contains($output->fetch(), '[DB FAIL]');
+    echo 'PASS ' . $checks . " jisu diagnostics assertions (isolated SQLite, fake HTTP/WS, no live tasks)\n";
+}