safeLoad(); $_ENV['APP_DEBUG'] = 'true'; $_ENV['WECHAT_H5_AUTH_MOCK'] = 'true'; use app\support\AppAuthService; use support\think\Db; use Webman\Http\Request; Db::setConfig(require dirname(__DIR__) . '/config/think-orm.php'); function assertTrue(bool $condition, string $message): void { if (!$condition) { throw new RuntimeException($message); } } function makeRequest(): Request { return new class("GET /api/app/auth/wechat/mock-test HTTP/1.1\r\nHost: 127.0.0.1\r\nUser-Agent: wechat-h5-auth-mock-test\r\n\r\n") extends Request { public function getRealIp(bool $safeMode = true): string { return '127.0.0.1'; } }; } function ensureConfig(string $group, string $key, string $value): void { $now = date('Y-m-d H:i:s'); $exists = Db::name('system_configs') ->where('config_group', $group) ->where('config_key', $key) ->find(); $payload = [ 'config_group' => $group, 'config_key' => $key, 'config_value' => $value, 'remark' => '微信 H5 授权测试配置', 'updated_at' => $now, ]; if ($exists) { Db::name('system_configs')->where('id', $exists['id'])->update($payload); return; } $payload['created_at'] = $now; Db::name('system_configs')->insert($payload); } function latestDebugCode(string $mobile): string { $record = Db::name('sms_code_logs') ->where('mobile', $mobile) ->where('scene', 'login') ->where('send_status', 'mock') ->whereNull('used_at') ->order('id', 'desc') ->find(); assertTrue((bool)$record, 'mock sms code record missing'); for ($code = 100000; $code <= 999999; $code++) { $codeText = (string)$code; if (hash_equals((string)$record['code_hash'], hash('sha256', implode('|', [$mobile, 'login', $codeText])))) { return $codeText; } } throw new RuntimeException('mock sms code not found'); } function cleanupWechatMockData(): void { $userIds = Db::name('users') ->whereLike('mobile', '1399900%') ->column('id'); if ($userIds) { Db::name('user_api_tokens')->whereIn('user_id', $userIds)->delete(); Db::name('user_auths')->whereIn('user_id', $userIds)->delete(); Db::name('users')->whereIn('id', $userIds)->delete(); } Db::name('sms_code_logs')->whereLike('mobile', '1399900%')->delete(); Db::name('user_auths')->where('auth_type', 'wechat_h5')->whereLike('auth_key', 'mock_openid_%')->delete(); } $service = new AppAuthService(); $request = makeRequest(); $originalConfigs = [ 'h5.app_id' => Db::name('system_configs')->where('config_group', 'h5')->where('config_key', 'app_id')->value('config_value'), 'h5.app_secret' => Db::name('system_configs')->where('config_group', 'h5')->where('config_key', 'app_secret')->value('config_value'), 'h5.page_base_url' => Db::name('system_configs')->where('config_group', 'h5')->where('config_key', 'page_base_url')->value('config_value'), ]; Db::startTrans(); try { cleanupWechatMockData(); ensureConfig('h5', 'app_id', 'wx_mock_appid'); ensureConfig('h5', 'app_secret', 'mock_secret'); ensureConfig('h5', 'page_base_url', 'https://m.example.com'); ensureConfig('sms', 'access_key_id', ''); ensureConfig('sms', 'access_key_secret', ''); ensureConfig('sms', 'sign_name', ''); ensureConfig('sms', 'login_template_code', ''); $config = $service->wechatConfig(); assertTrue($config['enabled'] === true, 'wechat config should be enabled'); assertTrue($config['oauth_redirect_url'] === 'https://m.example.com/#/pages/auth/login', 'oauth redirect url mismatch'); $stateOne = (string)$config['state']; $exchange = $service->exchangeWechatCode('mock_newuser', $stateOne, $request); assertTrue(($exchange['status'] ?? '') === 'need_bind', 'new wechat user should need binding'); assertTrue(!empty($exchange['bind_ticket']), 'bind ticket missing'); $mobile = '13999000001'; $service->sendLoginCode($mobile, $request); $bind = $service->bindWechatMobile((string)$exchange['bind_ticket'], $mobile, latestDebugCode($mobile), $request); assertTrue(($bind['status'] ?? '') === 'logged_in' && !empty($bind['token']), 'bind should return token'); $stateTwo = (string)$service->wechatConfig()['state']; $linked = $service->exchangeWechatCode('mock_newuser', $stateTwo, $request); assertTrue(($linked['status'] ?? '') === 'logged_in' && !empty($linked['token']), 'linked wechat user should login'); $stateThree = (string)$service->wechatConfig()['state']; $existingUser = $service->exchangeWechatCode('mock_existingmobile', $stateThree, $request); $existingMobile = '13999000002'; Db::name('users')->insert([ 'nickname' => '已有手机号用户', 'avatar' => '', 'mobile' => $existingMobile, 'password' => '', 'status' => 'enabled', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); $service->sendLoginCode($existingMobile, $request); $boundExisting = $service->bindWechatMobile((string)$existingUser['bind_ticket'], $existingMobile, latestDebugCode($existingMobile), $request); assertTrue(($boundExisting['status'] ?? '') === 'logged_in', 'existing mobile should bind'); assertTrue((int)Db::name('users')->where('mobile', $existingMobile)->count() === 1, 'existing mobile should not duplicate user'); $otherUserId = (int)Db::name('users')->insertGetId([ 'nickname' => '占用微信用户', 'avatar' => '', 'mobile' => '13999000003', 'password' => '', 'status' => 'enabled', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); Db::name('user_auths')->insert([ 'user_id' => $otherUserId, 'auth_type' => 'wechat_h5', 'auth_key' => 'mock_openid_conflict', 'auth_open_id' => 'mock_openid_conflict', 'auth_union_id' => 'mock_unionid_conflict', 'auth_extra' => json_encode(['mock' => true]), 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); $conflictUser = $service->exchangeWechatCode('mock_conflict', (string)$service->wechatConfig()['state'], $request); assertTrue(($conflictUser['status'] ?? '') === 'logged_in', 'existing conflicting openid should login owner directly'); try { $service->exchangeWechatCode('mock_expired', (string)$service->wechatConfig()['state'], $request); throw new RuntimeException('expired code should fail'); } catch (RuntimeException $e) { assertTrue(strpos($e->getMessage(), 'code 无效') !== false, 'expired code message mismatch'); } try { $service->bindWechatMobile('invalid.ticket', '13999000004', '123456', $request); throw new RuntimeException('invalid bind ticket should fail'); } catch (RuntimeException $e) { assertTrue(strpos($e->getMessage(), '绑定凭证') !== false, 'invalid bind ticket message mismatch'); } try { $service->exchangeWechatCode('mock_statefail', 'wrongstate', $request); throw new RuntimeException('invalid state should fail'); } catch (RuntimeException $e) { assertTrue(strpos($e->getMessage(), '状态') !== false, 'invalid state message mismatch'); } echo "WECHAT_H5_AUTH_MOCK_TEST_OK\n"; Db::rollback(); } catch (Throwable $e) { Db::rollback(); fwrite(STDERR, "WECHAT_H5_AUTH_MOCK_TEST_FAIL: " . $e->getMessage() . "\n"); exit(1); } finally { foreach ($originalConfigs as $mapKey => $value) { [$group, $key] = explode('.', $mapKey, 2); ensureConfig($group, $key, (string)$value); } }