600 lines
18 KiB
Markdown
600 lines
18 KiB
Markdown
# iOS 自动打包服务
|
||
|
||
基于 FastAPI + Vue 3 的 iOS 应用自动打包服务,支持 Ad_Hoc 和 App_Store 两种打包类型,提供 Web 界面操作、实时日志流、构建历史管理、多用户权限和健康监测。
|
||
|
||
## 目录
|
||
|
||
- [环境要求](#环境要求)
|
||
- [快速开始](#快速开始)
|
||
- [配置说明](#配置说明)
|
||
- [公网部署](#公网部署)
|
||
- [服务管理](#服务管理)
|
||
- [多用户管理](#多用户管理)
|
||
- [Watchdog 健康监测](#watchdog-健康监测)
|
||
- [macOS 开机自启](#macos-开机自启)
|
||
- [开发模式](#开发模式)
|
||
- [自动化测试](#自动化测试)
|
||
- [项目结构](#项目结构)
|
||
- [常见问题](#常见问题)
|
||
|
||
---
|
||
|
||
## 环境要求
|
||
|
||
| 依赖 | 版本要求 | 说明 |
|
||
|------|----------|------|
|
||
| macOS | 11+ | 必须,xcodebuild 依赖 |
|
||
| Xcode | 14+ | 必须,含 Command Line Tools |
|
||
| Python | 3.9+ | 后端运行 |
|
||
| Node.js | 18+ | 前端构建 |
|
||
| CocoaPods | - | 项目已有 Podfile 时需要 |
|
||
|
||
## 快速开始
|
||
|
||
### 1. 克隆项目
|
||
|
||
```bash
|
||
git clone <repo-url> BuildServer
|
||
cd BuildServer
|
||
```
|
||
|
||
### 2. 配置环境变量
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
vim .env
|
||
```
|
||
|
||
**建议优先检查的配置项:**
|
||
|
||
```bash
|
||
# 分支源码根目录,默认在当前项目下
|
||
GIT_SOURCE_BASE=/path/to/iOSBuildServer/ReadoorBranches
|
||
|
||
# 打包输出目录,默认在当前项目下
|
||
BUILD_BASE_DIR=/path/to/iOSBuildServer/build
|
||
|
||
# Git 远程仓库地址(配置后可自动拉取分支)
|
||
GIT_REMOTE_URL=git@github.com:org/repo.git
|
||
|
||
# 管理员密码(建议修改)
|
||
ADMIN_PASSWORD=your_secure_password
|
||
```
|
||
|
||
### 3. 构建与启动
|
||
|
||
```bash
|
||
# 一键构建(安装依赖 + 构建前端)
|
||
./deploy.sh build
|
||
|
||
# 启动服务
|
||
./deploy.sh start
|
||
```
|
||
|
||
启动后访问 `http://<服务器IP>:8000` 即可使用。
|
||
|
||
### 4.(可选)初始化源码目录
|
||
|
||
配置 `GIT_REMOTE_URL` 后,系统默认使用唯一的共享源码目录 `GIT_SOURCE_DIR`。任务会串行执行拉取、切分支、清理和复制,复制完成后在各自的打包目录中并行构建。
|
||
|
||
未配置远程仓库时,需要继续手动准备每个分支目录:
|
||
|
||
```bash
|
||
# 创建分支源码根目录
|
||
mkdir -p /path/to/ReadoorBranches
|
||
|
||
# 手动 clone 主分支(后续可继续手动维护)
|
||
git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
|
||
```
|
||
|
||
配置远程仓库时,无需手动 clone;首次打包会初始化共享目录。
|
||
|
||
### 5.(可选)启用健康监测
|
||
|
||
```bash
|
||
./deploy.sh watchdog
|
||
```
|
||
|
||
### 6.(可选)设为开机自启
|
||
|
||
```bash
|
||
./deploy/install-service.sh install
|
||
```
|
||
|
||
---
|
||
|
||
## 配置说明
|
||
|
||
所有配置通过项目根目录的 `.env` 文件管理,修改后重启服务生效。
|
||
|
||
### 目录配置
|
||
|
||
| 变量 | 示例 | 说明 |
|
||
|------|------|------|
|
||
| `GIT_SOURCE_BASE` | `/Users/shen/Work/Code/iOSBuildServer/ReadoorBranches` | 人工维护模式下的分支源码根目录 |
|
||
| `GIT_SOURCE_DIR` | `/Users/shen/Work/Code/iOSBuildServer/ReadoorBranches/workspace` | 配置远程仓库时唯一的共享源码工作目录 |
|
||
| `BUILD_BASE_DIR` | `/Users/shen/Work/Code/iOSBuildServer/build` | 打包产物输出基础目录 |
|
||
| `GIT_REMOTE_URL` | `git@github.com:org/repo.git` | 远程仓库地址,首次构建时自动初始化共享源码目录 |
|
||
|
||
私有仓库请创建仅有 `read_repository` 权限的 Personal Access Token,并配置为 `GIT_PASSWORD`;`GIT_USERNAME` 使用该 Token 所属账号。不要在 `.env`、`config.json` 或 Git 仓库中保存账号登录密码。
|
||
|
||
### 服务配置
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `BACKEND_PORT` | `8000` | 后端服务端口 |
|
||
| `ADMIN_USERNAME` | `admin` | 初始管理员用户名(首次启动自动创建) |
|
||
| `ADMIN_PASSWORD` | `admin123` | 初始管理员密码 |
|
||
| `JWT_SECRET` | `ios-build-server-secret-key-change-in-production` | JWT 签名密钥(生产环境务必修改) |
|
||
|
||
## 公网部署
|
||
|
||
不要将 Uvicorn 端口直接暴露到公网。使用 Nginx 或 Caddy 提供 HTTPS,服务仅监听 `127.0.0.1`。项目提供了 [nginx.conf.example](/Users/shen/Work/Code/iOSBuildServer/deploy/nginx.conf.example) 作为模板。
|
||
|
||
公网 `.env` 至少需要如下配置:
|
||
|
||
```bash
|
||
APP_ENV=production
|
||
BIND_HOST=127.0.0.1
|
||
JWT_SECRET=<至少 32 位随机字符串>
|
||
ADMIN_PASSWORD=<至少 12 位强密码>
|
||
CORS_ALLOWED_ORIGINS=https://build.example.com
|
||
TRUSTED_HOSTS=build.example.com
|
||
GIT_USERNAME=<只读 Token 所属账号>
|
||
GIT_PASSWORD=<仅 read_repository 权限的 Personal Access Token>
|
||
```
|
||
|
||
设置完成后执行 `chmod 600 .env`,并在云安全组和系统防火墙中只开放 `80/443`。`APP_ENV=production` 会在默认 JWT、弱管理员密码、通配 CORS 或未设置可信域名时拒绝启动。
|
||
|
||
### 打包配置
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `MAX_CONCURRENT_BUILDS` | `2` | 最大并行打包数 |
|
||
| `BUILD_DIR_RETENTION_HOURS` | `24` | 打包目录保留时间(小时),超期自动清理 |
|
||
| `BUILD_TIMEOUT_HOURS` | `1` | 打包超时时间(小时),超时自动标记失败 |
|
||
|
||
### Web 管理配置
|
||
|
||
以下配置通过管理后台 Web 界面管理(存储在 `config.json`):
|
||
|
||
首次部署时,如果项目根目录下存在 `config.json`,系统会自动导入其中的 `apps` 和 `schemes` 作为初始配置;导入后即可继续在后台增删改。
|
||
|
||
| 配置项 | 说明 |
|
||
|--------|------|
|
||
| App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 |
|
||
| Scheme 管理 | 打包 Scheme 配置 |
|
||
| 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 |
|
||
| 分支管理 | 可打包的代码分支列表 |
|
||
| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码 |
|
||
| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 |
|
||
|
||
### 服务端自动化资源
|
||
|
||
项目内的 `backend/automation/` 是 Web 打包服务唯一使用的自动化资源:默认皮肤和混淆工具都在此处维护。分支源码中的 `AutoPacking/` 不会被复制、执行或修改,仍可供开发者手动打包使用。
|
||
|
||
### Watchdog 配置
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `WATCHDOG_INTERVAL` | `30` | 健康检查间隔(秒) |
|
||
| `WATCHDOG_TIMEOUT` | `10` | HTTP 健康检查超时(秒) |
|
||
| `MAX_RESTART_ATTEMPTS` | `3` | 连续重启上限,超过后进入冷却 |
|
||
| `COOLDOWN_SECONDS` | `300` | 冷却时间(秒) |
|
||
|
||
---
|
||
|
||
## 服务管理
|
||
|
||
所有服务管理通过 `deploy.sh` 完成:
|
||
|
||
```bash
|
||
./deploy.sh <command>
|
||
```
|
||
|
||
### 命令一览
|
||
|
||
| 命令 | 说明 |
|
||
|------|------|
|
||
| `build` | 构建前端、创建 Python 虚拟环境、安装所有依赖 |
|
||
| `start` | 后台启动服务 |
|
||
| `stop` | 停止服务和 watchdog |
|
||
| `restart` | 重启服务 |
|
||
| `status` | 查看服务和 watchdog 运行状态 |
|
||
| `test` | 一键运行全部测试(后端 + 前端) |
|
||
| `watchdog` | 启动健康监测(后台守护) |
|
||
| `watchdog-stop` | 停止健康监测 |
|
||
| `watchdog-fg` | 前台运行健康监测(调试用) |
|
||
|
||
### 常用操作
|
||
|
||
```bash
|
||
# 首次部署
|
||
./deploy.sh build && ./deploy.sh start
|
||
|
||
# 查看状态
|
||
./deploy.sh status
|
||
|
||
# 更新代码后重新部署
|
||
git pull
|
||
./deploy.sh build
|
||
./deploy.sh restart
|
||
|
||
# 启用健康监测
|
||
./deploy.sh watchdog
|
||
|
||
# 停止所有服务
|
||
./deploy.sh stop
|
||
```
|
||
|
||
### 日志
|
||
|
||
日志文件位于 `logs/` 目录:
|
||
|
||
```
|
||
logs/
|
||
├── server.log # 服务运行日志
|
||
├── watchdog.log # Watchdog 操作日志(重启记录等)
|
||
├── server-stdout.log # launchd 模式下的标准输出
|
||
├── server-stderr.log # launchd 模式下的标准错误
|
||
├── watchdog-stdout.log # launchd watchdog 标准输出
|
||
└── watchdog-stderr.log # launchd watchdog 标准错误
|
||
```
|
||
|
||
---
|
||
|
||
## 多用户管理
|
||
|
||
系统支持多用户管理,基于 JWT 认证。首次启动时自动创建 `.env` 中配置的管理员账号。
|
||
|
||
### 用户角色
|
||
|
||
| 角色 | 权限 |
|
||
|------|------|
|
||
| 管理员 | 所有功能 + 用户管理(增删改查、角色切换) |
|
||
| 普通用户 | 打包、查看历史、查看配置(不可修改配置、不可管理用户) |
|
||
|
||
### 管理用户
|
||
|
||
在管理后台「用户管理」页面(仅管理员可见):
|
||
|
||
- 创建用户:设置用户名、密码(至少 6 位)、是否管理员
|
||
- 修改密码:管理员可修改任意用户密码
|
||
- 切换角色:在管理员/普通用户之间切换
|
||
- 删除用户:不能删除自己
|
||
|
||
### 认证流程
|
||
|
||
1. `POST /api/auth/login` — 用户名+密码登录,返回 JWT token
|
||
2. 后续请求在 `Authorization: Bearer <token>` 头中携带 token
|
||
3. Token 有效期 24 小时,过期需重新登录
|
||
|
||
---
|
||
|
||
## Watchdog 健康监测
|
||
|
||
Watchdog 定期检查服务健康状态,发现异常自动重启。
|
||
|
||
### 检测逻辑
|
||
|
||
```
|
||
每 N 秒执行一次
|
||
│
|
||
├─ 进程是否存在? (kill -0)
|
||
│ └─ 否 → 重启
|
||
│
|
||
└─ HTTP /api/health 返回 200?
|
||
├─ 是 → 正常,继续轮询
|
||
└─ 否 → 重启
|
||
```
|
||
|
||
### 重启保护
|
||
|
||
- 连续失败次数 <= `MAX_RESTART_ATTEMPTS`:立即重启
|
||
- 连续失败次数 > `MAX_RESTART_ATTEMPTS`:进入冷却期(`COOLDOWN_SECONDS` 秒),冷却结束后重置计数器重新尝试
|
||
|
||
### 运行方式
|
||
|
||
```bash
|
||
# 后台守护(推荐)
|
||
./deploy.sh watchdog
|
||
|
||
# 前台运行(调试,可看到实时输出)
|
||
./deploy.sh watchdog-fg
|
||
|
||
# 查看 watchdog 日志
|
||
tail -f logs/watchdog.log
|
||
|
||
# 停止 watchdog
|
||
./deploy.sh watchdog-stop
|
||
```
|
||
|
||
---
|
||
|
||
## macOS 开机自启
|
||
|
||
通过 macOS 原生的 launchd 实现开机自启和进程守护。
|
||
|
||
### 安装
|
||
|
||
```bash
|
||
# 前置条件:先完成构建
|
||
./deploy.sh build
|
||
|
||
# 安装服务
|
||
./deploy/install-service.sh install
|
||
```
|
||
|
||
安装后会注册两个 launchd 服务:
|
||
|
||
| 服务 | 说明 |
|
||
|------|------|
|
||
| `com.readoor.buildserver` | 主服务,进程崩溃自动拉起 |
|
||
| `com.readoor.buildserver.watchdog` | 健康监测,检测 HTTP 卡死并重启 |
|
||
|
||
### 卸载
|
||
|
||
```bash
|
||
./deploy/install-service.sh uninstall
|
||
```
|
||
|
||
### 手动管理 launchd 服务
|
||
|
||
```bash
|
||
# 查看服务状态
|
||
launchctl list | grep readoor
|
||
|
||
# 手动停止
|
||
launchctl unload ~/Library/LaunchAgents/com.readoor.buildserver.plist
|
||
launchctl unload ~/Library/LaunchAgents/com.readoor.buildserver.watchdog.plist
|
||
|
||
# 手动启动
|
||
launchctl load ~/Library/LaunchAgents/com.readoor.buildserver.plist
|
||
launchctl load ~/Library/LaunchAgents/com.readoor.buildserver.watchdog.plist
|
||
```
|
||
|
||
---
|
||
|
||
## 开发模式
|
||
|
||
一键启动本地开发环境(后端 + 前端同时运行):
|
||
|
||
```bash
|
||
./start.sh
|
||
```
|
||
|
||
- 前端界面:`http://localhost:3000`(自动代理 API 请求到后端)
|
||
- 后端 API:`http://localhost:8000`
|
||
- 前端修改自动热更新,后端修改自动 reload
|
||
- `Ctrl+C` 同时停止所有服务
|
||
|
||
---
|
||
|
||
## 自动化测试
|
||
|
||
### 运行测试
|
||
|
||
```bash
|
||
# 一键运行全部测试(后端 89 + 前端 35 = 124 个)
|
||
./deploy.sh test
|
||
|
||
# 只运行匹配的测试
|
||
./deploy.sh test -k test_login
|
||
|
||
# 单独运行后端测试
|
||
.venv/bin/python -m pytest tests/ -v
|
||
|
||
# 单独运行前端测试
|
||
cd frontend && npx vitest run
|
||
```
|
||
|
||
### 测试覆盖
|
||
|
||
| 测试文件 | 覆盖内容 | 数量 |
|
||
|----------|----------|------|
|
||
| `test_api_health.py` | 健康检查 | 1 |
|
||
| `test_api_auth.py` | 登录认证 | 3 |
|
||
| `test_api_config.py` | App/Scheme/Branch/Server/Build 配置 CRUD | 20 |
|
||
| `test_api_tasks.py` | 任务创建/列表/详情/取消 | 9 |
|
||
| `test_api_apps.py` | 打包选择接口 | 3 |
|
||
| `test_websocket.py` | 日志流订阅/发送/完成/清理 | 10 |
|
||
| `test_downloads.py` | dSYM/混淆映射/二维码下载 | 11 |
|
||
| `test_build_service.py` | 源码更新/拷贝/配置生成/目录清理 | 11 |
|
||
| `test_build_queue.py` | 并发控制/任务执行/取消 | 7 |
|
||
| `test_edge_cases.py` | 边界情况/无效参数/配置备份 | 14 |
|
||
| `App.test.js` | 登录/退出/导航 | 8 |
|
||
| `BuildView.test.js` | 打包表单/任务列表/提交 | 8 |
|
||
| `ConfigView.test.js` | 配置管理/分支增删/设置保存 | 8 |
|
||
| `HistoryView.test.js` | 历史列表/过滤/下载/二维码 | 11 |
|
||
|
||
---
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
BuildServer/
|
||
├── .env.example # 环境变量模板
|
||
├── .env # 实际配置(不入库)
|
||
├── .gitignore
|
||
├── README.md # 项目文档
|
||
├── deploy.sh # 部署与管理脚本
|
||
├── start.sh # 开发模式启动脚本(一键启动前后端)
|
||
├── requirements.txt # Python 依赖
|
||
├── requirements-dev.txt # 测试依赖(pytest, httpx)
|
||
├── pytest.ini # pytest 配置
|
||
│
|
||
├── deploy/ # 部署相关
|
||
│ └── install-service.sh # macOS launchd 服务安装
|
||
│
|
||
├── tests/ # 后端测试(pytest)
|
||
│ ├── conftest.py # 测试 fixtures
|
||
│ ├── test_api_*.py # API 接口测试
|
||
│ ├── test_websocket.py # WebSocket 测试
|
||
│ ├── test_downloads.py # 文件下载测试
|
||
│ ├── test_build_service.py # 打包服务测试
|
||
│ ├── test_build_queue.py # 队列测试
|
||
│ └── test_edge_cases.py # 边界情况测试
|
||
│
|
||
├── backend/ # 后端(FastAPI)
|
||
│ ├── main.py # 入口,路由注册,静态文件服务
|
||
│ ├── config.py # 配置加载(读取 .env)
|
||
│ ├── database.py # SQLAlchemy 数据库初始化
|
||
│ ├── models.py # 数据库模型(Task, BuildConfig, User)
|
||
│ ├── schemas.py # Pydantic 请求/响应模型
|
||
│ ├── deps.py # JWT 认证依赖
|
||
│ ├── routers/
|
||
│ │ ├── auth.py # 登录认证(JWT)
|
||
│ │ ├── users.py # 用户管理(仅管理员)
|
||
│ │ ├── config.py # 配置管理(App/Scheme/Server/Upload/Branch/Version CRUD)
|
||
│ │ ├── apps.py # 打包选择(只读)
|
||
│ │ └── tasks.py # 任务管理(创建/列表/取消/下载)
|
||
│ └── services/
|
||
│ ├── build_service.py # 打包核心流程
|
||
│ ├── build_queue.py # 异步任务队列
|
||
│ └── log_streamer.py # WebSocket 日志流
|
||
│
|
||
└── frontend/ # 前端(Vue 3 + Vite)
|
||
├── vite.config.js # 构建配置,开发代理
|
||
├── vitest.config.js # 前端测试配置
|
||
├── index.html
|
||
└── src/
|
||
├── App.vue # 根组件(导航/登录)
|
||
├── router/index.js # 路由
|
||
├── composables/
|
||
│ └── useWebSocket.js
|
||
├── views/
|
||
│ ├── BuildView.vue # 打包页面
|
||
│ ├── HistoryView.vue # 历史记录
|
||
│ └── ConfigView.vue # 管理后台
|
||
└── __tests__/ # 前端测试(vitest)
|
||
├── App.test.js
|
||
├── BuildView.test.js
|
||
├── ConfigView.test.js
|
||
└── HistoryView.test.js
|
||
```
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
### Q: 打包时提示 "未找到 xcodebuild"
|
||
|
||
确保安装了 Xcode 和 Command Line Tools:
|
||
|
||
```bash
|
||
xcode-select --install
|
||
xcodebuild -version
|
||
```
|
||
|
||
### Q: 前端页面打不开
|
||
|
||
检查前端是否已构建:
|
||
|
||
```bash
|
||
ls backend/static/index.html # 应该存在
|
||
# 如果不存在:
|
||
./deploy.sh build
|
||
./deploy.sh restart
|
||
```
|
||
|
||
### Q: 端口被占用
|
||
|
||
修改 `.env` 中的 `BACKEND_PORT`,然后重启:
|
||
|
||
```bash
|
||
vim .env
|
||
./deploy.sh restart
|
||
```
|
||
|
||
### Q: 打包目录占满磁盘
|
||
|
||
调整保留时间或手动清理:
|
||
|
||
```bash
|
||
# 缩短保留时间
|
||
vim .env # BUILD_DIR_RETENTION_HOURS=12
|
||
./deploy.sh restart
|
||
|
||
# 手动清理所有打包目录
|
||
rm -rf /path/to/build/output/build_readoor_*
|
||
```
|
||
|
||
### Q: Watchdog 频繁重启
|
||
|
||
查看 watchdog 日志定位原因:
|
||
|
||
```bash
|
||
tail -50 logs/watchdog.log
|
||
tail -50 logs/server.log
|
||
```
|
||
|
||
常见原因:
|
||
- `GIT_SOURCE_BASE` 下对应分支目录不存在,且未配置 `GIT_REMOTE_URL`
|
||
- 磁盘空间不足
|
||
- Python 依赖缺失(重新执行 `./deploy.sh build`)
|
||
|
||
### Q: 如何修改管理员密码
|
||
|
||
**方法一**:通过管理后台「用户管理」页面修改(推荐)
|
||
|
||
**方法二**:编辑 `.env` 后重启(仅影响初始管理员账号):
|
||
|
||
```bash
|
||
ADMIN_PASSWORD=new_password
|
||
./deploy.sh restart
|
||
```
|
||
|
||
注意:`.env` 中的管理员账号仅在首次启动时创建,之后的密码修改请通过管理后台操作。
|
||
|
||
### Q: 多台 Mac 部署
|
||
|
||
每台 Mac 上:
|
||
|
||
```bash
|
||
git clone <repo>
|
||
cd BuildServer
|
||
cp .env.example .env
|
||
# 编辑 .env 配置路径和密码
|
||
./deploy.sh build
|
||
./deploy.sh start
|
||
./deploy.sh watchdog
|
||
```
|
||
|
||
各机器独立运行,配置互不影响。
|
||
|
||
### Q: 如何按分支打包
|
||
|
||
1. 在 `.env` 中配置 `GIT_SOURCE_BASE`、`GIT_SOURCE_DIR` 和 `GIT_REMOTE_URL`
|
||
2. 在管理页面「分支管理」中添加需要打包的分支(默认已有 `main`)
|
||
3. 打包时在「代码分支」下拉框中选择分支
|
||
4. 首次打包会初始化共享源码目录;每次任务会串行 `git fetch`、切换目标分支、清理并复制源码快照
|
||
5. 快照复制完成后,依赖安装和 Xcode 打包仍在独立目录中并行执行
|
||
|
||
### Q: 源码目录占满磁盘怎么办
|
||
|
||
```bash
|
||
# 远程仓库模式下只有一个共享工作目录
|
||
du -sh /path/to/ReadoorBranches/workspace
|
||
|
||
# 打包目录由 BUILD_DIR_RETENTION_HOURS 自动清理;可查看其占用
|
||
du -sh /path/to/build/build_readoor_*
|
||
```
|
||
|
||
共享工作目录由服务在每次打包前自动同步和清理。请勿在服务运行期间手动删除或修改该目录。
|
||
|
||
### Q: 如何运行测试
|
||
|
||
```bash
|
||
# 一键运行全部测试(后端 + 前端)
|
||
./deploy.sh test
|
||
|
||
# 只运行后端测试
|
||
.venv/bin/python -m pytest tests/ -v
|
||
|
||
# 只运行前端测试
|
||
cd frontend && npx vitest run
|
||
|
||
# 运行指定测试
|
||
./deploy.sh test -k test_login
|
||
```
|