├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── app ├── __init__.py ├── api_test │ ├── __init__.py │ ├── apiMsg │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── case │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── errorRecord │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── errors.py │ ├── func │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── home │ │ ├── __init__.py │ │ └── views.py │ ├── module │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── project │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── report │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── sets │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ ├── step │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py │ └── task │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py ├── baseForm.py ├── baseModel.py ├── baseView.py ├── config │ ├── __init__.py │ ├── errors.py │ ├── forms.py │ ├── models.py │ └── views.py ├── test_work │ ├── __init__.py │ ├── account │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── dataBase │ │ ├── __init__.py │ │ └── views.py │ ├── dataPool │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── errors.py │ ├── file │ │ ├── __init__.py │ │ └── views.py │ ├── frontDiff │ │ ├── __init__.py │ │ └── models.py │ ├── kym │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── swagger │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ └── yapi │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py ├── tools │ ├── __init__.py │ ├── errors.py │ ├── examination.py │ ├── makeUser.py │ ├── mockData.py │ └── zhengXinTest.json ├── ucenter │ ├── __init__.py │ ├── errors.py │ └── user │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── views.py ├── ui_test │ ├── __init__.py │ ├── errors.py │ ├── page │ │ └── __init__.py │ └── project │ │ └── __init__.py └── utils │ ├── __init__.py │ ├── gitOperation.py │ ├── globalVariable.py │ ├── httprunner │ ├── __about__.py │ ├── __init__.py │ ├── api.py │ ├── built_in.py │ ├── cli.py │ ├── client.py │ ├── compat.py │ ├── context.py │ ├── exceptions.py │ ├── loader.py │ ├── locusts.py │ ├── logger.py │ ├── parser.py │ ├── report.py │ ├── response.py │ ├── runner.py │ ├── templates │ │ ├── locustfile_template │ │ ├── report_template.html │ │ └── report_template原版.html │ ├── utils.py │ └── validator.py │ ├── jsonUtil.py │ ├── log.py │ ├── makeUserTools.py │ ├── makeXmind.py │ ├── parse.py │ ├── parseCron.py │ ├── parseExcel.py │ ├── parseModel.py │ ├── regexp.py │ ├── report │ ├── __init__.py │ ├── extent_report_template.html │ ├── report.py │ ├── report_template.html │ └── summary.json │ ├── required.py │ ├── restful.py │ ├── runHttpRunner.py │ ├── sendEmail.py │ ├── sendReport.py │ └── yamlUtil.py ├── config ├── __init__.py ├── config.py └── config.yaml ├── dbMigration.py ├── gunicornConfig.py ├── images ├── api │ ├── form-data.png │ ├── json.png │ ├── xml.png │ ├── 头部信息.png │ ├── 拖拽排序.png │ ├── 接口概要信息.png │ ├── 提取信息.png │ ├── 数据提取.png │ ├── 断言.png │ ├── 新增接口入口.png │ ├── 查询字符串参数.png │ ├── 测试报告.png │ ├── 请求信息.png │ ├── 请求方式配置.png │ ├── 调试接口.png │ ├── 运行接口.png │ ├── 返回信息.png │ └── 返回结果.png ├── case │ ├── 修改用例.png │ ├── 公用变量.png │ ├── 复制用例.png │ ├── 头部信息.png │ ├── 引用用例.png │ ├── 拖拽排序.png │ ├── 添加用例入口.png │ ├── 用例信息.png │ ├── 运行用例.png │ └── 运行用例结果.png ├── config │ ├── 全局参数设置.png │ ├── 配置类型管理.png │ └── 配置类型管理_数据库.png ├── file │ └── 文件管理.png ├── funcFile │ ├── 使用自定义函数.png │ ├── 引用函数文件_用例管理处.png │ ├── 引用函数文件_项目管理处.png │ ├── 新建函数文件.png │ └── 调试函数.png ├── home │ └── 首页统计图.png ├── module │ ├── 模块内容.png │ ├── 模块操作.png │ └── 添加模块.png ├── project │ ├── 使用公用变量.png │ ├── 公用变量.png │ ├── 头部信息.png │ └── 添加项目.png ├── report │ └── 测试报告.png ├── step │ ├── 保存步骤.png │ ├── 接口转步骤.png │ ├── 数据驱动.png │ └── 步骤信息.png └── task │ ├── webhook.png │ ├── 任务名称.png │ ├── 任务概要信息.png │ ├── 修改任务.png │ ├── 修改任务状态.png │ ├── 发送报告.png │ ├── 定时任务编辑框.png │ ├── 微信群.png │ ├── 新增定时任务.png │ ├── 时间配置.png │ ├── 运行定时任务.png │ ├── 选择用例.png │ ├── 邮件.png │ ├── 邮箱服务器配置.png │ ├── 都接收.png │ └── 钉钉群.png ├── job.py ├── main.py ├── nginx.conf ├── requirements.txt ├── template ├── __init__.py └── 接口导入模板.xls └── 操作手册.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | venv/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | #Ipython Notebook 64 | .ipynb_checkpoints 65 | 66 | # pyenv 67 | .python-version 68 | 69 | # pycharm 70 | .idea/ 71 | 72 | # Windows: 73 | data.sqlite 74 | # /migrations/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 注:此版本只支持接口自动化,后面不再维护,新版本请移步[测试平台](https://github.com/zhongyehai/test-platform-api) 2 | 3 | # 基于python-flask生态 + HttpRunner 开发的rest风格的测试平台后端 4 | 5 | ## 线上预览:http://139.196.100.202/#/login 账号:tester、密码:123456 6 | 7 | ## 前端传送门:https://github.com/zhongyehai/api-test-front 8 | 9 | ## 系统操作手册:[gitee](https://gitee.com/Xiang-Qian-Zou/api-test-api/blob/master/%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8C.md) ,[github](https://github.com/zhongyehai/api-test-api/blob/main/%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8C.md) 10 | 11 | ## Python版本:python => 3.9+ 12 | 13 | ### 1.安装依赖包:sudo pip install -i https://pypi.douban.com/simple/ -r requirements.txt 14 | 15 | ### 2.创建MySQL数据库,数据库名自己取,编码选择utf8mb4,对应config.yaml下db配置为当前数据库信息即可 16 | 17 | ### 3.初始化数据库表结构(项目根目录下依次执行下面3条命令): 18 | sudo python dbMigration.py db init 19 | sudo python dbMigration.py db migrate 20 | sudo python dbMigration.py db upgrade 21 | 22 | ### 4.初始化权限、角色、管理员(项目根目录下执行,账号:admin,密码:123456) 23 | sudo python dbMigration.py init 24 | 25 | ### 5.生产环境下的一些配置: 26 | 1.把后端端口改为8024启动 27 | 2.为避免定时任务重复触发,需关闭debug模式(debug=False或去掉debug参数) 28 | 3.准备好前端包,并在nginx.location / 下指定前端包的路径 29 | 4.直接把项目下的nginx.conf文件替换nginx下的nginx.conf文件 30 | 5.nginx -s reload 重启nginx 31 | 32 | ### 6.启动项目 33 | 开发环境: 34 | 运行测试平台 main.py 35 | 运行定时任务服务 jobServer.py 36 | 37 | 生产环境: 38 | 39 | 运行测试平台: 40 | 使用配置文件: sudo nohup gunicorn -c gunicornConfig.py main:app –preload & 41 | 不使用配置文件: sudo nohup gunicorn -w 1 -b 0.0.0.0:8024 main:app –preload & 42 | 43 | 运行定时任务服务(定时任务只起一个进程即可,起多了会冲突): 44 | 不使用配置文件: sudo nohup gunicorn -w 1 -b 0.0.0.0:8025 job:job –preload & 45 | 46 | 如果报 gunicorn 命令不存在,则先找到 gunicorn 安装目录,创建软连接 47 | ln -s /usr/local/python3/bin/gunicorn /usr/bin/gunicorn 48 | ln -s /usr/local/python3/bin/gunicorn /usr/local/bin/gunicorn 49 | sudo nohup /usr/local/bin/gunicorn -w 1 -b 0.0.0.0:8024 main:app –preload & 50 | 51 | ### 修改依赖后创建依赖:sudo pip freeze > requirements.txt 52 | 53 | 54 | ### 7.项目逻辑 55 | 1.项目数据管理:项目 --> 模块 --> 接口 56 | 2.用例数据管理:项目 --> 用例集 --> 用例 --> 步骤(由接口转化而来) 57 | 3.自定义变量: 58 | 1.在项目管理可以创建项目级的自定义变量,在需要使用的时候用 "$变量名" 引用 59 | 2.在用例管理可以创建用例级的自定义变量,在需要使用的时候用 "$变量名" 引用 60 | 3.在用例运行中可以提取步骤级的自定义变量,在需要使用的时候用 "$变量名" 引用 61 | 4.辅助函数 62 | 1.在自定义函数模块下可以创建用于辅助实现某些功能的py脚本,里面可以创建函数用于实现平台未提供的功能 63 | 2.使用: 64 | 1.在项目管理 或者用例管理,引用此文件 65 | 2.在需要使用的地方,用 "${函数名(参数)}" 引用 66 | 5.参数传递 67 | 1.在每个步骤中都有一个数据提取的模块,此模块支持xpath提取,逻辑为 key: 变量名、value: xpath 68 | 2.当数据提取出来过后,在用例钟就可以使用 "$变量名" 引用 69 | 6.支持数据驱动,详见操作手册 70 | 7.支持跨项目的接口组装为用例 71 | 当用例需要多个项目的接口组装为一条用例的时候,支持按需引用,即需要哪个项目的哪个接口,就引入该接口即可 72 | 8.用例引用 73 | 支持引用已组装的用例,如登录,当前用例的前置条件,只需引用即可 74 | 75 | ### 创作不易,麻烦给个星哦 76 | 77 | ### QQ交流群:249728408 78 | ### 博客地址:https://www.cnblogs.com/zhongyehai/ 79 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/__init__.py -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py 7 | # @Software: PyCharm 8 | import os 9 | 10 | from flask import Flask 11 | from flask_login import LoginManager 12 | 13 | from app.utils import globalVariable 14 | from app.utils.log import logger 15 | from config.config import conf, ProductionConfig # ,logger 16 | from app.baseModel import db 17 | 18 | login_manager = LoginManager() 19 | basedir = os.path.abspath(os.path.dirname(__file__)) 20 | 21 | 22 | def create_app(): 23 | app = Flask(__name__) 24 | app.conf = conf 25 | app.config.from_object(ProductionConfig) 26 | app.logger = logger 27 | ProductionConfig.init_app(app) 28 | 29 | db.init_app(app) 30 | db.app = app 31 | db.create_all() 32 | login_manager.init_app(app) 33 | 34 | from app.api_test import api_test as api_test_blueprint 35 | from app.ui_test import ui_test as ui_test_blueprint 36 | from app.ucenter import ucenter as ucenter_blueprint 37 | from app.test_work import test_work as test_work_blueprint 38 | from app.config import config as config_blueprint 39 | from app.tools import tool as tool_blueprint 40 | app.register_blueprint(api_test_blueprint, url_prefix='/api/apiTest') 41 | app.register_blueprint(ui_test_blueprint, url_prefix='/api/uiTest') 42 | app.register_blueprint(ucenter_blueprint, url_prefix='/api/ucenter') 43 | app.register_blueprint(config_blueprint, url_prefix='/api/config') 44 | app.register_blueprint(test_work_blueprint, url_prefix='/api/testWork') 45 | app.register_blueprint(tool_blueprint, url_prefix='/api/tool') 46 | 47 | return app 48 | -------------------------------------------------------------------------------- /app/api_test/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from flask import Blueprint, current_app, request 5 | from flask_login import current_user 6 | 7 | from app.utils.log import logger 8 | 9 | api_test = Blueprint('apiTest', __name__) 10 | api_test.logger = logger 11 | 12 | from . import (errors) 13 | from app.api_test.project import views 14 | from app.api_test.module import views 15 | from app.api_test.apiMsg import views 16 | from app.api_test.sets import views 17 | from app.api_test.case import views 18 | from app.api_test.step import views 19 | from app.api_test.task import views 20 | from app.api_test.report import views 21 | from app.api_test.func import views 22 | from app.api_test.errorRecord import views 23 | from app.api_test.home import views 24 | 25 | 26 | @api_test.before_request 27 | def before_request(): 28 | """ 前置钩子函数, 每个请求进来先经过此函数""" 29 | name = current_user.name if hasattr(current_user, 'name') else '' 30 | current_app.logger.info( 31 | f'[{request.remote_addr}] [{name}] [{request.method}] [{request.url}]: \n请求参数:{request.json}') 32 | 33 | 34 | @api_test.after_request 35 | def after_request(response_obj): 36 | """ 后置钩子函数,每个请求最后都会经过此函数 """ 37 | if 'download' in request.path: 38 | return response_obj 39 | result = copy.copy(response_obj.response) 40 | if isinstance(result[0], bytes): 41 | result[0] = bytes.decode(result[0]) 42 | # 减少日志数据打印,跑用例的数据均不打印到日志 43 | if 'apiMsg/run' not in request.path and 'report/run' not in request.path and 'report/list' not in request.path: 44 | current_app.logger.info(f'{request.method}==>{request.url}, 返回数据:{json.loads(result[0])}') 45 | return response_obj 46 | -------------------------------------------------------------------------------- /app/api_test/apiMsg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/apiMsg/__init__.py -------------------------------------------------------------------------------- /app/api_test/apiMsg/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | from wtforms.validators import ValidationError, Length, DataRequired 10 | 11 | from app.baseForm import BaseForm 12 | from ..case.models import Case 13 | from ..func.models import Func 14 | from ..step.models import Step 15 | from ..apiMsg.models import ApiMsg 16 | from ..module.models import Module 17 | from ..project.models import Project, ProjectEnv 18 | 19 | 20 | class AddApiForm(BaseForm): 21 | """ 添加接口信息的校验 """ 22 | project_id = StringField(validators=[DataRequired('服务id必传')]) 23 | module_id = StringField(validators=[DataRequired('模块id必传')]) 24 | 25 | name = StringField(validators=[DataRequired('接口名必传'), Length(1, 255, '接口名长度为1~255位')]) 26 | desc = StringField() 27 | up_func = StringField() # 前置条件 28 | down_func = StringField() # 后置条件 29 | method = StringField(validators=[DataRequired('请求方法必传'), Length(1, 10, message='请求方法长度为1~10位')]) 30 | choice_host = StringField(validators=[DataRequired('请选择要运行的环境')]) 31 | addr = StringField(validators=[DataRequired('接口地址必传')]) 32 | headers = StringField() 33 | params = StringField() 34 | data_type = StringField() 35 | data_form = StringField() 36 | data_json = StringField() 37 | data_xml = StringField() 38 | extracts = StringField() 39 | validates = StringField() 40 | num = StringField() 41 | 42 | def validate_project_id(self, field): 43 | """ 校验服务id """ 44 | project = Project.get_first(id=field.data) 45 | if not project: 46 | raise ValidationError(f'id为【{field.data}】的服务不存在') 47 | setattr(self, 'project', project) 48 | 49 | def validate_module_id(self, field): 50 | """ 校验模块id """ 51 | if not Module.get_first(id=field.data): 52 | raise ValidationError(f'id为【{field.data}】的模块不存在') 53 | 54 | def validate_name(self, field): 55 | """ 校验同一模块下接口名不重复 """ 56 | if ApiMsg.get_first(name=field.data, module_id=self.module_id.data): 57 | raise ValidationError(f'当前模块下,名为【{field.data}】的接口已存在') 58 | 59 | def validate_addr(self, field): 60 | """ 接口地址校验 """ 61 | if not field.data.split('?')[0]: 62 | raise ValidationError('接口地址不能为空') 63 | 64 | def validate_extracts(self, field): 65 | """ 校验提取数据表达式 """ 66 | self.validate_base_extracts(field.data) 67 | 68 | def validate_validates(self, field): 69 | """ 校验断言表达式 """ 70 | func_files = self.loads( 71 | ProjectEnv.get_first(project_id=self.project.id, env=self.choice_host.data).func_files) 72 | func_container = Func.get_func_by_func_file_name(func_files) 73 | self.validate_base_validates(field.data, func_container) 74 | 75 | 76 | class EditApiForm(AddApiForm): 77 | """ 修改接口信息 """ 78 | id = IntegerField(validators=[DataRequired('接口id必传')]) 79 | 80 | def validate_id(self, field): 81 | """ 校验接口id已存在 """ 82 | old = ApiMsg.get_first(id=field.data) 83 | if not old: 84 | raise ValidationError(f'id为【{field.data}】的接口不存在') 85 | setattr(self, 'old', old) 86 | 87 | def validate_name(self, field): 88 | """ 校验接口名不重复 """ 89 | old_api = ApiMsg.get_first(name=field.data, module_id=self.module_id.data) 90 | if old_api and old_api.id != self.id.data: 91 | raise ValidationError(f'当前模块下,名为【{field.data}】的接口已存在') 92 | 93 | 94 | class ValidateProjectId(BaseForm): 95 | """ 校验服务id """ 96 | projectId = IntegerField(validators=[DataRequired('服务id必传')]) 97 | 98 | def validate_projectId(self, field): 99 | """ 校验服务id """ 100 | if not Project.get_first(id=field.data): 101 | raise ValidationError(f'id为【{field.data}】的服务不存在') 102 | 103 | 104 | class RunApiMsgForm(ValidateProjectId): 105 | """ 运行接口 """ 106 | apis = StringField(validators=[DataRequired('请选择接口,再进行测试')]) 107 | 108 | def validate_apis(self, field): 109 | """ 校验接口存在 """ 110 | api = ApiMsg.get_first(id=field.data) 111 | if not api: 112 | raise ValidationError(f'id为【{field.data}】的接口不存在') 113 | setattr(self, 'api', api) 114 | 115 | 116 | class ApiListForm(BaseForm): 117 | """ 查询接口信息 """ 118 | moduleId = IntegerField(validators=[DataRequired('请选择模块')]) 119 | name = StringField() 120 | pageNum = IntegerField() 121 | pageSize = IntegerField() 122 | 123 | 124 | class GetApiById(BaseForm): 125 | """ 待编辑信息 """ 126 | id = IntegerField(validators=[DataRequired('接口id必传')]) 127 | 128 | def validate_id(self, field): 129 | api = ApiMsg.get_first(id=field.data) 130 | if not api: 131 | raise ValidationError(f'id为【{field.data}】的接口不存在') 132 | setattr(self, 'api', api) 133 | 134 | 135 | class ApiBelongToForm(BaseForm): 136 | """ 查询api归属 """ 137 | addr = StringField(validators=[DataRequired('接口地址必传')]) 138 | 139 | def validate_addr(self, field): 140 | api = ApiMsg.get_first(addr=field.data) 141 | if not api: 142 | raise ValidationError(f'地址为【{field.data}】的接口不存在') 143 | setattr(self, 'api', api) 144 | 145 | 146 | class DeleteApiForm(GetApiById): 147 | """ 删除接口 """ 148 | 149 | def validate_id(self, field): 150 | api = ApiMsg.get_first(id=field.data) 151 | if not api: 152 | raise ValidationError(f'id为【{field.data}】的接口不存在') 153 | 154 | # 校验接口是否被测试用例引用 155 | case_data = Step.get_first(api_id=field.data) 156 | if case_data: 157 | case = Case.get_first(id=case_data.case_id) 158 | raise ValidationError(f'用例【{case.name}】已引用此接口,请先解除引用') 159 | 160 | project_id = Module.get_first(id=api.module_id).project_id 161 | if not Project.is_can_delete(project_id, api): 162 | raise ValidationError('不能删除别人服务下的接口') 163 | setattr(self, 'api', api) 164 | -------------------------------------------------------------------------------- /app/api_test/apiMsg/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class ApiMsg(BaseModel): 12 | """ 接口表 """ 13 | __tablename__ = 'apis' 14 | num = db.Column(db.Integer(), nullable=True, comment='接口序号') 15 | name = db.Column(db.String(255), nullable=True, comment='接口名称') 16 | desc = db.Column(db.Text(), default='', nullable=True, comment='接口描述') 17 | up_func = db.Column(db.Text(), default='', comment='接口执行前的函数') 18 | down_func = db.Column(db.Text(), default='', comment='接口执行后的函数') 19 | 20 | method = db.Column(db.String(10), nullable=True, comment='请求方式') 21 | choice_host = db.Column(db.String(10), default='test', comment='选择的环境') 22 | addr = db.Column(db.Text(), nullable=True, comment='接口地址') 23 | headers = db.Column(db.Text(), default='[{"key": null, "remark": null, "value": null}]', comment='头部信息') 24 | params = db.Column(db.Text(), default='[{"key": null, "value": null}]', comment='url参数') 25 | data_type = db.Column(db.String(10), nullable=True, default='json', comment='参数类型,json、form-data、xml') 26 | data_form = db.Column(db.Text(), 27 | default='[{"data_type": null, "key": null, "remark": null, "value": null}]', 28 | comment='form-data参数') 29 | data_json = db.Column(db.Text(), default='{}', comment='json参数') 30 | data_xml = db.Column(db.Text(), default='', comment='xml参数') 31 | extracts = db.Column( 32 | db.Text(), 33 | default='[{"key": null, "data_source": null, "value": null, "remark": null}]', 34 | comment='提取信息' 35 | ) 36 | validates = db.Column( 37 | db.Text(), 38 | default='[{"data_source": null, "key": null, "validate_type": null, "data_type": null, "value": null, "remark": null}]', 39 | comment='断言信息') 40 | 41 | module_id = db.Column(db.Integer(), db.ForeignKey('module.id'), comment='所属的接口模块id') 42 | project_id = db.Column(db.Integer(), nullable=True, comment='所属的服务id') 43 | yapi_id = db.Column(db.Integer(), comment='当前接口在yapi平台的id') 44 | 45 | module = db.relationship('Module', backref='apis') 46 | 47 | def to_dict(self, *args, **kwargs): 48 | return super(ApiMsg, self).to_dict( 49 | to_dict=['headers', 'params', 'data_form', 'data_json', 'extracts', 'validates'] 50 | ) 51 | 52 | @classmethod 53 | def make_pagination(cls, form): 54 | """ 解析分页条件 """ 55 | filters = [] 56 | if form.moduleId.data: 57 | filters.append(ApiMsg.module_id == form.moduleId.data) 58 | if form.name.data: 59 | filters.append(ApiMsg.name.like(f'%{form.name.data}%')) 60 | return cls.pagination( 61 | page_num=form.pageNum.data, 62 | page_size=form.pageSize.data, 63 | filters=filters, 64 | order_by=ApiMsg.num.asc() 65 | ) 66 | -------------------------------------------------------------------------------- /app/api_test/case/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/case/__init__.py -------------------------------------------------------------------------------- /app/api_test/case/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class Case(BaseModel): 12 | """ 用例表 """ 13 | __tablename__ = 'case' 14 | num = db.Column(db.Integer(), nullable=True, comment='用例序号') 15 | name = db.Column(db.String(255), nullable=True, comment='用例名称') 16 | desc = db.Column(db.Text(), comment='用例描述') 17 | is_run = db.Column(db.Boolean(), default=True, comment='是否执行此用例,True执行,False不执行,默认执行') 18 | run_times = db.Column(db.Integer(), default=1, comment='执行次数,默认执行1次') 19 | choice_host = db.Column(db.String(10), default='test', comment='运行环境') 20 | func_files = db.Column(db.Text(), comment='用例需要引用的函数list') 21 | variables = db.Column(db.Text(), comment='用例级的公共参数') 22 | headers = db.Column(db.Text(), comment='用例级的头部信息') 23 | 24 | set_id = db.Column(db.Integer, db.ForeignKey('sets.id'), comment='所属的用例集id') 25 | 26 | def to_dict(self, *args, **kwargs): 27 | return super(Case, self).to_dict(to_dict=['func_files', 'variables', 'headers']) 28 | 29 | @classmethod 30 | def make_pagination(cls, form): 31 | """ 解析分页条件 """ 32 | filters = [] 33 | if form.setId.data: 34 | filters.append(cls.set_id == form.setId.data) 35 | if form.name.data: 36 | filters.append(cls.name.like(f'%{form.name.data}%')) 37 | return cls.pagination( 38 | page_num=form.pageNum.data, 39 | page_size=form.pageSize.data, 40 | filters=filters, 41 | order_by=cls.num.asc() 42 | ) 43 | -------------------------------------------------------------------------------- /app/api_test/errorRecord/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/2/11 17:31 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/api_test/errorRecord/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | 10 | from app.baseForm import BaseForm 11 | 12 | 13 | class FindErrorForm(BaseForm): 14 | """ 查找服务form """ 15 | name = StringField() 16 | pageNum = IntegerField() 17 | pageSize = IntegerField() 18 | -------------------------------------------------------------------------------- /app/api_test/errorRecord/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class ErrorRecord(BaseModel): 12 | """ 服务表 """ 13 | __tablename__ = 'error_record' 14 | 15 | name = db.Column(db.String(255), nullable=True, comment='错误title') 16 | detail = db.Column(db.Text(), default='', comment='错误详情') 17 | 18 | def to_dict(self, *args, **kwargs): 19 | """ 自定义序列化器,把模型的每个字段转为字典,方便返回给前端 """ 20 | return super(ErrorRecord, self).to_dict() 21 | 22 | @classmethod 23 | def make_pagination(cls, form): 24 | """ 解析分页条件 """ 25 | filters = [] 26 | if form.name.data: 27 | filters.append(ErrorRecord.name.like(f'%{form.name.data}%')) 28 | return cls.pagination( 29 | page_num=form.pageNum.data, 30 | page_size=form.pageSize.data, 31 | filters=filters, 32 | order_by=cls.created_time.desc()) 33 | -------------------------------------------------------------------------------- /app/api_test/errorRecord/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/2/11 17:31 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from app.utils import restful 9 | from app.api_test import api_test 10 | from .models import ErrorRecord 11 | from .forms import FindErrorForm 12 | 13 | 14 | @api_test.route('/errorRecord/list', methods=['GET']) 15 | # @login_required 16 | def error_record_list(): 17 | """ 错误列表 """ 18 | form = FindErrorForm() 19 | if form.validate(): 20 | return restful.success(data=ErrorRecord.make_pagination(form)) 21 | return restful.fail(form.get_error()) 22 | -------------------------------------------------------------------------------- /app/api_test/errors.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : errors.py 7 | # @Software: PyCharm 8 | import traceback 9 | 10 | import requests 11 | from flask import current_app, request 12 | 13 | from ..utils import restful 14 | from . import api_test 15 | from config.config import conf 16 | 17 | 18 | @api_test.app_errorhandler(404) 19 | def page_not_found(e): 20 | """ 捕获404的所有异常 """ 21 | # current_app.logger.exception(f'404错误url: {request.path}') 22 | return restful.url_not_find(msg=f'接口 {request.path} 不存在') 23 | 24 | 25 | @api_test.app_errorhandler(Exception) 26 | def error_handler(e): 27 | """ 捕获所有服务器内部的异常 """ 28 | # 把错误发送到 即时达推送 的 系统错误 通道 29 | try: 30 | current_app.logger.error(f'系统出错了: {e}') 31 | requests.post( 32 | url=conf['error_push']['url'], 33 | json={ 34 | 'key': conf['error_push']['key'], 35 | 'head': f'{conf["SECRET_KEY"]}报错了', 36 | 'body': f'{e}' 37 | } 38 | ) 39 | except: 40 | pass 41 | current_app.logger.exception(f'触发错误url: {request.path}\n{traceback.format_exc()}') 42 | return restful.error(f'服务器异常: {e}') 43 | -------------------------------------------------------------------------------- /app/api_test/func/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/api_test/func/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | from wtforms.validators import ValidationError, DataRequired 10 | from flask_login import current_user 11 | 12 | from app.baseForm import BaseForm 13 | from app.api_test.case.models import Case 14 | from app.api_test.project.models import Project 15 | from .models import Func 16 | 17 | 18 | class GetFuncFileForm(BaseForm): 19 | """ 获取函数文件列表 """ 20 | pageNum = IntegerField() 21 | pageSize = IntegerField() 22 | 23 | 24 | class HasFuncForm(BaseForm): 25 | """ 获取自定义函数文件 """ 26 | id = IntegerField(validators=[DataRequired('请输选择函数文件')]) 27 | 28 | def validate_id(self, field): 29 | """ 校验自定义函数文件需存在 """ 30 | func = Func.get_first(id=field.data) 31 | if not func: 32 | raise ValidationError(f'id为 【{field.data}】 的函数文件不存在') 33 | setattr(self, 'func', func) 34 | 35 | 36 | class SaveFuncForm(HasFuncForm): 37 | """ 修改自定义函数文件 """ 38 | func_data = StringField() 39 | name = StringField(validators=[DataRequired('请输入函数文件名')]) 40 | 41 | def validate_name(self, field): 42 | """ 校验自定义函数文件名不重复 """ 43 | func = Func.get_first(name=field.data) 44 | 45 | if func and func.id != self.id.data: 46 | raise ValidationError(f'函数文件名 {field.data} 已存在') 47 | 48 | 49 | class CreatFuncForm(BaseForm): 50 | """ 创建自定义函数文件 """ 51 | name = StringField(validators=[DataRequired('请输入函数文件名')]) 52 | 53 | def validate_name(self, field): 54 | """ 校验Python函数文件 """ 55 | if Func.get_first(name=field.data): 56 | raise ValidationError(f'函数文件【{field.data}】已经存在') 57 | 58 | 59 | class DebuggerFuncForm(HasFuncForm): 60 | """ 调试函数 """ 61 | debug_data = StringField(validators=[DataRequired('请输入要调试的函数')]) 62 | 63 | # def validate_debug_data(self, field): 64 | # if not re.findall(r"\$\{([\w_]+\([\$\w\.\-/_ =,]*\))\}", field.data): 65 | # raise ValidationError('格式错误,请使用【 ${func(*args)} 】格式') 66 | 67 | 68 | class DeleteFuncForm(BaseForm): 69 | """ 删除form """ 70 | 71 | name = StringField(validators=[DataRequired('函数文件必传')]) 72 | 73 | def validate_name(self, field): 74 | """ 75 | 1.校验自定义函数文件需存在 76 | 2.校验是否有引用 77 | 3.校验当前用户是否为管理员或者创建者 78 | """ 79 | func = Func.get_first(name=field.data) 80 | if not func: 81 | raise ValidationError(f'函数文件【{field.data}】不存在') 82 | else: 83 | # 服务引用 84 | project = Project.query.filter(Project.func_files.like(f'%{field.data}%')).first() 85 | if project: 86 | raise ValidationError(f'服务【{project.name}】已引用此函数文件,请先解除依赖再删除') 87 | # 用例引用 88 | case = Case.query.filter(Case.func_files.like(f'%{field.data}%')).first() 89 | if case: 90 | raise ValidationError(f'用例【{case.name}】已引用此函数文件,请先解除依赖再删除') 91 | # 用户是管理员或者创建者 92 | if self.is_not_admin() and not func.is_create_user(current_user.id): 93 | raise ValidationError('函数文件仅【管理员】或【当前函数文件的创建者】可删除') 94 | setattr(self, 'func', func) 95 | -------------------------------------------------------------------------------- /app/api_test/func/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/10/19 14:51 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | import importlib 9 | import os 10 | import types 11 | 12 | from sqlalchemy.dialects.mysql import LONGTEXT 13 | 14 | from app.baseModel import BaseModel, db 15 | from app.utils.globalVariable import FUNC_ADDRESS 16 | 17 | 18 | class Func(BaseModel): 19 | """ 自定义函数 """ 20 | __tablename__ = 'func' 21 | 22 | name = db.Column(db.String(128), nullable=True, unique=True, comment='脚本名称') 23 | func_data = db.Column(LONGTEXT, default='', comment='脚本代码') 24 | 25 | @classmethod 26 | def create_func_file(cls): 27 | """ 创建所有自定义函数 py 文件 """ 28 | for func in cls.get_all(): 29 | with open(os.path.join(FUNC_ADDRESS, f'{func.name}.py'), 'w', encoding='utf8') as file: 30 | file.write(func.func_data) 31 | 32 | @classmethod 33 | def get_func_by_func_file_name(cls, func_file_name_list): 34 | """ 获取指定函数文件中的函数 """ 35 | cls.create_func_file() # 创建所有函数文件 36 | func_dict = {} 37 | for func_file_name in func_file_name_list: 38 | func_list = importlib.reload(importlib.import_module(f'func_list.{func_file_name}')) 39 | func_dict.update({ 40 | name: item for name, item in vars(func_list).items() if isinstance(item, types.FunctionType) 41 | }) 42 | return func_dict 43 | 44 | @classmethod 45 | def make_pagination(cls, form): 46 | """ 解析分页条件 """ 47 | filters = [] 48 | return cls.pagination( 49 | page_num=form.pageNum.data, 50 | page_size=form.pageSize.data, 51 | filters=filters, 52 | order_by=cls.id.asc() 53 | ) 54 | -------------------------------------------------------------------------------- /app/api_test/func/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | import importlib 9 | import types 10 | import traceback 11 | 12 | from flask import current_app 13 | from flask_login import current_user 14 | 15 | from app.utils import restful 16 | from app.utils.required import login_required 17 | from app.utils.globalVariable import os, FUNC_ADDRESS 18 | from app.utils.parse import parse_function, extract_functions 19 | from app.api_test import api_test 20 | from app.baseModel import db 21 | from app.baseView import BaseMethodView 22 | from .models import Func 23 | from .forms import HasFuncForm, SaveFuncForm, CreatFuncForm, DebuggerFuncForm, DeleteFuncForm, GetFuncFileForm 24 | 25 | 26 | @api_test.route('/func/list', methods=['GET']) 27 | @login_required 28 | def func_list(): 29 | """ 查找所有自定义函数文件 """ 30 | form = GetFuncFileForm() 31 | if form.validate(): 32 | return restful.success('获取成功', data=Func.make_pagination(form)) 33 | return restful.error(form.get_error()) 34 | 35 | 36 | @api_test.route('/func/debug', methods=['POST']) 37 | @login_required 38 | def debug_func(): 39 | """ 函数调试 """ 40 | form = DebuggerFuncForm() 41 | if form.validate(): 42 | name, debug_data = form.func.name, form.debug_data.data 43 | # 把自定义函数脚本内容写入到python脚本中 44 | with open(os.path.join(FUNC_ADDRESS, f'{name}.py'), 'w', encoding='utf8') as file: 45 | file.write(form.func.func_data) 46 | # 动态导入脚本 47 | try: 48 | import_path = f'func_list.{name}' 49 | func_list = importlib.reload(importlib.import_module(import_path)) 50 | module_functions_dict = {name: item for name, item in vars(func_list).items() if 51 | isinstance(item, types.FunctionType)} 52 | ext_func = extract_functions(debug_data) 53 | func = parse_function(ext_func[0]) 54 | result = module_functions_dict[func['func_name']](*func['args'], **func['kwargs']) 55 | return restful.success(msg='执行成功,请查看执行结果', result=result) 56 | except Exception as e: 57 | current_app.logger.info(str(e)) 58 | error_data = '\n'.join('{}'.format(traceback.format_exc()).split('↵')) 59 | return restful.fail(msg='语法错误,请检查', result=error_data) 60 | return restful.fail(msg=form.get_error()) 61 | 62 | 63 | class FuncView(BaseMethodView): 64 | 65 | def get(self): 66 | form = HasFuncForm() 67 | if form.validate(): 68 | return restful.success(msg='获取成功', func_data=form.func.func_data) 69 | return restful.fail(form.get_error()) 70 | 71 | def post(self): 72 | form = CreatFuncForm() 73 | if form.validate(): 74 | with db.auto_commit(): 75 | func = Func(name=form.name.data, create_user=current_user.id, update_user=current_user.id) 76 | db.session.add(func) 77 | return restful.success(f'函数文件 {form.name.data} 创建成功') 78 | return restful.fail(form.get_error()) 79 | 80 | def put(self): 81 | form = SaveFuncForm() 82 | 83 | # 把自定义函数脚本内容写入到python脚本中 84 | with open(os.path.join(FUNC_ADDRESS, f'{form.name.data}.py'), 'w', encoding='utf8') as file: 85 | file.write(form.func_data.data) 86 | 87 | # 动态导入脚本,语法有错误则不保存 88 | try: 89 | importlib.reload(importlib.import_module(f'func_list.{form.name.data}')) 90 | except Exception as e: 91 | current_app.logger.info(str(e)) 92 | error_data = '\n'.join('{}'.format(traceback.format_exc()).split('↵')) 93 | return restful.fail(msg='语法错误,请检查', result=error_data) 94 | 95 | if form.validate(): 96 | with db.auto_commit(): 97 | form.func.name, form.func.func_data, form.func.update_user = form.name.data, form.func_data.data, current_user.id 98 | return restful.success(f'函数文件 {form.name.data} 修改成功', data=form.func.to_dict()) 99 | return restful.fail(form.get_error()) 100 | 101 | def delete(self): 102 | form = DeleteFuncForm() 103 | if form.validate(): 104 | form.func.delete() 105 | return restful.success(f'函数文件 {form.name.data} 删除成功') 106 | return restful.fail(form.get_error()) 107 | 108 | 109 | api_test.add_url_rule('/func', view_func=FuncView.as_view('func')) 110 | -------------------------------------------------------------------------------- /app/api_test/home/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/5/24 14:47 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/api_test/module/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/module/__init__.py -------------------------------------------------------------------------------- /app/api_test/module/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | from wtforms.validators import ValidationError, Length, DataRequired 10 | 11 | from app.baseForm import BaseForm 12 | from ..project.models import Project 13 | from .models import Module 14 | 15 | 16 | class AddModelForm(BaseForm): 17 | """ 添加模块的校验 """ 18 | project_id = IntegerField(validators=[DataRequired('服务id必传')]) 19 | name = StringField(validators=[DataRequired('模块名必传'), Length(1, 255, message='模块名称为1~255位')]) 20 | level = StringField() 21 | parent = StringField() 22 | id = StringField() 23 | num = StringField() 24 | 25 | def validate_project_id(self, field): 26 | """ 服务id合法 """ 27 | project = Project.get_first(id=field.data) 28 | if not project: 29 | raise ValidationError(f'id为【{field.data}】的服务不存在,请先创建') 30 | setattr(self, 'project', project) 31 | 32 | def validate_name(self, field): 33 | """ 模块名不重复 """ 34 | old_module = Module.get_first( 35 | project_id=self.project_id.data, level=self.level.data, name=field.data, parent=self.parent.data 36 | ) 37 | if old_module: 38 | raise ValidationError(f'当前服务中已存在名为【{field.data}】的模块') 39 | 40 | 41 | class FindModelForm(BaseForm): 42 | """ 查找模块 """ 43 | projectId = IntegerField(validators=[DataRequired('服务id必传')]) 44 | name = StringField() 45 | pageNum = IntegerField() 46 | pageSize = IntegerField() 47 | 48 | 49 | class GetModelForm(BaseForm): 50 | """ 获取模块信息 """ 51 | id = IntegerField(validators=[DataRequired('模块id必传')]) 52 | 53 | def validate_id(self, field): 54 | module = Module.get_first(id=field.data) 55 | if not module: 56 | raise ValidationError(f'id为【{field.data}】的模块不存在') 57 | setattr(self, 'module', module) 58 | 59 | 60 | class ModuleIdForm(BaseForm): 61 | """ 返回待编辑模块信息 """ 62 | id = IntegerField(validators=[DataRequired('模块id必传')]) 63 | 64 | def validate_id(self, field): 65 | module = Module.get_first(id=field.data) 66 | if not module: 67 | raise ValidationError(f'id为【{field.data}】的模块不存在') 68 | setattr(self, 'module', module) 69 | 70 | 71 | class DeleteModelForm(ModuleIdForm): 72 | """ 删除模块 """ 73 | 74 | def validate_id(self, field): 75 | module = Module.get_first(id=field.data) 76 | if not module: 77 | raise ValidationError(f'id为【{field.data}】的模块不存在') 78 | if not Project.is_can_delete(module.project_id, module): 79 | raise ValidationError('不能删除别人服务下的模块') 80 | if module.apis: 81 | raise ValidationError('请先删除模块下的接口') 82 | if Module.get_first(parent=module.id): 83 | raise ValidationError('请先删除当前模块下的子模块') 84 | 85 | setattr(self, 'module', module) 86 | 87 | 88 | class EditModelForm(ModuleIdForm, AddModelForm): 89 | """ 修改模块的校验 """ 90 | 91 | def validate_id(self, field): 92 | """ 模块必须存在 """ 93 | old_module = Module.get_first(id=field.data) 94 | if not old_module: 95 | raise ValidationError(f'id为【{field.data}】的模块不存在') 96 | setattr(self, 'old_module', old_module) 97 | 98 | def validate_name(self, field): 99 | """ 同一个服务下,模块名不重复 """ 100 | old_module = Module.get_first( 101 | project_id=self.project_id.data, level=self.level.data, name=field.data, parent=self.parent.data 102 | ) 103 | if old_module and old_module.id != self.id.data: 104 | raise ValidationError(f'id为【{self.project_id.data}】的服务下已存在名为【{field.data}】的模块') 105 | 106 | 107 | class StickModuleForm(BaseForm): 108 | """ 置顶模块 """ 109 | project_id = IntegerField(validators=[DataRequired('服务id必传')]) 110 | id = IntegerField(validators=[DataRequired('模块id必传')]) 111 | -------------------------------------------------------------------------------- /app/api_test/module/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class Module(BaseModel): 12 | """ 模块表 """ 13 | __tablename__ = 'module' 14 | name = db.Column(db.String(255), nullable=True, comment='模块名') 15 | num = db.Column(db.Integer(), nullable=True, comment='模块在对应服务下的序号') 16 | level = db.Column(db.Integer(), nullable=True, default=2, comment='模块级数') 17 | parent = db.Column(db.Integer(), nullable=True, default=None, comment='上一级模块id') 18 | yapi_id = db.Column(db.Integer(), comment='当前模块在yapi平台对应的模块id') 19 | # yapi_project = db.Column(db.Integer(), comment='当前模块在yapi平台对应的服务id') 20 | # yapi_data = db.Column(db.Text, comment='当前模块在yapi平台的数据') 21 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), comment='所属的服务id') 22 | 23 | project = db.relationship('Project', backref='modules') # 一对多 24 | 25 | @classmethod 26 | def make_pagination(cls, form): 27 | """ 解析分页条件 """ 28 | filters = [] 29 | if form.projectId.data: 30 | filters.append(cls.project_id == form.projectId.data) 31 | if form.name.data: 32 | filters.append(cls.name.like(f'%{form.name.data}%')) 33 | return cls.pagination( 34 | page_num=form.pageNum.data, 35 | page_size=form.pageSize.data, 36 | filters=filters, 37 | order_by=cls.num.asc() 38 | ) 39 | -------------------------------------------------------------------------------- /app/api_test/module/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from flask import request 9 | 10 | from app.utils import restful 11 | from app.utils.required import login_required 12 | from app.api_test import api_test 13 | from app.baseView import BaseMethodView 14 | from .models import Module 15 | from .forms import AddModelForm, EditModelForm, FindModelForm, DeleteModelForm, GetModelForm 16 | 17 | 18 | @api_test.route('/module/list', methods=['GET']) 19 | @login_required 20 | def get_module_list(): 21 | """ 模块列表 """ 22 | form = FindModelForm() 23 | if form.validate(): 24 | return restful.get_success(data=Module.make_pagination(form)) 25 | return restful.fail(form.get_error()) 26 | 27 | 28 | @api_test.route('/module/tree', methods=['GET']) 29 | @login_required 30 | def module_tree(): 31 | """ 获取当前服务下的模块树 """ 32 | project_id = int(request.args.get('project_id')) 33 | module_list = [ 34 | module.to_dict() for module in Module.query.filter_by( 35 | project_id=project_id).order_by(Module.parent.asc()).all() 36 | ] 37 | return restful.success(data=module_list) 38 | 39 | 40 | class ModuleView(BaseMethodView): 41 | """ 模块管理 """ 42 | 43 | def get(self): 44 | form = GetModelForm() 45 | if form.validate(): 46 | return restful.get_success(data=form.module.to_dict()) 47 | return restful.fail(form.get_error()) 48 | 49 | def post(self): 50 | form = AddModelForm() 51 | if form.validate(): 52 | form.num.data = Module.get_insert_num(project_id=form.project_id.data) 53 | new_model = Module().create(form.data) 54 | setattr(new_model, 'children', []) 55 | return restful.success(f'名为【{form.name.data}】的模块创建成功', new_model.to_dict()) 56 | return restful.fail(form.get_error()) 57 | 58 | def put(self): 59 | form = EditModelForm() 60 | if form.validate(): 61 | form.old_module.update(form.data) 62 | return restful.success(f'模块【{form.name.data}】修改成功', form.old_module.to_dict()) 63 | return restful.fail(form.get_error()) 64 | 65 | def delete(self): 66 | form = DeleteModelForm() 67 | if form.validate(): 68 | form.module.delete() 69 | return restful.success(f'名为【{form.module.name}】的模块删除成功') 70 | return restful.fail(form.get_error()) 71 | 72 | 73 | api_test.add_url_rule('/module', view_func=ModuleView.as_view('module')) 74 | -------------------------------------------------------------------------------- /app/api_test/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/project/__init__.py -------------------------------------------------------------------------------- /app/api_test/project/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from flask_login import current_user 9 | 10 | from app.baseModel import BaseModel, db 11 | 12 | 13 | class Project(BaseModel): 14 | """ 服务表 """ 15 | __tablename__ = 'project' 16 | 17 | name = db.Column(db.String(255), nullable=True, comment='服务名称') 18 | manager = db.Column(db.Integer(), nullable=True, default=1, comment='服务管理员id,默认为admin') 19 | test = db.Column(db.String(255), default='', comment='测试环境域名') 20 | swagger = db.Column(db.String(255), default='', comment='服务对应的swagger地址') 21 | yapi_id = db.Column(db.Integer(), default=None, comment='对应YapiProject表里面的原始数据在yapi平台的id') 22 | 23 | def is_not_manager(self): 24 | """ 判断用户非服务负责人 """ 25 | return current_user.id != self.manager 26 | 27 | @classmethod 28 | def is_not_manager_id(cls, project_id): 29 | """ 判断当前用户非当前数据的负责人 """ 30 | return cls.get_first(id=project_id).manager != current_user.id 31 | 32 | @classmethod 33 | def is_manager_id(cls, project_id): 34 | """ 判断当前用户为当前数据的负责人 """ 35 | return cls.get_first(id=project_id).manager == current_user.id 36 | 37 | @classmethod 38 | def is_admin(cls): 39 | """ 角色为2,为管理员 """ 40 | return current_user.role_id == 2 41 | 42 | @classmethod 43 | def is_not_admin(cls): 44 | """ 角色不为2,非管理员 """ 45 | return not cls.is_admin() 46 | 47 | @classmethod 48 | def is_can_delete(cls, project_id, obj): 49 | """ 50 | 判断是否有权限删除, 51 | 可删除条件(或): 52 | 1.当前用户为系统管理员 53 | 2.当前用户为当前数据的创建者 54 | 3.当前用户为当前要删除服务的负责人 55 | """ 56 | return Project.is_manager_id(project_id) or cls.is_admin() or obj.is_create_user(current_user.id) 57 | 58 | @classmethod 59 | def make_pagination(cls, form): 60 | """ 解析分页条件 """ 61 | filters = [] 62 | if form.name.data: 63 | filters.append(Project.name.like(f'%{form.name.data}%')) 64 | if form.projectId.data: 65 | filters.append(Project.id == form.projectId.data) 66 | if form.manager.data: 67 | filters.append(Project.manager == form.manager.data) 68 | if form.create_user.data: 69 | filters.append(Project.create_user == form.create_user.data) 70 | return cls.pagination( 71 | page_num=form.pageNum.data, 72 | page_size=form.pageSize.data, 73 | filters=filters, 74 | order_by=cls.created_time.desc()) 75 | 76 | def create_env(self, env_list=None): 77 | if env_list is None: 78 | env_list = ['dev', 'test', 'uat', 'production'] 79 | for env in env_list: 80 | ProjectEnv().create({"env": env, "project_id": self.id}) 81 | 82 | 83 | class ProjectEnv(BaseModel): 84 | """ 服务环境表 """ 85 | __tablename__ = 'project_env' 86 | 87 | env = db.Column(db.String(10), nullable=True, comment='所属环境') 88 | host = db.Column(db.String(255), default='', comment='域名') 89 | func_files = db.Column(db.Text(), nullable=True, default='[]', comment='引用的函数文件') 90 | variables = db.Column(db.Text(), default='[{"key": "", "value": "", "remark": ""}]', comment='服务的公共变量') 91 | headers = db.Column(db.Text(), default='[{"key": "", "value": "", "remark": ""}]', comment='服务的公共头部信息') 92 | project_id = db.Column(db.Integer(), nullable=True, comment='所属的服务id') 93 | 94 | def to_dict(self, *args, **kwargs): 95 | """ 自定义序列化器,把模型的每个字段转为字典,方便返回给前端 """ 96 | return super(ProjectEnv, self).to_dict(to_dict=["variables", "headers", "func_files"]) 97 | -------------------------------------------------------------------------------- /app/api_test/project/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from app.utils import restful 9 | from app.utils.parse import parse_list_to_dict, parse_dict_to_list 10 | from app.utils.required import login_required 11 | from app.api_test import api_test 12 | from app.baseView import BaseMethodView 13 | from .models import Project, ProjectEnv 14 | from .forms import ( 15 | AddProjectForm, EditProjectForm, FindProjectForm, DeleteProjectForm, GetProjectByIdForm, 16 | EditEnv, AddEnv, FindEnvForm, SynchronizationEnvForm 17 | ) 18 | 19 | 20 | @api_test.route('/project/all', methods=['GET']) 21 | @login_required 22 | def project_all(): 23 | """ 所有服务列表 """ 24 | return restful.success(data=[project.to_dict() for project in Project.get_all()]) 25 | 26 | 27 | @api_test.route('/project/list', methods=['GET']) 28 | @login_required 29 | def project_list(): 30 | """ 查找服务列表 """ 31 | form = FindProjectForm() 32 | if form.validate(): 33 | return restful.success(data=Project.make_pagination(form)) 34 | return restful.fail(form.get_error()) 35 | 36 | 37 | class ProjectView(BaseMethodView): 38 | """ 服务管理 """ 39 | 40 | def get(self): 41 | """ 获取服务 """ 42 | form = GetProjectByIdForm() 43 | if form.validate(): 44 | return restful.success(data=form.project.to_dict()) 45 | return restful.fail(form.get_error()) 46 | 47 | def post(self): 48 | """ 新增服务 """ 49 | form = AddProjectForm() 50 | if form.validate(): 51 | project = Project().create(form.data, 'variables', 'headers', 'func_files') 52 | project.create_env() # 新增服务的时候,一并把环境设置齐全 53 | return restful.success(f'服务【{form.name.data}】新建成功', project.to_dict()) 54 | return restful.fail(msg=form.get_error()) 55 | 56 | def put(self): 57 | """ 修改服务 """ 58 | form = EditProjectForm() 59 | if form.validate(): 60 | form.project.update(form.data, 'variables', 'headers', 'func_files') 61 | return restful.success(f'服务【{form.name.data}】修改成功', form.project.to_dict()) 62 | return restful.fail(msg=form.get_error()) 63 | 64 | def delete(self): 65 | """ 删除服务 """ 66 | form = DeleteProjectForm() 67 | if form.validate(): 68 | form.project.delete() 69 | # 删除服务的时候把环境也删掉 70 | for env in ProjectEnv.get_all(project_id=form.project.id): 71 | env.delete() 72 | return restful.success(msg=f'服务【{form.project.name}】删除成功') 73 | return restful.fail(form.get_error()) 74 | 75 | 76 | @api_test.route('/project/env/synchronization', methods=['POST']) 77 | @login_required 78 | def project_env_synchronization(): 79 | """ 同步环境数据 """ 80 | form = SynchronizationEnvForm() 81 | if form.validate(): 82 | from_env = ProjectEnv.get_first(project_id=form.projectId.data, env=form.envFrom.data) 83 | from_env_variable = parse_list_to_dict(form.loads(from_env.variables)) 84 | from_env_headers = parse_list_to_dict(form.loads(from_env.headers)) 85 | from_env_func_files = form.loads(from_env.func_files) 86 | synchronization_result = {} 87 | for to_env in form.envTo.data: 88 | to_env_data = ProjectEnv.get_first(project_id=form.projectId.data, env=to_env) 89 | 90 | # 变量 91 | variables = parse_list_to_dict(to_env_data.loads(to_env_data.variables)) 92 | for key, value in from_env_variable.items(): 93 | variables.setdefault(key, value) 94 | 95 | # 头部信息 96 | headers = parse_list_to_dict(to_env_data.loads(to_env_data.headers)) 97 | for key, value in from_env_headers.items(): 98 | headers.setdefault(key, value) 99 | 100 | # 函数文件 101 | func_files = to_env_data.loads(to_env_data.func_files) 102 | func_files.extend(from_env_func_files) 103 | 104 | to_env_data.update({ 105 | 'variables': form.dumps(parse_dict_to_list(variables)), 106 | 'headers': form.dumps(parse_dict_to_list(headers)), 107 | 'func_files': form.dumps(func_files), 108 | }) 109 | synchronization_result[to_env] = to_env_data.to_dict() 110 | return restful.success('同步成功', data=synchronization_result) 111 | return restful.fail(form.get_error()) 112 | 113 | 114 | class ProjectEnvView(BaseMethodView): 115 | """ 服务环境管理 """ 116 | 117 | def get(self): 118 | """ 获取服务环境 """ 119 | form = FindEnvForm() 120 | if form.validate(): 121 | return restful.success(data=form.env_data.to_dict()) 122 | return restful.fail(form.get_error()) 123 | 124 | def post(self): 125 | """ 新增服务环境 """ 126 | form = AddEnv() 127 | if form.validate(): 128 | env = ProjectEnv().create(form.data, 'variables', 'headers', 'func_files') 129 | return restful.success(f'环境新建成功', env.to_dict()) 130 | return restful.fail(msg=form.get_error()) 131 | 132 | def put(self): 133 | """ 修改服务环境 """ 134 | form = EditEnv() 135 | if form.validate(): 136 | form.env_data.update(form.data, 'variables', 'headers', 'func_files') 137 | # 修改环境的时候,如果是测试环境,一并把服务的测试环境地址更新 138 | if form.env_data.env == 'test': 139 | form.project.update({'test': form.env_data.host}) 140 | return restful.success(f'环境修改成功', form.env_data.to_dict()) 141 | return restful.fail(msg=form.get_error()) 142 | 143 | 144 | api_test.add_url_rule('/project', view_func=ProjectView.as_view('project')) 145 | api_test.add_url_rule('/project/env', view_func=ProjectEnvView.as_view('project_env')) 146 | -------------------------------------------------------------------------------- /app/api_test/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/report/__init__.py -------------------------------------------------------------------------------- /app/api_test/report/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | import json 9 | import os 10 | 11 | from wtforms import IntegerField 12 | from wtforms.validators import ValidationError, DataRequired 13 | 14 | from app.utils.globalVariable import REPORT_ADDRESS 15 | from app.baseForm import BaseForm 16 | from ..report.models import Report 17 | 18 | 19 | class DownloadReportForm(BaseForm): 20 | """ 报告下载 """ 21 | id = IntegerField(validators=[DataRequired('请选择报告')]) 22 | 23 | def validate_id(self, field): 24 | """ 校验报告是否存在 """ 25 | report = Report.get_first(id=field.data) 26 | if not report: 27 | raise ValidationError('报告还未生成, 请联系管理员处理') 28 | report_path = os.path.join(REPORT_ADDRESS, f'{report.id}.txt') 29 | if not os.path.exists(report_path): 30 | raise ValidationError('报告文件不存在, 可能是未生成,请联系管理员处理') 31 | with open(report_path, 'r') as file: 32 | report_content = json.load(file) 33 | setattr(self, 'report', report) 34 | setattr(self, 'report_path', report_path) 35 | setattr(self, 'report_content', report_content) 36 | 37 | 38 | class GetReportForm(DownloadReportForm): 39 | """ 查看报告 """ 40 | 41 | 42 | class DeleteReportForm(BaseForm): 43 | """ 删除报告 """ 44 | id = IntegerField(validators=[DataRequired('请选择报告')]) 45 | 46 | def validate_id(self, field): 47 | report = Report.get_first(id=field.data) 48 | if not report: 49 | raise ValidationError('报告不存在') 50 | report_path = os.path.join(REPORT_ADDRESS, f'{report.id}.txt') 51 | if not report_path: 52 | raise ValidationError('报告文件不存在, 请联系管理员处理') 53 | setattr(self, 'report', report) 54 | setattr(self, 'report_path', report_path) 55 | 56 | 57 | class FindReportForm(BaseForm): 58 | """ 查找报告 """ 59 | projectId = IntegerField(validators=[DataRequired('请选择服务')]) 60 | pageNum = IntegerField() 61 | pageSize = IntegerField() 62 | -------------------------------------------------------------------------------- /app/api_test/report/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from sqlalchemy.dialects.mysql import TEXT 9 | 10 | from app.baseModel import BaseModel, db 11 | 12 | 13 | class Report(BaseModel): 14 | """ 测试报告表 """ 15 | __tablename__ = 'report' 16 | name = db.Column(TEXT, nullable=True, comment='用例的名称集合') 17 | status = db.Column(db.String(10), nullable=True, default='未读', comment='阅读状态,已读、未读') 18 | is_passed = db.Column(db.Integer, default=1, comment='是否全部通过,1全部通过,0有报错') 19 | performer = db.Column(db.String(16), nullable=True, comment='执行者') 20 | run_type = db.Column(db.String(10), default='task', nullable=True, comment='报告类型,task/case/api') 21 | is_done = db.Column(db.Integer, default=0, comment='是否执行完毕,1执行完毕,0执行中') 22 | 23 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), comment='所属的服务id') 24 | project = db.relationship('Project', backref='reports') 25 | 26 | @classmethod 27 | def get_new_report(cls, name, run_type, performer, create_user, project_id): 28 | with db.auto_commit(): 29 | report = Report() 30 | report.name = name 31 | report.run_type = run_type 32 | report.performer = performer 33 | report.create_user = create_user 34 | report.project_id = project_id 35 | db.session.add(report) 36 | return report 37 | 38 | @classmethod 39 | def make_pagination(cls, form): 40 | """ 解析分页条件 """ 41 | filters = [] 42 | if form.projectId.data: 43 | filters.append(cls.project_id == form.projectId.data) 44 | return cls.pagination( 45 | page_num=form.pageNum.data, 46 | page_size=form.pageSize.data, 47 | filters=filters, 48 | order_by=cls.created_time.desc() 49 | ) 50 | -------------------------------------------------------------------------------- /app/api_test/report/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | import os 9 | 10 | from flask import request 11 | 12 | from app.utils.report.report import render_html_report 13 | from app.utils import restful 14 | from app.utils.required import login_required 15 | from app.api_test import api_test 16 | from app.baseModel import db 17 | from .models import Report 18 | from .forms import GetReportForm, DownloadReportForm, DeleteReportForm, FindReportForm 19 | 20 | 21 | @api_test.route('/report/download', methods=['GET']) 22 | @login_required 23 | def download_report(): 24 | """ 报告下载 """ 25 | form = DownloadReportForm() 26 | if form.validate(): 27 | return restful.success(data=render_html_report(form.report_content)) 28 | return restful.fail(form.get_error()) 29 | 30 | 31 | @api_test.route('/report/list', methods=['GET']) 32 | @login_required 33 | def report_list(): 34 | """ 报告列表 """ 35 | form = FindReportForm() 36 | if form.validate(): 37 | return restful.success(data=Report.make_pagination(form)) 38 | return restful.fail(form.get_error()) 39 | 40 | 41 | @api_test.route('/report/done', methods=['GET']) 42 | @login_required 43 | def report_is_done(): 44 | """ 报告是否生成 """ 45 | return restful.success(data=Report.get_first(id=request.args.to_dict().get('id')).is_done) 46 | 47 | 48 | @api_test.route('/report', methods=['GET']) 49 | def get_report(): 50 | """ 获取测试报告 """ 51 | form = GetReportForm() 52 | if form.validate(): 53 | with db.auto_commit(): 54 | form.report.status = '已读' 55 | return restful.success('获取成功', data=form.report_content) 56 | return restful.fail(form.get_error()) 57 | 58 | 59 | @api_test.route('/report', methods=['DELETE']) 60 | @login_required 61 | def delete_report(): 62 | """ 删除测试报告 """ 63 | form = DeleteReportForm() 64 | if form.validate(): 65 | form.report.delete() 66 | if os.path.exists(form.report_path): 67 | os.remove(form.report_path) 68 | return restful.success('删除成功') 69 | return restful.fail(form.get_error()) 70 | 71 | # class ReportView(BaseMethodView): 72 | # """ 报告管理 """ 73 | # 74 | # def get(self): 75 | # form = GetReportForm() 76 | # if form.validate(): 77 | # with db.auto_commit(): 78 | # form.report.status = '已读' 79 | # return restful.success('获取成功', data=form.report_content) 80 | # return restful.fail(form.get_error()) 81 | # 82 | # def delete(self): 83 | # form = DeleteReportForm() 84 | # if form.validate(): 85 | # form.report.delete() 86 | # if os.path.exists(form.report_path): 87 | # os.remove(form.report_path) 88 | # return restful.success('删除成功') 89 | # return restful.fail(form.get_error()) 90 | # 91 | # 92 | # api.add_url_rule('/report', view_func=ReportView.as_view('report')) 93 | -------------------------------------------------------------------------------- /app/api_test/sets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/sets/__init__.py -------------------------------------------------------------------------------- /app/api_test/sets/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | from wtforms.validators import ValidationError, Length, DataRequired 10 | 11 | from app.baseForm import BaseForm 12 | from ..case.models import Case 13 | from ..project.models import Project 14 | from .models import Set 15 | 16 | 17 | class GetCaseSetForm(BaseForm): 18 | """ 获取用例集信息 """ 19 | id = IntegerField(validators=[DataRequired('用例集id必传')]) 20 | 21 | def validate_id(self, field): 22 | set = Set.get_first(id=field.data) 23 | if not set: 24 | raise ValidationError(f'id为【{field.data}】的模块不存在') 25 | setattr(self, 'set', set) 26 | 27 | 28 | class AddCaseSetForm(BaseForm): 29 | """ 添加用例集的校验 """ 30 | project_id = StringField(validators=[DataRequired('请先选择首页服务')]) 31 | name = StringField(validators=[DataRequired('用例集名称不能为空'), Length(1, 255, message='用例集名长度为1~255位')]) 32 | level = StringField() 33 | parent = StringField() 34 | id = StringField() 35 | num = StringField() 36 | 37 | def validate_project_id(self, field): 38 | """ 服务id合法 """ 39 | project = Project.get_first(id=field.data) 40 | if not project: 41 | raise ValidationError(f'id为【{field.data}】的服务不存在,请先创建') 42 | setattr(self, 'project', project) 43 | 44 | def validate_name(self, field): 45 | """ 校验用例集名不重复 """ 46 | if Set.get_first(project_id=self.project_id.data, 47 | level=self.level.data, 48 | name=field.data, 49 | parent=self.parent.data): 50 | raise ValidationError(f'用例集名字【{field.data}】已存在') 51 | 52 | 53 | class GetCaseSetEditForm(BaseForm): 54 | """ 返回待编辑用例集合 """ 55 | id = IntegerField(validators=[DataRequired('用例集id必传')]) 56 | 57 | def validate_id(self, field): 58 | set = Set.get_first(id=field.data) 59 | if not set: 60 | raise ValidationError('没有此用例集') 61 | setattr(self, 'set', set) 62 | 63 | 64 | class DeleteCaseSetForm(GetCaseSetEditForm): 65 | """ 删除用例集 """ 66 | 67 | def validate_id(self, field): 68 | case_set = Set.get_first(id=field.data) 69 | # 数据权限 70 | if not Project.is_can_delete(case_set.project_id, case_set): 71 | raise ValidationError('不能删除别人服务下的用例集') 72 | # 用例集下是否有用例集 73 | if Set.get_first(parent=field.data): 74 | raise ValidationError('请先删除当前用例集下的用例集') 75 | # 用例集下是否有用例 76 | if Case.get_first(set_id=field.data): 77 | raise ValidationError('请先删除当前用例集下的用例') 78 | setattr(self, 'case_set', case_set) 79 | 80 | 81 | class EditCaseSetForm(GetCaseSetEditForm, AddCaseSetForm): 82 | """ 编辑用例集 """ 83 | 84 | def validate_id(self, field): 85 | """ 用例集id已存在 """ 86 | case_set = Set.get_first(id=field.data) 87 | if not case_set: 88 | raise ValidationError(f'不存在id为【{field.data}】的用例集') 89 | setattr(self, 'case_set', case_set) 90 | 91 | def validate_name(self, field): 92 | """ 校验用例集名不重复 """ 93 | old_set = Set.get_first(project_id=self.project_id.data, level=self.level.data, name=field.data, parent=self.parent.data) 94 | if old_set and old_set.id != self.id.data: 95 | raise ValidationError(f'用例集名字【{field.data}】已存在') 96 | 97 | 98 | class FindCaseSet(BaseForm): 99 | """ 查找用例集合 """ 100 | pageNum = IntegerField() 101 | pageSize = IntegerField() 102 | name = StringField() 103 | projectId = IntegerField(validators=[DataRequired('服务id必传')]) 104 | 105 | def validate_projectId(self, field): 106 | project = Project.get_first(id=field.data) 107 | if not project: 108 | raise ValidationError(f'id为【{field.data}】的服务不存在') 109 | setattr(self, 'all_sets', project.sets) 110 | -------------------------------------------------------------------------------- /app/api_test/sets/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from ..case.models import Case 9 | from app.baseModel import BaseModel, db 10 | 11 | 12 | class Set(BaseModel): 13 | """ 用例集表 """ 14 | __tablename__ = 'sets' 15 | 16 | name = db.Column(db.String(255), nullable=True, comment='用例集名称') 17 | num = db.Column(db.Integer(), nullable=True, comment='用例集在对应服务下的序号') 18 | level = db.Column(db.Integer(), nullable=True, default=2, comment='用例集级数') 19 | parent = db.Column(db.Integer(), nullable=True, default=None, comment='上一级用例集id') 20 | yapi_id = db.Column(db.Integer(), comment='当前用例集在yapi平台对应的服务id') 21 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), comment='所属的服务id') 22 | 23 | project = db.relationship('Project', backref='sets') # 一对多 24 | cases = db.relationship('Case', order_by='Case.num.asc()', lazy='dynamic') 25 | 26 | @classmethod 27 | def get_case_id(cls, project_id: int, set_id: list, case_id: list): 28 | """ 29 | 获取要执行的用例的id 30 | 1.如果有用例id,则只拿对应的用例 31 | 2.如果没有用例id,有模块id,则拿模块下的所有用例id 32 | 3.如果没有用例id,也没有用模块id,则拿服务下所有模块下的所有用例 33 | """ 34 | if len(case_id) != 0: 35 | return case_id 36 | elif len(set_id) != 0: 37 | set_ids = set_id 38 | else: 39 | set_ids = [ 40 | set.id for set in cls.query.filter_by(project_id=project_id).order_by(Set.num.asc()).all() 41 | ] 42 | case_ids = [ 43 | case.id for set_id in set_ids for case in Case.query.filter_by( 44 | set_id=set_id, 45 | is_run=1 46 | ).order_by(Case.num.asc()).all() if case and case.is_run 47 | ] 48 | return case_ids 49 | 50 | @classmethod 51 | def make_pagination(cls, form): 52 | """ 解析分页条件 """ 53 | filters = [] 54 | if form.projectId.data: 55 | filters.append(cls.project_id == form.projectId.data) 56 | if form.name.data: 57 | filters.append(cls.name == form.name.data) 58 | return cls.pagination( 59 | page_num=form.pageNum.data, 60 | page_size=form.pageSize.data, 61 | filters=filters, 62 | order_by=cls.num.asc() 63 | ) 64 | -------------------------------------------------------------------------------- /app/api_test/sets/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from threading import Thread 9 | 10 | from flask import request 11 | from flask_login import current_user 12 | 13 | from ..case.models import Case 14 | from ..report.models import Report 15 | from app.utils import restful 16 | from app.utils.required import login_required 17 | from app.utils.runHttpRunner import RunCase 18 | from app.api_test import api_test 19 | from app.baseView import BaseMethodView 20 | from .models import Set 21 | from .forms import AddCaseSetForm, EditCaseSetForm, FindCaseSet, GetCaseSetEditForm, DeleteCaseSetForm, GetCaseSetForm 22 | 23 | 24 | @api_test.route('/caseSet/list', methods=['GET']) 25 | @login_required 26 | def get_set_list(): 27 | """ 用例集list """ 28 | form = FindCaseSet() 29 | if form.validate(): 30 | return restful.success(data=Set.make_pagination(form)) 31 | return restful.fail(form.get_error()) 32 | 33 | 34 | @api_test.route('/caseSet/run', methods=['POST']) 35 | @login_required 36 | def run_case_set(): 37 | """ 运行用例集下的用例 """ 38 | form = GetCaseSetForm() 39 | if form.validate(): 40 | project_id = form.set.project_id 41 | report = Report.get_new_report(form.set.name, 'set', current_user.name, current_user.id, project_id) 42 | 43 | # 新起线程运行任务 44 | Thread( 45 | target=RunCase( 46 | project_id=project_id, 47 | run_name=report.name, 48 | case_id=[case.id for case in Case.query.filter_by(set_id=form.set.id).order_by(Case.num.asc()).all() 49 | if case.is_run], 50 | report_id=report.id 51 | ).run_case 52 | ).start() 53 | return restful.success(msg='触发执行成功,请等待执行完毕', data={'report_id': report.id}) 54 | return restful.fail(form.get_error()) 55 | 56 | 57 | @api_test.route('/caseSet/tree', methods=['GET']) 58 | @login_required 59 | def case_set_tree(): 60 | """ 获取当前服务下的用例集树 """ 61 | set_list = [ 62 | case_set.to_dict() for case_set in Set.query.filter_by( 63 | project_id=int(request.args.get('project_id'))).order_by(Set.parent.asc()).all() 64 | ] 65 | return restful.success(data=set_list) 66 | 67 | 68 | class CaseSetView(BaseMethodView): 69 | """ 用例集管理 """ 70 | 71 | def get(self): 72 | form = GetCaseSetEditForm() 73 | if form.validate(): 74 | return restful.success(data={'name': form.set.name, 'num': form.set.num}) 75 | return restful.fail(form.get_error()) 76 | 77 | def post(self): 78 | form = AddCaseSetForm() 79 | if form.validate(): 80 | form.num.data = Set.get_insert_num(project_id=form.project_id.data) 81 | new_set = Set().create(form.data) 82 | return restful.success(f'名为【{form.name.data}】的用例集创建成功', new_set.to_dict()) 83 | return restful.fail(form.get_error()) 84 | 85 | def put(self): 86 | form = EditCaseSetForm() 87 | if form.validate(): 88 | form.case_set.update(form.data) 89 | return restful.success(f'用例集【{form.name.data}】修改成功', form.case_set.to_dict()) 90 | return restful.fail(form.get_error()) 91 | 92 | def delete(self): 93 | form = DeleteCaseSetForm() 94 | if form.validate(): 95 | form.case_set.delete() 96 | return restful.success('删除成功') 97 | return restful.fail(form.get_error()) 98 | 99 | 100 | api_test.add_url_rule('/caseSet', view_func=CaseSetView.as_view('caseSet')) 101 | -------------------------------------------------------------------------------- /app/api_test/step/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/step/__init__.py -------------------------------------------------------------------------------- /app/api_test/step/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/4/16 9:42 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | from wtforms.validators import ValidationError, DataRequired, Length 10 | 11 | from ..apiMsg.models import ApiMsg 12 | from ..case.models import Case 13 | from app.baseForm import BaseForm 14 | from ..func.models import Func 15 | from ..project.models import Project, ProjectEnv 16 | from .models import Step 17 | 18 | 19 | class GetStepListForm(BaseForm): 20 | """ 根据用例id获取步骤列表 """ 21 | caseId = IntegerField(validators=[DataRequired('用例id必传')]) 22 | 23 | def validate_caseId(self, field): 24 | case = Case.get_first(id=field.data) 25 | if not case: 26 | raise ValidationError(f'id为 {field.data} 的用例不存在') 27 | setattr(self, 'case', case) 28 | 29 | 30 | class GetStepForm(BaseForm): 31 | """ 根据步骤id获取步骤 """ 32 | id = IntegerField(validators=[DataRequired('步骤id必传')]) 33 | 34 | def validate_id(self, field): 35 | step = Step.get_first(id=field.data) 36 | if not step: 37 | raise ValidationError(f'id为 {field.data} 的步骤不存在') 38 | setattr(self, 'step', step) 39 | 40 | 41 | class AddStepForm(BaseForm): 42 | """ 添加步骤校验 """ 43 | project_id = IntegerField() 44 | case_id = IntegerField(validators=[DataRequired('用例id必传')]) 45 | api_id = IntegerField() 46 | quote_case = IntegerField() 47 | 48 | name = StringField(validators=[DataRequired('步骤名称不能为空'), Length(1, 255, message='步骤名长度为1~255位')]) 49 | up_func = StringField() 50 | down_func = StringField() 51 | is_run = IntegerField() 52 | run_times = IntegerField() 53 | headers = StringField() 54 | params = StringField() 55 | data_form = StringField() 56 | data_json = StringField() 57 | data_xml = StringField() 58 | extracts = StringField() 59 | validates = StringField() 60 | data_driver = StringField() 61 | num = StringField() 62 | 63 | def validate_project_id(self, field): 64 | """ 校验服务id """ 65 | if not self.quote_case.data: 66 | project = Project.get_first(id=field.data) 67 | if not project: 68 | raise ValidationError(f'id为【{field.data}】的服务不存在') 69 | setattr(self, 'project', project) 70 | 71 | def validate_case_id(self, field): 72 | """ 校验用例存在 """ 73 | case = Case.get_first(id=field.data) 74 | if not case: 75 | raise ValidationError(f'id为【{field.data}】的用例不存在') 76 | setattr(self, 'case', case) 77 | 78 | def validate_api_id(self, field): 79 | """ 校验接口存在 """ 80 | if not self.quote_case.data: 81 | if not ApiMsg.get_first(id=field.data): 82 | raise ValidationError(f'id为【{field.data}】的接口不存在') 83 | 84 | def validate_quote_case(self, field): 85 | """ 不能自己引用自己 """ 86 | if field.data and field.data == self.case_id: 87 | raise ValidationError(f'不能自己引用自己') 88 | 89 | def validate_extracts(self, field): 90 | """ 校验数据提取信息 """ 91 | if not self.quote_case.data: 92 | self.validate_base_extracts(field.data) 93 | 94 | def validate_validates(self, field): 95 | """ 校验断言信息 """ 96 | if not self.quote_case.data: 97 | func_files = self.loads( 98 | ProjectEnv.get_first(project_id=self.project.id, env=self.case.choice_host).func_files) 99 | func_container = Func.get_func_by_func_file_name(func_files) 100 | self.validate_base_validates(field.data, func_container) 101 | 102 | 103 | class EditStepForm(AddStepForm): 104 | """ 修改步骤校验 """ 105 | id = IntegerField(validators=[DataRequired('用例id必传')]) 106 | 107 | def validate_id(self, field): 108 | """ 校验步骤id已存在 """ 109 | step = Step.get_first(id=field.data) 110 | if not step: 111 | raise ValidationError(f'id为【{field.data}】的步骤不存在') 112 | setattr(self, 'step', step) 113 | -------------------------------------------------------------------------------- /app/api_test/step/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/4/16 9:42 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class Step(BaseModel): 12 | """ 用例数据表 """ 13 | __tablename__ = 'step' 14 | num = db.Column(db.Integer(), nullable=True, comment='步骤序号,执行顺序按序号来') 15 | is_run = db.Column(db.Boolean(), default=True, comment='是否执行此步骤,True执行,False不执行,默认执行') 16 | run_times = db.Column(db.Integer(), default=1, comment='执行次数,默认执行1次') 17 | replace_host = db.Column(db.Boolean(), default=False, comment='是否使用用例所在项目的域名,True使用用例所在服务的域名,False使用步骤对应接口所在服务的域名') 18 | 19 | name = db.Column(db.String(255), comment='步骤名称') 20 | up_func = db.Column(db.Text(), default='', comment='步骤执行前的函数') 21 | down_func = db.Column(db.Text(), default='', comment='步骤执行后的函数') 22 | headers = db.Column(db.Text(), default='[{"key": null, "remark": null, "value": null}]', comment='头部信息') 23 | params = db.Column(db.Text(), default='[{"key": null, "value": null}]', comment='url参数') 24 | data_form = db.Column(db.Text(), 25 | default='[{"data_type": null, "key": null, "remark": null, "value": null}]', 26 | comment='form-data参数') 27 | data_json = db.Column(db.Text(), default='{}', comment='json参数') 28 | data_xml = db.Column(db.Text(), default='', comment='xml参数') 29 | extracts = db.Column( 30 | db.Text(), 31 | default='[{"key": null, "data_source": null, "value": null, "remark": null}]', 32 | comment='提取信息' 33 | ) 34 | validates = db.Column( 35 | db.Text(), 36 | default='[{"data_source": null, "key": null, "validate_type": null, "data_type": null, "value": null, "remark": null}]', 37 | comment='断言信息') 38 | data_driver = db.Column(db.Text(), default='[]', comment='数据驱动,若此字段有值,则走数据驱动的解析') 39 | quote_case = db.Column(db.String(5), default='', comment='引用用例的id') 40 | 41 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), comment='步骤所在的服务的id') 42 | project = db.relationship('Project', backref='steps') 43 | 44 | case_id = db.Column(db.Integer, db.ForeignKey('case.id'), comment='步骤所在的用例的id') 45 | 46 | api_id = db.Column(db.Integer, db.ForeignKey('apis.id'), comment='步骤所引用的接口的id') 47 | api = db.relationship('ApiMsg', backref='apis') 48 | 49 | def to_dict(self, *args, **kwargs): 50 | return super(Step, self).to_dict( 51 | to_dict=["headers", "params", "data_form", "data_json", "extracts", "validates", "data_driver"] 52 | ) 53 | -------------------------------------------------------------------------------- /app/api_test/step/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/4/16 9:42 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from flask import request 9 | 10 | from app.api_test import api_test 11 | from app.utils import restful 12 | from app.utils.required import login_required 13 | from app.baseView import BaseMethodView 14 | from app.baseModel import db 15 | from .models import Step 16 | from .forms import GetStepListForm, GetStepForm, AddStepForm, EditStepForm 17 | 18 | 19 | @api_test.route('/step/list', methods=['GET']) 20 | @login_required 21 | def get_step_list(): 22 | """ 根据用例id获取步骤列表 """ 23 | form = GetStepListForm() 24 | if form.validate(): 25 | step_obj_list = Step.query.filter_by(case_id=form.caseId.data).order_by(Step.num.asc()).all() 26 | return restful.success('获取成功', data=[step.to_dict() for step in step_obj_list]) 27 | return restful.error(form.get_error()) 28 | 29 | 30 | @api_test.route('/step/changeIsRun', methods=['PUT']) 31 | @login_required 32 | def change_step_status(): 33 | """ 修改步骤状态(是否执行) """ 34 | with db.auto_commit(): 35 | Step.get_first(id=request.json.get('id')).is_run = request.json.get('is_run') 36 | return restful.success(f'步骤已修改为 {"执行" if request.json.get("is_run") else "不执行"}') 37 | 38 | 39 | @api_test.route('/step/changeHost', methods=['PUT']) 40 | @login_required 41 | def change_step_host(): 42 | """ 修改步骤引用的host """ 43 | step = Step.get_first(id=request.json.get('id')) 44 | with db.auto_commit(): 45 | step.replace_host = request.json.get('replace_host') 46 | return restful.success( 47 | f'步骤已修改为 {"使用【用例】所在服务的host" if request.json.get("replace_host") else "使用【接口】所在服务的host"}', 48 | data=step.to_dict() 49 | ) 50 | 51 | 52 | @api_test.route('/step/sort', methods=['PUT']) 53 | @login_required 54 | def change_step_sort(): 55 | """ 更新步骤的排序 """ 56 | Step.change_sort(request.json.get('List'), request.json.get('pageNum', 0), request.json.get('pageSize', 0)) 57 | return restful.success(msg='修改排序成功') 58 | 59 | 60 | @api_test.route('/step/copy', methods=['POST']) 61 | @login_required 62 | def copy_step(): 63 | """ 复制步骤 """ 64 | old = Step.get_first(id=request.json.get('id')).to_dict() 65 | old['name'] = f"{old['name']}_copy" 66 | old['num'] = Step.get_insert_num(case_id=old['case_id']) 67 | step = Step().create(old, "headers", "params", "data_form", "data_json", "extracts", "validates", "data_driver") 68 | return restful.success(msg='步骤复制成功', data=step.to_dict()) 69 | 70 | 71 | class StepMethodView(BaseMethodView): 72 | 73 | def get(self): 74 | """ 获取步骤 """ 75 | form = GetStepForm() 76 | if form.validate(): 77 | return restful.success('获取成功', data=form.step.to_dict()) 78 | return restful.error(form.get_error()) 79 | 80 | def post(self): 81 | """ 新增步骤 """ 82 | form = AddStepForm() 83 | if form.validate(): 84 | form.num.data = Step.get_insert_num(case_id=form.case_id.data) 85 | step = Step().create( 86 | form.data, 'headers', 'params', 'data_form', 'data_json', 'extracts', 'validates', 'data_driver') 87 | return restful.success(f'步骤【{step.name}】新建成功', data=step.to_dict()) 88 | return restful.error(form.get_error()) 89 | 90 | def put(self): 91 | """ 修改步骤 """ 92 | form = EditStepForm() 93 | if form.validate(): 94 | form.step.update( 95 | form.data, 'headers', 'params', 'data_form', 'data_json', 'extracts', 'validates', 'data_driver' 96 | ) 97 | return restful.success(msg=f'步骤【{form.step.name}】修改成功', data=form.step.to_dict()) 98 | return restful.fail(form.get_error()) 99 | 100 | def delete(self): 101 | """ 删除步骤 """ 102 | form = GetStepForm() 103 | if form.validate(): 104 | form.step.delete() 105 | return restful.success(f'步骤【{form.step.name}】删除成功') 106 | return restful.error(form.get_error()) 107 | 108 | 109 | api_test.add_url_rule('/step', view_func=StepMethodView.as_view('step')) 110 | -------------------------------------------------------------------------------- /app/api_test/task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/api_test/task/__init__.py -------------------------------------------------------------------------------- /app/api_test/task/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class Task(BaseModel): 12 | """ 测试任务表 """ 13 | __tablename__ = 'tasks' 14 | num = db.Column(db.Integer(), comment='任务序号') 15 | name = db.Column(db.String(255), comment='任务名称') 16 | choice_host = db.Column(db.String(10), default='test', comment='运行环境') 17 | case_id = db.Column(db.Text(), comment='用例id') 18 | task_type = db.Column(db.String(10), default='cron', comment='定时类型') 19 | cron = db.Column(db.String(255), nullable=True, comment='cron表达式') 20 | is_send = db.Column(db.String(10), comment='是否发送报告,1.不发送、2.始终发送、3.仅用例不通过时发送') 21 | send_type = db.Column(db.String(10), default='webhook', comment='测试报告发送类型,webhook,email,all') 22 | we_chat = db.Column(db.Text(), comment='企业微信机器人地址') 23 | ding_ding = db.Column(db.Text(), comment='钉钉机器人地址') 24 | email_server = db.Column(db.String(255), comment='发件邮箱服务器') 25 | email_from = db.Column(db.String(255), comment='发件人邮箱') 26 | email_pwd = db.Column(db.String(255), comment='发件人邮箱密码') 27 | email_to = db.Column(db.Text(), comment='收件人邮箱') 28 | status = db.Column(db.String(10), default=u'禁用中', comment='任务的运行状态,默认是禁用中') 29 | set_id = db.Column(db.Text(), comment='用例集id') 30 | 31 | project_id = db.Column(db.Integer, db.ForeignKey('project.id'), comment='所属的服务id') 32 | project = db.relationship('Project', backref='tasks') 33 | 34 | def to_dict(self, *args, **kwargs): 35 | return super(Task, self).to_dict(to_dict=['set_id', 'case_id']) 36 | 37 | @classmethod 38 | def make_pagination(cls, form): 39 | """ 解析分页条件 """ 40 | filters = [] 41 | if form.projectId.data: 42 | filters.append(cls.project_id == form.projectId.data) 43 | return cls.pagination( 44 | page_num=form.pageNum.data, 45 | page_size=form.pageSize.data, 46 | filters=filters, 47 | order_by=cls.num.asc() 48 | ) 49 | 50 | 51 | # class TaskDetail(BaseModel): 52 | # """ 测试任务详情表 """ 53 | # __tablename__ = 'tasks_detail' 54 | # relation_id = db.Column(db.Integer(), comment='用例集、用例id') 55 | # data_type = db.Column(db.Integer(), default=1, comment='1、用例集,2、用例') 56 | # name = db.Column(db.Text(), comment='用例集、用例名字') 57 | # task_id = db.Column(db.String(10), comment='任务id') 58 | # 59 | # def to_dict(self, *args, **kwargs): 60 | # return super(TaskDetail, self).to_dict(to_dict=['set_detail', 'case_detail']) 61 | 62 | 63 | class ApschedulerJobs(BaseModel): 64 | """ apscheduler任务表,防止执行数据库迁移的时候,把定时任务删除了 """ 65 | __tablename__ = 'apscheduler_jobs' 66 | id = db.Column(db.String(191), primary_key=True, nullable=False) 67 | next_run_time = db.Column(db.String(128), comment='任务下一次运行时间') 68 | job_state = db.Column(db.LargeBinary(length=(2 ** 32) - 1), comment='任务详情') 69 | -------------------------------------------------------------------------------- /app/baseForm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : baseForm.py 7 | # @Software: PyCharm 8 | import re 9 | 10 | from flask import request 11 | from flask_login import current_user 12 | from wtforms import Form, ValidationError 13 | 14 | from .utils.jsonUtil import JsonUtil 15 | from .utils.parse import extract_functions, parse_function, extract_variables 16 | 17 | 18 | class BaseForm(Form, JsonUtil): 19 | """ 初始化Form校验基类,并初统一处理请求参数 """ 20 | 21 | def __init__(self): 22 | """ 初始化的时候获取所有参数一起传给BaseForm """ 23 | data, args = request.get_json(silent=True) or request.form.to_dict(), request.args.to_dict() 24 | super(BaseForm, self).__init__(data=data, **args) 25 | 26 | def get_error(self): 27 | """ 获取form校验不通过的报错 """ 28 | return self.errors.popitem()[1][0] 29 | 30 | def is_admin(self): 31 | """ 角色为2,为管理员 """ 32 | return current_user.role_id == 2 33 | 34 | def is_not_admin(self): 35 | """ 角色不为2,非管理员 """ 36 | return not self.is_admin() 37 | 38 | def is_can_delete(self, is_manager, obj): 39 | """ 40 | 判断是否有权限删除, 41 | 可删除条件(或): 42 | 1.当前用户为系统管理员 43 | 2.当前用户为当前数据的创建者 44 | 3.当前用户为当前要删除服务的负责人 45 | """ 46 | return is_manager or self.is_admin() or obj.is_create_user(current_user.id) 47 | 48 | def set_attr(self, **kwargs): 49 | """ 根据键值对 对form对应字段的值赋值 """ 50 | for key, value in kwargs.items(): 51 | if hasattr(self, key): 52 | getattr(self, key).data = value 53 | 54 | def validate_func(self, func_container: dict, content: str, message=''): 55 | 56 | # 使用了自定义函数,但是没有引用函数文件的情况 57 | functions = extract_functions(content) 58 | if functions and not func_container: 59 | raise ValidationError(f'{message}要使用自定义函数则需引用对应的函数文件') 60 | 61 | # 使用了自定义函数,但是引用的函数文件中没有当前函数的情况 62 | for function in functions: 63 | func_name = parse_function(function)['func_name'] 64 | if func_name not in func_container: 65 | raise ValidationError(f'{message}引用的自定义函数【{func_name}】在引用的函数文件中均未找到') 66 | 67 | def validate_is_regexp(self, regexp): 68 | """ 校验字符串是否为正则表达式 """ 69 | return re.compile(r".*\(.*\).*").match(regexp) 70 | 71 | def validate_variable(self, variables_container: dict, content: str, message=''): 72 | """ 引用的变量需存在 """ 73 | for variable in extract_variables(content): 74 | if variable not in variables_container: 75 | raise ValidationError(f'{message}引用的变量【{variable}】不存在') 76 | 77 | def validate_variable_and_header_format(self, content: list, message1='', message2=''): 78 | """ 自定义变量、头部信息,格式校验 """ 79 | for index, data in enumerate(content): 80 | if (data['key'] and not data['value']) or (not data['key'] and data['value']): 81 | raise ValidationError(f'{message1}{index + 1}{message2}') 82 | 83 | def validate_base_validates(self, data, func_container): 84 | """ 校验断言信息 """ 85 | for index, validate in enumerate(data): 86 | row = f'断言,第【{index + 1}】行,' 87 | data_source, key = validate.get('data_source'), validate.get('key') 88 | validate_type = validate.get('validate_type') 89 | data_type, value = validate.get('data_type'), validate.get('value') 90 | 91 | # 实际结果数据源和预期结果数据类型必须同时存在或者同时不存在 92 | if (data_source and not data_type) or (not data_source and data_type): 93 | raise ValidationError(f'{row}若要进行断言,则实际结果数据源和预期结果数据类型需同时存在,若不进行断言,则实际结果数据源和预期结果数据类型需同时不存在') 94 | elif not data_source and not data_type: # 都没有,此条断言无效,不解析 95 | continue 96 | else: # 有效的断言 97 | # 实际结果,选择的数据源为正则表达式,但是正则表达式错误 98 | if data_source == 'regexp' and not self.validate_is_regexp(key): 99 | raise ValidationError(f'{row}正则表达式【{key}】错误') 100 | 101 | if not validate_type: # 没有选择断言类型 102 | raise ValidationError(f'{row}请选择断言类型') 103 | 104 | if value is None: # 要进行断言,则预期结果必须有值 105 | raise ValidationError(f'{row}预期结果需填写') 106 | 107 | if data_type == "str": # 普通字符串,无需解析,填的是什么就用什么 108 | pass 109 | elif data_type == "variable": # 预期结果为自定义变量,能解析出变量即可 110 | if extract_variables(value).__len__() < 1: 111 | raise ValidationError(f'{row}引用的变量表达式【{value}】错误') 112 | elif data_type == "func": # 预期结果为自定义函数,校验校验预期结果表达式、实际结果表达式 113 | self.validate_func(func_container, value, message=row) # 实际结果表达式是否引用自定义函数 114 | elif data_type == 'json': # 预期结果为json 115 | try: 116 | self.dumps(self.loads(value)) 117 | except Exception as error: 118 | raise ValidationError(f'{row}预期结果【{value}】,不可转为【{data_type}】') 119 | else: # python数据类型 120 | try: 121 | eval(f'{data_type}({value})') 122 | except Exception as error: 123 | raise ValidationError(f'{row}预期结果【{value}】,不可转为【{data_type}】') 124 | 125 | def validate_base_extracts(self, data): 126 | """ 校验数据提取表达式 """ 127 | for index, validate in enumerate(data): 128 | row = f'数据提取,第【{index + 1}】行,' 129 | data_source, key, value = validate.get('data_source'), validate.get('key'), validate.get('value') 130 | 131 | # 实际结果数据源和预期结果数据类型必须同时存在或者同时不存在 132 | if (data_source and not key) or (not data_source and key): 133 | raise ValidationError(f'{row}若要进行数据提取,则自定义变量名和提取数据源需同时存在,若不进行提取,则自定义变量名和提取数据源需同时不存在') 134 | elif not data_source and not key: # 都没有,此条数据无效,不解析 135 | continue 136 | else: # 有效的数据提取 137 | # 实际结果,选择的数据源为正则表达式,但是正则表达式错误 138 | if data_source == 'regexp' and not self.validate_is_regexp(value): 139 | raise ValidationError(f'{row}正则表达式【{value}】错误') 140 | -------------------------------------------------------------------------------- /app/baseView.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/10/26 9:19 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : baseView.py 7 | # @Software: PyCharm 8 | from flask import views 9 | 10 | from .utils.required import login_required, admin_required 11 | from .utils.jsonUtil import JsonUtil 12 | 13 | 14 | class BaseMethodView(views.MethodView, JsonUtil): 15 | """ 继承views.MethodView, 并使每个继承此基类MethodView的类视图默认带上 login_required登录校验 """ 16 | decorators = [login_required] 17 | 18 | 19 | class AdminMethodView(BaseMethodView): 20 | """ 管理员权限校验 """ 21 | decorators = [login_required, admin_required] 22 | -------------------------------------------------------------------------------- /app/config/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from flask import Blueprint, current_app, request 5 | from flask_login import current_user 6 | 7 | from app.utils.log import logger 8 | 9 | config = Blueprint('config', __name__) 10 | config.logger = logger 11 | 12 | from . import (errors) 13 | from app.config import views 14 | 15 | 16 | @config.before_request 17 | def before_request(): 18 | """ 前置钩子函数, 每个请求进来先经过此函数""" 19 | name = current_user.name if hasattr(current_user, 'name') else '' 20 | current_app.logger.info( 21 | f'[{request.remote_addr}] [{name}] [{request.method}] [{request.url}]: \n请求参数:{request.json}') 22 | 23 | 24 | @config.after_request 25 | def after_request(response_obj): 26 | """ 后置钩子函数,每个请求最后都会经过此函数 """ 27 | if 'download' in request.path: 28 | return response_obj 29 | result = copy.copy(response_obj.response) 30 | if isinstance(result[0], bytes): 31 | result[0] = bytes.decode(result[0]) 32 | # 减少日志数据打印,跑用例的数据均不打印到日志 33 | if 'apiMsg/run' not in request.path and 'report/run' not in request.path and 'report/list' not in request.path: 34 | current_app.logger.info(f'{request.method}==>{request.url}, 返回数据:{json.loads(result[0])}') 35 | return response_obj 36 | -------------------------------------------------------------------------------- /app/config/errors.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : errors.py 7 | # @Software: PyCharm 8 | import traceback 9 | 10 | import requests 11 | from flask import current_app, request 12 | 13 | from ..utils import restful 14 | from . import config 15 | from config.config import conf 16 | 17 | 18 | @config.app_errorhandler(404) 19 | def page_not_found(e): 20 | """ 捕获404的所有异常 """ 21 | # current_app.logger.exception(f'404错误url: {request.path}') 22 | return restful.url_not_find(msg=f'接口 {request.path} 不存在') 23 | 24 | 25 | @config.app_errorhandler(Exception) 26 | def error_handler(e): 27 | """ 捕获所有服务器内部的异常 """ 28 | # 把错误发送到 即时达推送 的 系统错误 通道 29 | try: 30 | config.logger.error(f'系统出错了: {e}') 31 | requests.post( 32 | url=conf['error_push']['url'], 33 | json={ 34 | 'key': conf['error_push']['key'], 35 | 'head': f'{conf["SECRET_KEY"]}报错了', 36 | 'body': f'{e}' 37 | } 38 | ) 39 | except: 40 | pass 41 | current_app.logger.exception(f'触发错误url: {request.path}\n{traceback.format_exc()}') 42 | return restful.error(f'服务器异常: {e}') 43 | -------------------------------------------------------------------------------- /app/config/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/6/21 9:29 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from wtforms import StringField, IntegerField 9 | from wtforms.validators import ValidationError, DataRequired 10 | 11 | from .models import Config, ConfigType 12 | from app.baseForm import BaseForm 13 | 14 | 15 | class GetConfigTypeListForm(BaseForm): 16 | """ 获取配置类型列表 """ 17 | pageNum = IntegerField() 18 | pageSize = IntegerField() 19 | 20 | 21 | class DeleteConfigTypeForm(BaseForm): 22 | """ 删除配置类型表单校验 """ 23 | id = IntegerField(validators=[DataRequired('配置类型id必传')]) 24 | 25 | def validate_id(self, field): 26 | conf_type = ConfigType.get_first(id=field.data) 27 | if not conf_type: 28 | raise ValidationError(f'id为 {field.data} 的配置类型不存在') 29 | setattr(self, 'conf_type', conf_type) 30 | 31 | 32 | class GetConfigTypeForm(DeleteConfigTypeForm): 33 | """ 获取配置类型表单校验 """ 34 | 35 | 36 | class PostConfigTypeForm(BaseForm): 37 | """ 新增配置类型表单校验 """ 38 | name = StringField(validators=[DataRequired('请输入配置类型')]) 39 | desc = StringField() 40 | 41 | def validate_name(self, field): 42 | if ConfigType.get_first(name=field.data): 43 | raise ValidationError(f'名为 {field.data} 的配置类型已存在') 44 | 45 | 46 | class PutConfigTypeForm(PostConfigTypeForm): 47 | """ 修改配置类型表单校验 """ 48 | id = IntegerField(validators=[DataRequired('配置类型id必传')]) 49 | 50 | def validate_id(self, field): 51 | old_conf_type = ConfigType.get_first(id=field.data) 52 | if not old_conf_type: 53 | raise ValidationError(f'id为 {field.data} 的配置类型不存在') 54 | setattr(self, 'conf_type', old_conf_type) 55 | 56 | def validate_name(self, field): 57 | old_conf_type = ConfigType.get_first(name=field.data) 58 | if old_conf_type and old_conf_type.id != self.id.data: 59 | raise ValidationError(f'名为 {field.data} 的配置类型已存在') 60 | setattr(self, 'conf_type', old_conf_type) 61 | 62 | 63 | class GetConfigListForm(BaseForm): 64 | """ 获取配置列表 """ 65 | type = StringField() 66 | pageNum = IntegerField() 67 | pageSize = IntegerField() 68 | 69 | 70 | class DeleteConfigForm(BaseForm): 71 | """ 删除配置表单校验 """ 72 | id = IntegerField(validators=[DataRequired('配置id必传')]) 73 | 74 | def validate_id(self, field): 75 | conf = Config.get_first(id=field.data) 76 | if not conf: 77 | raise ValidationError(f'id为 {field.data} 的配置不存在') 78 | setattr(self, 'conf', conf) 79 | 80 | 81 | class GetConfigForm(DeleteConfigForm): 82 | """ 获取配置表单校验 """ 83 | 84 | 85 | class PostConfigForm(BaseForm): 86 | """ 新增配置表单校验 """ 87 | name = StringField(validators=[DataRequired('请输入配置名')]) 88 | value = StringField(validators=[DataRequired('请输入配置的值')]) 89 | type = StringField(validators=[DataRequired('请输入配置的类型')]) 90 | desc = StringField() 91 | 92 | def validate_name(self, field): 93 | if Config.get_first(name=field.data): 94 | raise ValidationError(f'名为 {field.data} 的配置已存在') 95 | 96 | 97 | class PutConfigForm(PostConfigForm): 98 | """ 修改配置表单校验 """ 99 | id = IntegerField(validators=[DataRequired('配置id必传')]) 100 | 101 | def validate_name(self, field): 102 | conf = Config.get_first(name=field.data) 103 | if conf.id != self.id.data: 104 | raise ValidationError(f'名为 {field.data} 的配置已存在') 105 | setattr(self, 'conf', conf) 106 | -------------------------------------------------------------------------------- /app/config/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/6/21 9:28 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class ConfigType(BaseModel): 12 | """ 配置类型表 """ 13 | 14 | __tablename__ = 'config_type' 15 | 16 | name = db.Column(db.String(128), nullable=True, unique=True, comment='字段名') 17 | desc = db.Column(db.Text(), comment='描述') 18 | 19 | @classmethod 20 | def make_pagination(cls, form): 21 | """ 解析分页条件 """ 22 | filters = [] 23 | return cls.pagination( 24 | page_num=form.pageNum.data, 25 | page_size=form.pageSize.data, 26 | filters=filters, 27 | order_by=cls.id.asc() 28 | ) 29 | 30 | 31 | class Config(BaseModel): 32 | """ 配置表 """ 33 | 34 | __tablename__ = 'config' 35 | 36 | name = db.Column(db.String(128), nullable=True, unique=True, comment='字段名') 37 | value = db.Column(db.Text(), nullable=True, comment='字段值') 38 | type = db.Column(db.String(128), nullable=True, comment='配置类型') 39 | desc = db.Column(db.Text(), comment='描述') 40 | 41 | @classmethod 42 | def make_pagination(cls, form): 43 | """ 解析分页条件 """ 44 | filters = [] 45 | if form.type.data: 46 | filters.append(cls.type == form.type.data) 47 | return cls.pagination( 48 | page_num=form.pageNum.data, 49 | page_size=form.pageSize.data, 50 | filters=filters, 51 | order_by=cls.id.asc() 52 | ) 53 | -------------------------------------------------------------------------------- /app/config/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/6/21 9:28 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from flask import request 9 | 10 | from app.utils import restful 11 | from app.utils.required import login_required 12 | 13 | from .models import Config, ConfigType, db 14 | from .forms import ( 15 | GetConfigTypeForm, DeleteConfigTypeForm, PostConfigTypeForm, PutConfigTypeForm, GetConfigTypeListForm, 16 | GetConfigForm, DeleteConfigForm, PostConfigForm, PutConfigForm, GetConfigListForm 17 | ) 18 | from app.baseView import BaseMethodView 19 | from app.config import config 20 | 21 | 22 | @config.route('/type/list', methods=['GET']) 23 | @login_required 24 | def conf_type_list(): 25 | form = GetConfigTypeListForm() 26 | if form.validate(): 27 | return restful.success(data=ConfigType.make_pagination(form)) 28 | return restful.error(form.get_error()) 29 | 30 | 31 | class ConfigTypeView(BaseMethodView): 32 | 33 | def get(self): 34 | form = GetConfigTypeForm() 35 | if form.validate(): 36 | return restful.success('获取成功', data=form.conf.to_dict()) 37 | return restful.error(form.get_error()) 38 | 39 | def post(self): 40 | form = PostConfigTypeForm() 41 | if form.validate(): 42 | with db.auto_commit(): 43 | config_type = ConfigType() 44 | config_type.create(form.data) 45 | db.session.add(config_type) 46 | return restful.success('新增成功', data=config_type.to_dict()) 47 | return restful.error(form.get_error()) 48 | 49 | def put(self): 50 | form = PutConfigTypeForm() 51 | if form.validate(): 52 | with db.auto_commit(): 53 | form.conf_type.update(form.data) 54 | return restful.success('修改成功', data=form.conf_type.to_dict()) 55 | return restful.error(form.get_error()) 56 | 57 | def delete(self): 58 | form = DeleteConfigTypeForm() 59 | if form.validate(): 60 | with db.auto_commit(): 61 | db.session.delete(form.config_type) 62 | return restful.success('删除成功') 63 | return restful.error(form.get_error()) 64 | 65 | 66 | @config.route('/list', methods=['GET']) 67 | @login_required 68 | def conf_list(): 69 | form = GetConfigListForm() 70 | if form.validate(): 71 | return restful.success(data=Config.make_pagination(form)) 72 | return restful.error(form.get_error()) 73 | 74 | 75 | @config.route('/configByName', methods=['GET']) 76 | @login_required 77 | def get_conf_by_name(): 78 | """ 根据配置名获取配置 """ 79 | return restful.success(data=Config.get_first(name=request.args.get('name')).to_dict()) 80 | 81 | 82 | class ConfigView(BaseMethodView): 83 | 84 | def get(self): 85 | form = GetConfigForm() 86 | if form.validate(): 87 | return restful.success('获取成功', data=form.conf.to_dict()) 88 | return restful.error(form.get_error()) 89 | 90 | def post(self): 91 | form = PostConfigForm() 92 | if form.validate(): 93 | config = Config().create(form.data) 94 | return restful.success('新增成功', data=config.to_dict()) 95 | return restful.error(form.get_error()) 96 | 97 | def put(self): 98 | form = PutConfigForm() 99 | if form.validate(): 100 | form.conf.update(form.data) 101 | return restful.success('修改成功', data=form.conf.to_dict()) 102 | return restful.error(form.get_error()) 103 | 104 | def delete(self): 105 | form = DeleteConfigForm() 106 | if form.validate(): 107 | form.conf.delete() 108 | return restful.success('删除成功') 109 | return restful.error(form.get_error()) 110 | 111 | 112 | config.add_url_rule('/', view_func=ConfigView.as_view('config')) 113 | config.add_url_rule('/type', view_func=ConfigTypeView.as_view('configType')) 114 | -------------------------------------------------------------------------------- /app/test_work/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from flask import Blueprint, current_app, request 5 | from flask_login import current_user 6 | 7 | from app.utils.log import logger 8 | 9 | test_work = Blueprint('test_work', __name__) 10 | test_work.logger = logger 11 | 12 | from . import (errors) 13 | from app.test_work.account import views 14 | from app.test_work.dataPool import views 15 | from app.test_work.dataBase import views 16 | # from app.test_work.frontDiff import views 17 | from app.test_work.kym import views 18 | from app.test_work.swagger import views 19 | from app.test_work.yapi import views 20 | from app.test_work.file import views 21 | 22 | 23 | @test_work.before_request 24 | def before_request(): 25 | """ 前置钩子函数, 每个请求进来先经过此函数""" 26 | name = current_user.name if hasattr(current_user, 'name') else '' 27 | current_app.logger.info( 28 | f'[{request.remote_addr}] [{name}] [{request.method}] [{request.url}]: \n请求参数:{request.json}') 29 | 30 | 31 | @test_work.after_request 32 | def after_request(response_obj): 33 | """ 后置钩子函数,每个请求最后都会经过此函数 """ 34 | if 'download' in request.path: 35 | return response_obj 36 | result = copy.copy(response_obj.response) 37 | if isinstance(result[0], bytes): 38 | result[0] = bytes.decode(result[0]) 39 | # 减少日志数据打印,跑用例的数据均不打印到日志 40 | if 'apiMsg/run' not in request.path and 'report/run' not in request.path and 'report/list' not in request.path: 41 | current_app.logger.info(f'{request.method}==>{request.url}, 返回数据:{json.loads(result[0])}') 42 | return response_obj 43 | -------------------------------------------------------------------------------- /app/test_work/account/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 15:17 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/account/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:05 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class AccountModel(BaseModel): 12 | """ 测试账号表 """ 13 | __tablename__ = 'account' 14 | 15 | project = db.Column(db.String(255), comment='服务名') 16 | name = db.Column(db.String(255), comment='账户名') 17 | account = db.Column(db.String(255), comment='登录账号') 18 | password = db.Column(db.String(255), comment='登录密码') 19 | desc = db.Column(db.Text(), comment='备注') 20 | event = db.Column(db.String(50), comment='环境') 21 | 22 | @classmethod 23 | def make_pagination(cls, filter): 24 | """ 解析分页条件 """ 25 | filters = [] 26 | if filter.get("name"): 27 | filters.append(AccountModel.name.like(f'%{filter.get("name")}%')) 28 | if filter.get("event"): 29 | filters.append(AccountModel.event == filter.get("event")) 30 | if filter.get("project"): 31 | filters.append(AccountModel.project == filter.get("project")) 32 | return cls.pagination( 33 | page_num=filter.get("page_num"), 34 | page_size=filter.get("page_size"), 35 | filters=filters, 36 | order_by=cls.created_time.desc()) 37 | -------------------------------------------------------------------------------- /app/test_work/account/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:02 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from flask import request 9 | 10 | from app.test_work import test_work 11 | from .models import AccountModel 12 | from app.utils import restful 13 | from app.baseView import BaseMethodView 14 | from app.utils.required import login_required 15 | 16 | 17 | @test_work.route('/account/project/list') 18 | @login_required 19 | def get_account_project_list(): 20 | """ 获取账号项目列表 """ 21 | project_list = AccountModel.query.with_entities(AccountModel.project).distinct().all() 22 | return restful.success('获取成功', data=[{'key': project[0], 'value': project[0]} for project in project_list]) 23 | 24 | 25 | @test_work.route('/account/list') 26 | @login_required 27 | def get_account_list(): 28 | """ 获取账号列表 """ 29 | return restful.success('获取成功', data=AccountModel.make_pagination({ 30 | 'page_num': request.args.get('pageNum'), 31 | 'page_size': request.args.get('pageSize'), 32 | 'event': request.args.get('event'), 33 | 'project': request.args.get('project'), 34 | 'name': request.args.get('name') 35 | })) 36 | 37 | 38 | class AccountView(BaseMethodView): 39 | """ 测试账号管理 """ 40 | 41 | def get(self): 42 | """ 获取用户信息 """ 43 | return restful.success('获取成功', data=AccountModel.get_first(id=request.args.get('id')).to_dict()) 44 | 45 | def post(self): 46 | """ 新增账号 """ 47 | 48 | if AccountModel.get_first( 49 | project=request.json['project'], 50 | event=request.json['event'], 51 | account=request.json['account']): 52 | return restful.fail(f"当前环境下 {request.json['account']} 账号已存在,直接修改即可") 53 | account = AccountModel().create(request.json) 54 | return restful.success('新增成功', data=account.to_dict()) 55 | 56 | def put(self): 57 | """ 修改账号 """ 58 | # 账号不重复 59 | account = AccountModel.get_first( 60 | project=request.json['project'], 61 | event=request.json['event'], 62 | account=request.json['account']) 63 | if account and account.id != request.json.get('id'): 64 | return restful.fail(f'当前环境下账号 {account.account} 已存在', data=account.to_dict()) 65 | 66 | old_account = AccountModel.get_first(id=request.json.get('id')) 67 | old_account.update(request.json) 68 | return restful.success('修改成功', data=old_account.to_dict()) 69 | 70 | def delete(self): 71 | """ 删除账号 """ 72 | AccountModel.get_first(id=request.json.get('id')).delete() 73 | return restful.success('删除成功') 74 | 75 | 76 | test_work.add_url_rule('/account', view_func=AccountView.as_view('account')) 77 | -------------------------------------------------------------------------------- /app/test_work/dataBase/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/27 17:14 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/dataBase/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/27 17:14 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | import os 9 | import time 10 | 11 | from flask import request 12 | 13 | from app.test_work import test_work 14 | from config.config import conf 15 | from app.utils.globalVariable import DB_BACK_UP_ADDRESS 16 | from app.utils import restful 17 | from app.utils.required import login_required 18 | 19 | 20 | def make_pagination(data_list, pag_size, page_num): 21 | """ 数据列表分页 """ 22 | start = (page_num - 1) * pag_size 23 | end = start + pag_size 24 | return data_list[start: end] 25 | 26 | 27 | @test_work.route('/db/backUp', methods=['POST']) 28 | @login_required 29 | def db_back_up(): 30 | """ 执行数据库备份命令 """ 31 | back_up_name = f'api_test{time.strftime("%Y-%m-%d-%H-%M-%S")}.sql' 32 | os.system( 33 | f'mysqldump ' 34 | f'-u{conf["db"]["user"]} ' 35 | f'-p{conf["db"]["password"]} ' 36 | f'--databases {conf["db"]["database"]} > {DB_BACK_UP_ADDRESS}/{back_up_name}' 37 | ) 38 | return restful.success('备份成功', data=back_up_name) 39 | 40 | 41 | @test_work.route('/db/backUp/list', methods=['GET']) 42 | @login_required 43 | def db_back_up_list(): 44 | """ 数据库备份文件列表 """ 45 | pag_size = request.args.get('pageSize') or conf['page']['pageSize'] 46 | page_num = request.args.get('pageNum') or conf['page']['pageNum'] 47 | file_list = os.listdir(DB_BACK_UP_ADDRESS) 48 | filter_list = make_pagination(file_list, int(pag_size), int(page_num)) # 分页 49 | return restful.success('获取成功', data={'data': filter_list, 'total': file_list.__len__()}) 50 | -------------------------------------------------------------------------------- /app/test_work/dataPool/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 15:23 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/dataPool/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:05 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class AutoTestPolyFactoring(BaseModel): 12 | """ 数据池 """ 13 | __tablename__ = 'auto_test_poly_factoring' 14 | asset_code = db.Column(db.String(100), nullable=True, default='', comment='资产编号') 15 | payment_no = db.Column(db.String(100), nullable=True, default='', comment='付款单编号') 16 | bill_code = db.Column(db.String(100), nullable=True, default='', comment='付款申请单编号') 17 | batch_no = db.Column(db.String(40), nullable=True, unique=True, default='', comment='批次号') 18 | batch_code = db.Column(db.String(40), nullable=True, default='', comment='批次号(编号)') 19 | confirm_date = db.Column(db.TIMESTAMP, nullable=True, default=None, comment='定数时间') 20 | product_id = db.Column(db.String(40), nullable=True, default='', comment='融资产品id') 21 | product_name = db.Column(db.String(60), nullable=True, default='', comment='产品名称') 22 | supplier_org_id = db.Column(db.String(40), nullable=True, default='', comment='债权人公司id') 23 | supplier_org_name = db.Column(db.String(128), nullable=True, default='', comment='债权人(特定供应商)') 24 | project_org_id = db.Column(db.String(40), nullable=True, default='', comment='债务人公司id') 25 | project_org_name = db.Column(db.String(128), nullable=True, default='', comment='债务人(服务公司)') 26 | purchaser_org_name = db.Column(db.String(128), nullable=True, default='', comment='核心企业名称') 27 | purchaser_org_id = db.Column(db.String(40), nullable=True, default='', comment='核心企业公司id') 28 | finance_money = db.Column(db.String(128), nullable=True, default='', comment='融资金额') 29 | file_upload = db.Column(db.String(10), nullable=True, default='0', comment='文件上传状态(0为上传;1已上传)') 30 | pledge_init = db.Column(db.String(10), nullable=True, default='0', comment='中登初验(0未进行;1已初验;2已中登)') 31 | agreement_create = db.Column(db.String(10), nullable=True, default='0', comment='协议生成(0为未生成)') 32 | document_collect_status = db.Column(db.String(10), nullable=True, default='1', comment='文件收集状态(1未开始;2进行中;3已完成)') 33 | access_audit_status = db.Column(db.String(10), nullable=True, default='1', comment='准入审核状态(1未开始;2进行中;3不通过;4通过)') 34 | filter_compare_status = db.Column(db.String(10), nullable=True, default='1', comment='初筛对比状态(1未开始;2进行中;3已完成)') 35 | six_order_match_status = db.Column(db.String(10), nullable=True, default='1', 36 | comment='六单匹配状态(1未开始,2初审中,3初审不通过,4复审中,5复审通过)') 37 | pledge_init_status = db.Column(db.String(10), nullable=True, default='1', 38 | comment='中登初验状态(1未开始;2进行中;3复核中;4有风险通过;5无风险通过)') 39 | agreement_audit_status = db.Column(db.String(10), nullable=True, default='1', comment='协议审核状态(1未开始;2进行中;3不通过;4通过)') 40 | pledge_status = db.Column(db.String(10), nullable=True, default='1', comment='中登登记状态(1未开始;2进行中;3复核中;4有风险通过;5无风险通过)') 41 | exception_status = db.Column(db.String(10), nullable=True, default='1', comment='异常状态') 42 | eliminate_apply_status = db.Column(db.String(10), nullable=True, default='1', comment='剔单申请状态(1默认状态;2剔除申请中;3已剔除;)') 43 | revoke_status = db.Column(db.String(10), nullable=True, default='1', comment='撤回状态(1默认状态;2已撤回)') 44 | enable_flag = db.Column(db.String(10), nullable=True, default='1', comment='是否可用(0:不可用;1:可用)') 45 | 46 | 47 | class AutoTestUser(BaseModel): 48 | """ 自动化测试用户表 """ 49 | __tablename__ = 'auto_test_user' 50 | mobile = db.Column(db.String(11), nullable=True, default='', comment='手机号') 51 | password = db.Column(db.String(128), nullable=True, default='', comment='密码') 52 | u_token = db.Column(db.String(128), nullable=True, default='', comment='token') 53 | role = db.Column(db.String(128), nullable=True, default='', comment='角色,平台方admin;核心企业core;供应商supplier;资金方capital') 54 | company_id = db.Column(db.String(128), nullable=True, default='', comment='公司id') 55 | company_name = db.Column(db.String(128), nullable=True, default='', comment='公司名') 56 | comment = db.Column(db.String(256), nullable=True, default='', comment='备注') 57 | -------------------------------------------------------------------------------- /app/test_work/dataPool/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:05 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from app.utils.required import login_required 9 | from app.test_work import test_work 10 | from .models import AutoTestPolyFactoring, AutoTestUser 11 | from app.utils import restful 12 | 13 | 14 | @test_work.route('/dataPool') 15 | @login_required 16 | def data_pool_list(): 17 | """ 数据池数据列表 """ 18 | return restful.success('获取成功', data=[ 19 | data_pool.to_dict(pop_list=['created_time', 'update_time']) for data_pool in 20 | AutoTestPolyFactoring.query.filter().order_by(AutoTestPolyFactoring.id.desc()).all() 21 | ]) 22 | 23 | 24 | @test_work.route('/autoTestUser') 25 | @login_required 26 | def auto_test_user_list(): 27 | """ 自动化测试用户数据列表 """ 28 | return restful.success('获取成功', data=[ 29 | user.to_dict(pop_list=['created_time', 'update_time']) for user in AutoTestUser.query.filter().all() 30 | ]) 31 | -------------------------------------------------------------------------------- /app/test_work/errors.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : errors.py 7 | # @Software: PyCharm 8 | import traceback 9 | 10 | import requests 11 | from flask import current_app, request 12 | 13 | from ..utils import restful 14 | from . import test_work 15 | from config.config import conf 16 | 17 | 18 | @test_work.app_errorhandler(404) 19 | def page_not_found(e): 20 | """ 捕获404的所有异常 """ 21 | # current_app.logger.exception(f'404错误url: {request.path}') 22 | return restful.url_not_find(msg=f'接口 {request.path} 不存在') 23 | 24 | 25 | @test_work.app_errorhandler(Exception) 26 | def error_handler(e): 27 | """ 捕获所有服务器内部的异常 """ 28 | # 把错误发送到 即时达推送 的 系统错误 通道 29 | try: 30 | test_work.logger.error(f'系统出错了: {e}') 31 | requests.post( 32 | url=conf['error_push']['url'], 33 | json={ 34 | 'key': conf['error_push']['key'], 35 | 'head': f'{conf["SECRET_KEY"]}报错了', 36 | 'body': f'{e}' 37 | } 38 | ) 39 | except: 40 | pass 41 | current_app.logger.exception(f'触发错误url: {request.path}\n{traceback.format_exc()}') 42 | return restful.error(f'服务器异常: {e}') 43 | -------------------------------------------------------------------------------- /app/test_work/file/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/test_work/file/__init__.py -------------------------------------------------------------------------------- /app/test_work/file/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/6/28 10:06 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : fileView.py 7 | # @Software: PyCharm 8 | import os 9 | import time 10 | 11 | from flask import request, send_from_directory 12 | 13 | from config.config import conf 14 | from app.test_work import test_work 15 | from app.utils import restful 16 | from app.utils.globalVariable import CASE_FILE_ADDRESS, CALL_BACK_ADDRESS, CFCA_FILE_ADDRESS, TEMP_FILE_ADDRESS 17 | from app.baseView import BaseMethodView 18 | from app.utils.required import login_required 19 | 20 | folders = { 21 | 'case': CASE_FILE_ADDRESS, 22 | 'cfca': CFCA_FILE_ADDRESS, 23 | 'callBack': CALL_BACK_ADDRESS, 24 | 'temp': TEMP_FILE_ADDRESS, 25 | } 26 | 27 | 28 | def format_time(atime): 29 | """ 时间戳转年月日时分秒 """ 30 | return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(atime)) 31 | 32 | 33 | @test_work.route('/file/create/download') 34 | def creat_file(): 35 | """ 创建指定大小和格式的文件 """ 36 | start = time.time() 37 | size, file_format = float(request.args.get('size')), request.args.get('format') 38 | local_time = time.strftime("%Y%m%d%H%M%S", time.localtime()) 39 | file_name = f"temp_file_{str(local_time)}.{file_format}" 40 | 41 | # 删除历史的大文件 42 | for old_file in os.listdir(TEMP_FILE_ADDRESS): 43 | if old_file.startswith('temp_file'): 44 | os.remove(os.path.join(TEMP_FILE_ADDRESS, old_file)) 45 | 46 | # 生成新的大文件 47 | file = open(os.path.join(TEMP_FILE_ADDRESS, file_name), 'w', encoding='utf-8') 48 | file.seek(1024 * 1024 * 1024 * size) 49 | file.write('test') 50 | file.close() 51 | print(time.time() - start) 52 | return send_from_directory(TEMP_FILE_ADDRESS, file_name, as_attachment=True) 53 | 54 | 55 | def make_pagination(data_list, pag_size, page_num): 56 | """ 数据列表分页 """ 57 | start = (page_num - 1) * pag_size 58 | end = start + pag_size 59 | return data_list[start: end] 60 | 61 | 62 | @test_work.route('/file/list') 63 | @login_required 64 | def get_file_list(): 65 | """ 文件列表 """ 66 | pag_size = request.args.get('pageSize') or conf['page']['pageSize'] 67 | page_num = request.args.get('pageNum') or conf['page']['pageNum'] 68 | addr = folders.get(request.args.get('fileType'), 'case') 69 | file_list = os.listdir(addr) 70 | 71 | # 分页 72 | filter_list = make_pagination(file_list, int(pag_size), int(page_num)) 73 | 74 | parsed_file_list = [] 75 | for file_name in filter_list: 76 | file_info = os.stat(os.path.join(addr, file_name)) 77 | parsed_file_list.append({ 78 | 'name': file_name, # 文件名 79 | 'size': file_info.st_size, # 文件文件大小 80 | 'lastVisitTime': format_time(file_info.st_atime), # 最近一次使用时间 81 | 'LastModifiedTime': format_time(file_info.st_mtime), # 最后一次更新时间 82 | }) 83 | return restful.success('获取成功', data={'data': parsed_file_list, 'total': file_list.__len__()}) 84 | 85 | 86 | @test_work.route('/file/check', methods=['GET']) 87 | @login_required 88 | def check_file(): 89 | """ 检查文件是否已存在 """ 90 | file_name, file_type = request.args.get('name'), request.args.get('fileType') 91 | return restful.fail(f'文件 {file_name} 已存在') if os.path.exists( 92 | os.path.join(folders.get(file_type), file_name)) else restful.success('文件不存在') 93 | 94 | 95 | @test_work.route('/file/download', methods=['GET']) 96 | @login_required 97 | def download_file(): 98 | """ 下载文件 """ 99 | addr = folders.get(request.args.get('fileType'), 'case') 100 | return send_from_directory(addr, request.args.to_dict().get('name'), as_attachment=True) 101 | 102 | 103 | @test_work.route('/upload', methods=['POST'], strict_slashes=False) 104 | @login_required 105 | def file_upload(): 106 | """ 文件上传 """ 107 | file, addr = request.files['file'], folders.get(request.form.get('fileType', 'case')) 108 | file.save(os.path.join(addr, file.filename)) 109 | return restful.success(msg='上传成功', data=file.filename) 110 | 111 | 112 | class FileManage(BaseMethodView): 113 | """ 文件管理视图 """ 114 | 115 | decorators = [] 116 | 117 | def get(self): 118 | args = request.args.to_dict() 119 | return send_from_directory(args.get('fileType', 'case'), args.get('name'), as_attachment=True) 120 | 121 | def post(self): 122 | """ 上传文件 """ 123 | file_name_list, addr = [], folders.get(request.form.get('fileType', 'case')) 124 | for file_io in request.files.getlist('files'): 125 | file_io.save(os.path.join(addr, file_io.filename)) 126 | file_name_list.append(file_io.filename) 127 | return restful.success(msg='上传成功', data=file_name_list) 128 | 129 | def delete(self): 130 | """ 删除文件 """ 131 | request_json = request.get_json(silent=True) 132 | name, addr = request_json.get('name'), folders.get(request_json.get('fileType'), 'case') 133 | path = os.path.join(addr, name) 134 | if os.path.exists(path): 135 | os.remove(path) 136 | return restful.success('删除成功', data={'name': name}) 137 | 138 | 139 | test_work.add_url_rule('/file', view_func=FileManage.as_view('file')) 140 | -------------------------------------------------------------------------------- /app/test_work/frontDiff/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 15:37 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/frontDiff/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:05 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class FrontDiffRecord(BaseModel): 12 | """ 前端引用接口数据比对记录 """ 13 | __tablename__ = 'front_diff_record' 14 | parent = db.Column(db.String(255), comment='父级文件夹') 15 | name = db.Column(db.String(255), comment='当前文件') 16 | is_changed = db.Column(db.Integer, default=0, comment='对比结果,1有改变,0没有改变') 17 | diff_summary = db.Column(db.Text, comment='比对结果数据') 18 | 19 | @classmethod 20 | def make_pagination(cls, attr): 21 | """ 解析分页条件 """ 22 | filters = [] 23 | if attr.get('name'): 24 | filters.append(FrontDiffRecord.name.like(f'%{attr.get("name")}%')) 25 | if attr.get('create_user'): 26 | filters.append(FrontDiffRecord.create_user == attr.get('create_user')) 27 | return cls.pagination( 28 | page_num=attr.get('pageNum', 1), 29 | page_size=attr.get('pageSize', 20), 30 | filters=filters, 31 | order_by=cls.created_time.desc()) 32 | -------------------------------------------------------------------------------- /app/test_work/kym/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 15:25 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/kym/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:05 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class KYMModule(BaseModel): 12 | """ KYM分析表 """ 13 | __tablename__ = 'kym' 14 | 15 | project = db.Column(db.String(255), comment='服务名') 16 | kym = db.Column(db.Text, default='{}', comment='kym分析') 17 | 18 | def to_dict(self, *args, **kwargs): 19 | return super(KYMModule, self).to_dict(to_dict=['kym']) 20 | -------------------------------------------------------------------------------- /app/test_work/kym/views.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:02 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | import json 9 | import os 10 | 11 | from flask import request, send_from_directory 12 | 13 | from app.test_work import test_work 14 | from app.utils.globalVariable import TEMP_FILE_ADDRESS 15 | from app.utils.makeXmind import make_xmind 16 | from .models import KYMModule, db 17 | from app.config.models import Config 18 | from app.utils import restful 19 | from app.baseView import BaseMethodView 20 | from app.utils.required import login_required 21 | 22 | 23 | @test_work.route('/kym/project', methods=['POST']) 24 | @login_required 25 | def add_kym_project(): 26 | """ kym添加服务 """ 27 | if KYMModule.get_first(project=request.json['project']): 28 | return restful.fail(f"服务 {request.json['project']} 已存在") 29 | with db.auto_commit(): 30 | kym_data = {"nodeData": {"topic": request.json['project'], "root": True, "children": []}} 31 | kym_data['nodeData']['children'] = json.loads(Config.get_first(name='kym').value) 32 | kym = KYMModule() 33 | kym.create({'project': request.json['project'], 'kym': json.dumps(kym_data, ensure_ascii=False, indent=4)}) 34 | db.session.add(kym) 35 | return restful.success('新增成功', data=kym.to_dict()) 36 | 37 | 38 | @test_work.route('/kym/project/list') 39 | @login_required 40 | def get_kym_project_list(): 41 | """ kym服务列表 """ 42 | project_list = KYMModule.query.with_entities(KYMModule.project).distinct().all() 43 | return restful.success('获取成功', data=[{'key': project[0], 'value': project[0]} for project in project_list]) 44 | 45 | 46 | @test_work.route('/kym/download', methods=['GET']) 47 | @login_required 48 | def export_kym_as_xmind(): 49 | """ 导出为xmind """ 50 | project = KYMModule.get_first(project=request.args.get("project")) 51 | file_path = os.path.join(TEMP_FILE_ADDRESS, f'{project.project}.xmind') 52 | if os.path.exists(file_path): 53 | os.remove(file_path) 54 | make_xmind(file_path, json.loads(project.kym)) 55 | return send_from_directory(TEMP_FILE_ADDRESS, f'{project.project}.xmind', as_attachment=True) 56 | 57 | 58 | class KYMView(BaseMethodView): 59 | """ KYM管理 """ 60 | 61 | def get(self): 62 | """ 获取KYM """ 63 | return restful.success('获取成功', data=KYMModule.get_first(project=request.args.get('project')).to_dict()) 64 | 65 | def put(self): 66 | """ 修改KYM号 """ 67 | kym = KYMModule.get_first(project=request.json['project']) 68 | kym.update({'kym': json.dumps(request.json['kym'], ensure_ascii=False, indent=4)}) 69 | return restful.success('修改成功', data=kym.to_dict()) 70 | 71 | 72 | test_work.add_url_rule('/kym', view_func=KYMView.as_view('kym')) 73 | -------------------------------------------------------------------------------- /app/test_work/swagger/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/31 11:32 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/swagger/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/31 11:34 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from app.baseModel import BaseModel, db 9 | 10 | 11 | class SwaggerDiffRecord(BaseModel): 12 | """ yapi数据比对记录 """ 13 | __tablename__ = 'swagger_diff_record' 14 | 15 | name = db.Column(db.String(255), comment='比对标识,全量比对,或者具体分组的比对') 16 | is_changed = db.Column(db.Integer, default=0, comment='对比结果,1有改变,0没有改变') 17 | diff_summary = db.Column(db.Text, comment='比对结果数据') 18 | 19 | @classmethod 20 | def make_pagination(cls, attr): 21 | """ 解析分页条件 """ 22 | filters = [] 23 | if attr.get('name'): 24 | filters.append(SwaggerDiffRecord.name.like(f'%{attr.get("name")}%')) 25 | if attr.get('create_user'): 26 | filters.append(SwaggerDiffRecord.create_user == attr.get('create_user')) 27 | return cls.pagination( 28 | page_num=attr.get('pageNum', 1), 29 | page_size=attr.get('pageSize', 20), 30 | filters=filters, 31 | order_by=cls.created_time.desc()) 32 | -------------------------------------------------------------------------------- /app/test_work/yapi/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 14:47 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/test_work/yapi/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/2 14:05 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from sqlalchemy.dialects.mysql import LONGTEXT 9 | from app.baseModel import BaseModel, db 10 | 11 | 12 | class YapiProject(BaseModel): 13 | """ yapi的服务表 """ 14 | __tablename__ = 'yapi_project' 15 | 16 | yapi_group = db.Column(db.Integer(), comment='当前服务归属于yapi平台分组的id') 17 | yapi_name = db.Column(db.String(255), comment='当前服务在yapi平台的名字') 18 | yapi_id = db.Column(db.Integer(), comment='当前服务在yapi平台的id') 19 | yapi_data = db.Column(db.Text, comment='当前服务在yapi平台的数据') 20 | 21 | 22 | class YapiModule(BaseModel): 23 | """ yapi的模块表 """ 24 | __tablename__ = 'yapi_module' 25 | 26 | yapi_project = db.Column(db.Integer(), comment='当前模块在yapi平台对应的服务id') 27 | yapi_name = db.Column(db.String(255), comment='当前模块在yapi平台的名字') 28 | yapi_id = db.Column(db.Integer(), comment='当前模块在yapi平台对应的模块id') 29 | yapi_data = db.Column(db.Text, comment='当前模块在yapi平台的数据') 30 | 31 | 32 | class YapiApiMsg(BaseModel): 33 | """ yapi的接口表 """ 34 | __tablename__ = 'yapi_apis' 35 | yapi_project = db.Column(db.Integer(), comment='当前接口在yapi平台对应的服务id') 36 | yapi_module = db.Column(db.Integer(), comment='当前接口在yapi平台对应的模块id') 37 | yapi_name = db.Column(db.String(255), comment='当前接口在yapi平台的名字') 38 | yapi_id = db.Column(db.Integer(), comment='当前接口在yapi平台对应的接口id') 39 | yapi_data = db.Column(LONGTEXT, comment='当前接口在yapi平台的数据') 40 | 41 | 42 | class YapiDiffRecord(BaseModel): 43 | """ yapi数据比对记录 """ 44 | __tablename__ = 'yapi_diff_record' 45 | 46 | name = db.Column(db.String(255), comment='比对标识,全量比对,或者具体分组的比对') 47 | is_changed = db.Column(db.Integer, default=0, comment='对比结果,1有改变,0没有改变') 48 | diff_summary = db.Column(db.Text, comment='比对结果数据') 49 | 50 | @classmethod 51 | def make_pagination(cls, attr): 52 | """ 解析分页条件 """ 53 | filters = [] 54 | if attr.get('name'): 55 | filters.append(YapiDiffRecord.name.like(f'%{attr.get("name")}%')) 56 | if attr.get('create_user'): 57 | filters.append(YapiDiffRecord.create_user == attr.get('create_user')) 58 | return cls.pagination( 59 | page_num=attr.get('pageNum', 1), 60 | page_size=attr.get('pageSize', 20), 61 | filters=filters, 62 | order_by=cls.created_time.desc()) 63 | -------------------------------------------------------------------------------- /app/tools/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from flask import Blueprint, current_app, request 5 | from flask_login import current_user 6 | 7 | from app.utils.log import logger 8 | 9 | tool = Blueprint('tool', __name__) 10 | tool.logger = logger 11 | 12 | from . import (errors) 13 | from app.tools import examination 14 | from app.tools import makeUser 15 | from app.tools import mockData 16 | 17 | 18 | @tool.before_request 19 | def before_request(): 20 | """ 前置钩子函数, 每个请求进来先经过此函数""" 21 | name = current_user.name if hasattr(current_user, 'name') else '' 22 | current_app.logger.info( 23 | f'[{request.remote_addr}] [{name}] [{request.method}] [{request.url}]: \n请求参数:{request.json}') 24 | 25 | 26 | @tool.after_request 27 | def after_request(response_obj): 28 | """ 后置钩子函数,每个请求最后都会经过此函数 """ 29 | if 'download' in request.path: 30 | return response_obj 31 | result = copy.copy(response_obj.response) 32 | if isinstance(result[0], bytes): 33 | result[0] = bytes.decode(result[0]) 34 | # 减少日志数据打印,跑用例的数据均不打印到日志 35 | if 'apiMsg/run' not in request.path and 'report/run' not in request.path and 'report/list' not in request.path: 36 | current_app.logger.info(f'{request.method}==>{request.url}, 返回数据:{json.loads(result[0])}') 37 | return response_obj 38 | -------------------------------------------------------------------------------- /app/tools/errors.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : errors.py 7 | # @Software: PyCharm 8 | import traceback 9 | 10 | import requests 11 | from flask import current_app, request 12 | 13 | from ..utils import restful 14 | from . import tool 15 | from config.config import conf 16 | 17 | 18 | @tool.app_errorhandler(404) 19 | def page_not_found(e): 20 | """ 捕获404的所有异常 """ 21 | # current_app.logger.exception(f'404错误url: {request.path}') 22 | return restful.url_not_find(msg=f'接口 {request.path} 不存在') 23 | 24 | 25 | @tool.app_errorhandler(Exception) 26 | def error_handler(e): 27 | """ 捕获所有服务器内部的异常 """ 28 | # 把错误发送到 即时达推送 的 系统错误 通道 29 | try: 30 | current_app.logger.error(f'系统出错了: {e}') 31 | requests.post( 32 | url=conf['error_push']['url'], 33 | json={ 34 | 'key': conf['error_push']['key'], 35 | 'head': f'{conf["SECRET_KEY"]}报错了', 36 | 'body': f'{e}' 37 | } 38 | ) 39 | except: 40 | pass 41 | current_app.logger.exception(f'触发错误url: {request.path}\n{traceback.format_exc()}') 42 | return restful.error(f'服务器异常: {e}') 43 | -------------------------------------------------------------------------------- /app/tools/examination.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 15:34 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : examination.py 7 | # @Software: PyCharm 8 | import json 9 | import os 10 | 11 | from app.tools import tool 12 | from app.utils import restful 13 | 14 | # 获取征信从业资格考试题目 15 | with open(os.path.join(os.path.dirname(__file__), 'zhengXinTest.json'), encoding='utf8') as file: 16 | zheng_xin_test_data = json.load(file) 17 | 18 | 19 | @tool.route('/examination', methods=['GET']) 20 | def get_test_data(): 21 | """ 征信考试 """ 22 | return restful.success('获取成功', data=zheng_xin_test_data) 23 | -------------------------------------------------------------------------------- /app/tools/makeUser.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/10/19 14:51 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : makeUser.py 7 | # @Software: PyCharm 8 | import json 9 | 10 | from faker import Faker 11 | from flask import request 12 | 13 | from app.tools import tool 14 | from app.utils import restful, makeUserTools 15 | from app.config.models import Config 16 | 17 | fake = Faker('zh_CN') 18 | 19 | 20 | @tool.route('/makeUserMapping', methods=['GET']) 21 | def get_make_user_info_mapping(): 22 | """ 获取生成用户信息可选项映射关系 """ 23 | return restful.success('获取成功', data=Config.get_first(name='make_user_info_mapping').value) 24 | 25 | 26 | @tool.route('/makeUser', methods=['GET']) 27 | def make_user_info(): 28 | """ 生成用户信息 """ 29 | args = request.args.to_dict() 30 | count, options, all_data = int(args.get('count')), json.loads(args.get('options')), [] 31 | for option in options: 32 | temp_data = [] 33 | if hasattr(fake, option) or option == 'credit_code': 34 | i = 0 35 | while True: 36 | if i >= count: 37 | break 38 | data = makeUserTools.get_credit_code() if option == 'credit_code' else getattr(fake, option)() 39 | if data not in temp_data: 40 | temp_data.append(data) 41 | i += 1 42 | all_data.append(temp_data) 43 | return restful.success('获取成功', data=[dict(zip(options, data)) for data in zip(*all_data)]) 44 | -------------------------------------------------------------------------------- /app/tools/mockData.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 15:35 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : mockData.py 7 | # @Software: PyCharm 8 | import io 9 | import json 10 | import os 11 | import time 12 | import datetime 13 | 14 | import requests 15 | from flask import request, jsonify 16 | 17 | from app.tools import tool 18 | from app.utils.globalVariable import CALL_BACK_ADDRESS 19 | from app.config.models import Config 20 | 21 | 22 | def send_msg_by_webhook(msg_type, msg): 23 | """ 回调数据源成功时发送消息 """ 24 | msg_format = { 25 | "msgtype": "text", 26 | "text": { 27 | "content": f"{msg}" 28 | } 29 | } 30 | try: 31 | print( 32 | f'{msg_type}发送企业微信:{requests.post(Config.get_first(name="callback_webhook").value, json=msg_format).json()}') 33 | except Exception as error: 34 | print(f'向企业微信发送{msg_type}失败,错误信息:\n{error}') 35 | 36 | 37 | def actions(action): 38 | """ 根据action执行不同的操作 """ 39 | if action == 'error': 40 | raise Exception('使用action参数触发的服务器内部错误') 41 | elif action == 'time_out': 42 | time.sleep(40) 43 | 44 | 45 | @tool.route('/mockData/autoTest', methods=['GET', 'POST']) 46 | def return_auto_test_mock_data(): 47 | """ 自动化测试模拟数据源 48 | 1.json参数接收什么就返回什么 49 | 2.args.action:查询字符串传参(非必传),在需要指定场景时使用,error、time_out、空 50 | { 51 | "action": "", # 指定事件,error为报错, time_out为等待40秒 52 | "is_async": "1", # 判断数据源是同步还是异步 53 | "addr": "", # 异步回调地址 54 | "token": "", # 异步回调地址的token 55 | } 56 | """ 57 | datas = request.json 58 | 59 | # action参数事件 60 | actions(datas.get('action')) 61 | 62 | # 根据是否有json参数判断是否为异步回调 63 | if datas and datas.get('is_async'): 64 | api_record_id, rating_request_id = datas.get('apiRecordId'), datas.get('ratingRequestId') 65 | try: 66 | # 发送异步回调 67 | res = requests.post( 68 | url=datas.get('addr', Config.get_first(name='data_source_callback_addr').value), 69 | headers={'x-auth-token': datas.get('token', Config.get_first(name='data_source_callback_token').value)}, 70 | json={ 71 | "applyType": 1, 72 | "code": 200, 73 | "apiRecordId": api_record_id, 74 | "ratingRequestId": rating_request_id, 75 | "message": "成功", 76 | "content": datas, 77 | "status": 200 78 | } 79 | ) 80 | msg = {"message": "异步数据源回调成功", "status": 200, "apiRecordId": api_record_id, "data": res.json()} 81 | except Exception as error: 82 | msg = {"message": "异步数据源回调失败", "status": 500, "apiRecordId": api_record_id, "data": str(error)} 83 | return jsonify(msg) 84 | return jsonify(datas) 85 | 86 | 87 | @tool.route('/mockData/common', methods=['GET', 'POST']) 88 | def return_mock_data(): 89 | """ 模拟数据源 90 | 1.json参数接收什么就返回什么 91 | 2.args.action:查询字符串传参(非必传),在需要指定场景时使用,error、time_out、空 92 | { 93 | "action": "", # 指定事件,error为报错, time_out为等待40秒 94 | "is_async": "1", # 判断数据源是同步还是异步 95 | "addr": "", # 异步回调地址 96 | "token": "", # 异步回调地址的token 97 | } 98 | """ 99 | datas = request.json 100 | 101 | # action参数事件 102 | actions(datas.get('action')) 103 | 104 | # 根据是否有json参数判断是否为异步回调 105 | if datas and datas.get('is_async'): 106 | api_record_id, rating_request_id = datas.get('apiRecordId'), datas.get('ratingRequestId') 107 | try: 108 | # 发送异步回调 109 | res = requests.post( 110 | url=Config.get_first(name='data_source_callback_addr').value, 111 | headers={'x-auth-token': Config.get_first(name='data_source_callback_token').value}, 112 | json={ 113 | "applyType": 1, 114 | "code": 200, 115 | "apiRecordId": api_record_id, 116 | "ratingRequestId": rating_request_id, 117 | "message": "成功", 118 | "content": datas, 119 | "status": 200 120 | } 121 | ) 122 | msg = {"message": "异步数据源回调成功", "status": 200, "apiRecordId": api_record_id, "data": res.json()} 123 | except Exception as error: 124 | msg = {"message": "异步数据源回调失败", "status": 500, "apiRecordId": api_record_id, "data": str(error)} 125 | send_msg_by_webhook('数据源回调结果', msg) 126 | return jsonify(msg) 127 | send_msg_by_webhook('数据源回调结果', {"message": "同步数据源回调成功", "status": 200}) 128 | return jsonify(datas) 129 | 130 | 131 | @tool.route('/callBack', methods=['GET', 'POST']) 132 | def call_back(): 133 | """ 回调接口 """ 134 | params, json_data, form_data = request.args.to_dict(), request.get_json(silent=True), request.form.to_dict() 135 | 136 | # 存回调数据 137 | name = f'callBack{datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")}.json' 138 | with io.open(os.path.join(CALL_BACK_ADDRESS, name), 'w', encoding='utf-8') as call_back_file: 139 | json.dump(json_data or form_data or params, call_back_file, ensure_ascii=False) 140 | send_msg_by_webhook('回调结果', f'已收到回调数据,保存文件名:{name}') 141 | 142 | return jsonify({ 143 | "timestamp": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), 144 | "status": 200, 145 | "message": "请求成功", 146 | "data": name}) 147 | 148 | 149 | @tool.route('/mock', methods=['GET', 'POST']) 150 | def mock_api(): 151 | """ mock_api, 收到什么就返回什么 """ 152 | params, json_data, form_data = request.args.to_dict(), request.get_json(silent=True), request.form.to_dict() 153 | return jsonify({ 154 | "timestamp": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())), 155 | "status": 200, 156 | "message": "请求成功", 157 | "data": json_data or form_data or params 158 | }) 159 | -------------------------------------------------------------------------------- /app/ucenter/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from flask import Blueprint, current_app, request 5 | from flask_login import current_user 6 | 7 | from app.utils.log import logger 8 | 9 | ucenter = Blueprint('user', __name__) 10 | ucenter.logger = logger 11 | 12 | from . import (errors) 13 | from app.ucenter.user import views 14 | 15 | 16 | @ucenter.before_request 17 | def before_request(): 18 | """ 前置钩子函数, 每个请求进来先经过此函数""" 19 | name = current_user.name if hasattr(current_user, 'name') else '' 20 | current_app.logger.info( 21 | f'[{request.remote_addr}] [{name}] [{request.method}] [{request.url}]: \n请求参数:{request.json}') 22 | 23 | 24 | @ucenter.after_request 25 | def after_request(response_obj): 26 | """ 后置钩子函数,每个请求最后都会经过此函数 """ 27 | if 'download' in request.path: 28 | return response_obj 29 | result = copy.copy(response_obj.response) 30 | if isinstance(result[0], bytes): 31 | result[0] = bytes.decode(result[0]) 32 | # 减少日志数据打印,跑用例的数据均不打印到日志 33 | if 'apiMsg/run' not in request.path and 'report/run' not in request.path and 'report/list' not in request.path: 34 | current_app.logger.info(f'{request.method}==>{request.url}, 返回数据:{json.loads(result[0])}') 35 | return response_obj 36 | -------------------------------------------------------------------------------- /app/ucenter/errors.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : errors.py 7 | # @Software: PyCharm 8 | import traceback 9 | 10 | import requests 11 | from flask import current_app, request 12 | 13 | from ..utils import restful 14 | from . import ucenter 15 | from config.config import conf 16 | 17 | 18 | @ucenter.app_errorhandler(404) 19 | def page_not_found(e): 20 | """ 捕获404的所有异常 """ 21 | # current_app.logger.exception(f'404错误url: {request.path}') 22 | return restful.url_not_find(msg=f'接口 {request.path} 不存在') 23 | 24 | 25 | @ucenter.app_errorhandler(Exception) 26 | def error_handler(e): 27 | """ 捕获所有服务器内部的异常 """ 28 | # 把错误发送到 即时达推送 的 系统错误 通道 29 | try: 30 | ucenter.logger.error(f'系统出错了: {e}') 31 | requests.post( 32 | url=conf['error_push']['url'], 33 | json={ 34 | 'key': conf['error_push']['key'], 35 | 'head': f'{conf["SECRET_KEY"]}报错了', 36 | 'body': f'{e}' 37 | } 38 | ) 39 | except: 40 | pass 41 | current_app.logger.exception(f'触发错误url: {request.path}\n{traceback.format_exc()}') 42 | return restful.error(f'服务器异常: {e}') 43 | -------------------------------------------------------------------------------- /app/ucenter/user/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/ucenter/user/forms.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : forms.py 7 | # @Software: PyCharm 8 | from flask_login import current_user 9 | from wtforms import StringField, IntegerField 10 | from wtforms.validators import ValidationError, Length, DataRequired 11 | 12 | from app.baseForm import BaseForm 13 | from .models import User, Role 14 | 15 | 16 | class CreateUserForm(BaseForm): 17 | """ 创建用户的验证 """ 18 | name = StringField(validators=[DataRequired('请设置用户名'), Length(2, 12, message='用户名长度为2~12位')]) 19 | account = StringField(validators=[DataRequired('请设置账号'), Length(2, 50, message='账号长度为2~50位')]) 20 | password = StringField(validators=[DataRequired('请设置密码'), Length(6, 18, message='密码长度长度为6~18位')]) 21 | role_id = IntegerField(validators=[DataRequired('请选择角色')]) 22 | 23 | def validate_name(self, field): 24 | """ 校验用户名不重复 """ 25 | if User.get_first(name=field.data): 26 | raise ValidationError(f'用户名 {field.data} 已存在') 27 | 28 | def validate_account(self, field): 29 | """ 校验账号不重复 """ 30 | if User.get_first(account=field.data): 31 | raise ValidationError(f'账号 {field.data} 已存在') 32 | 33 | def validate_role_id(self, field): 34 | """ 校验角色存在 """ 35 | if not Role.get_first(id=field.data): 36 | raise ValidationError(f'id为 {field.data} 的角色不存在') 37 | 38 | 39 | class ChangePasswordForm(BaseForm): 40 | """ 修改密码的校验 """ 41 | oldPassword = StringField(validators=[Length(6, 18, message='密码长度长度为6~18位')]) 42 | newPassword = StringField(validators=[Length(6, 18, message='密码长度长度为6~18位')]) 43 | surePassword = StringField(validators=[Length(6, 18, message='密码长度长度为6~18位')]) 44 | 45 | def validate_oldPassword(self, field): 46 | """ 校验旧密码是否正确 """ 47 | if not current_user.verify_password(field.data): 48 | raise ValidationError(f'旧密码 {field.data} 错误') 49 | 50 | def validate_surePassword(self, field): 51 | """ 校验两次密码是否一致 """ 52 | if self.newPassword.data != field.data: 53 | raise ValidationError(f'新密码 {self.newPassword} 与确认密码 {field.data} 不一致') 54 | 55 | 56 | class LoginForm(BaseForm): 57 | """ 登录校验 """ 58 | account = StringField(validators=[DataRequired('账号必填')]) 59 | password = StringField(validators=[DataRequired('密码必填')]) 60 | 61 | def validate_account(self, field): 62 | """ 校验账号 """ 63 | user = User.get_first(account=field.data) 64 | if user is None or not user.verify_password(self.password.data): 65 | raise ValidationError(f'账号或密码错误') 66 | if user.status == 0: 67 | raise ValidationError(f'账号 {field.data} 为冻结状态,请联系管理员') 68 | setattr(self, 'user', user) 69 | 70 | 71 | class FindUserForm(BaseForm): 72 | """ 查找用户参数校验 """ 73 | name = StringField() 74 | account = StringField() 75 | status = IntegerField() 76 | role_id = IntegerField() 77 | pageNum = IntegerField() 78 | pageSize = IntegerField() 79 | 80 | 81 | class GetUserEditForm(BaseForm): 82 | """ 返回待编辑用户信息 """ 83 | id = IntegerField(validators=[DataRequired('用户id必传')]) 84 | 85 | def validate_id(self, field): 86 | user = User.get_first(id=field.data) 87 | if not user: 88 | raise ValidationError(f'没有id为 {field.data} 的用户') 89 | setattr(self, 'user', user) 90 | 91 | 92 | class DeleteUserForm(GetUserEditForm): 93 | """ 删除用户 """ 94 | 95 | def validate_id(self, field): 96 | user = User.get_first(id=field.data) 97 | if not user: 98 | raise ValidationError(f'没有id为 {field.data} 的用户') 99 | if user.id == current_user.id: 100 | raise ValidationError('不能自己删自己') 101 | setattr(self, 'user', user) 102 | 103 | 104 | class ChangeStatusUserForm(GetUserEditForm): 105 | """ 改变用户状态 """ 106 | 107 | def validate_id(self, field): 108 | user = User.get_first(id=field.data) 109 | if not user: 110 | raise ValidationError(f'没有id为 {field.data} 的用户') 111 | setattr(self, 'user', user) 112 | 113 | 114 | class EditUserForm(GetUserEditForm, CreateUserForm): 115 | """ 编辑用户的校验 """ 116 | password = StringField() 117 | 118 | def validate_id(self, field): 119 | """ 校验id需存在 """ 120 | user = User.get_first(id=field.data) 121 | if not user: 122 | raise ValidationError(f'id为 {field.data} 的用户不存在') 123 | setattr(self, 'user', user) 124 | 125 | def validate_name(self, field): 126 | """ 校验用户名不重复 """ 127 | old_data = User.get_first(name=field.data) 128 | if old_data and old_data.name == field.data and old_data.id != self.id.data: 129 | raise ValidationError(f'用户名 {field.data} 已存在') 130 | 131 | def validate_account(self, field): 132 | """ 校验账号不重复 """ 133 | old_data = User.get_first(account=field.data) 134 | if old_data and old_data.account == field.data and old_data.id != self.id.data: 135 | raise ValidationError(f'账号 {field.data} 已存在') 136 | -------------------------------------------------------------------------------- /app/ucenter/user/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : models.py 7 | # @Software: PyCharm 8 | from werkzeug.security import check_password_hash, generate_password_hash 9 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 10 | from functools import wraps 11 | 12 | from flask_login import UserMixin, current_user 13 | from flask import current_app, request 14 | 15 | from app import login_manager 16 | from app.baseModel import BaseModel, db 17 | from app.utils import restful 18 | from config.config import conf 19 | 20 | # 角色 与 权限映射表 21 | roles_permissions = db.Table( 22 | 'roles_permissions', 23 | db.Column('role_id', db.Integer, db.ForeignKey('role.id')), 24 | db.Column('permission_id', db.Integer, db.ForeignKey('permission.id'))) 25 | 26 | 27 | class Role(BaseModel): 28 | """ 角色表 """ 29 | __tablename__ = 'role' 30 | name = db.Column(db.String(30), unique=True, comment='角色名称') 31 | users = db.relationship('User', back_populates='role') 32 | permission = db.relationship('Permission', secondary=roles_permissions, back_populates='role') 33 | 34 | 35 | class Permission(BaseModel): 36 | """ 角色对应的权限 """ 37 | __tablename__ = 'permission' 38 | name = db.Column(db.String(30), unique=True, comment='权限名称') 39 | role = db.relationship('Role', secondary=roles_permissions, back_populates='permission') 40 | 41 | 42 | class User(UserMixin, BaseModel): 43 | """ 用户表 """ 44 | __tablename__ = 'users' 45 | account = db.Column(db.String(50), unique=True, index=True, comment='账号') 46 | password_hash = db.Column(db.String(255), comment='密码') 47 | name = db.Column(db.String(12), comment='姓名') 48 | status = db.Column(db.Integer, default=1, comment='状态,1为启用,2为冻结') 49 | role_id = db.Column(db.Integer, db.ForeignKey('role.id'), comment='所属的角色id') 50 | role = db.relationship('Role', back_populates='users') 51 | 52 | @property 53 | def password(self): 54 | return self.password_hash 55 | 56 | @password.setter 57 | def password(self, _password): 58 | """ 设置加密密码 """ 59 | self.password_hash = generate_password_hash(_password) 60 | 61 | def verify_password(self, password): 62 | """ 校验密码 """ 63 | return check_password_hash(self.password_hash, password) 64 | 65 | @classmethod 66 | def make_pagination(cls, form): 67 | """ 解析分页条件 """ 68 | filters = [] 69 | if form.name.data: 70 | filters.append(User.name.like(f'%{form.name.data}%')) 71 | if form.account.data: 72 | filters.append(User.account.like(f'%{form.account.data}%')) 73 | if form.status.data: 74 | filters.append(User.status == form.status.data) 75 | if form.role_id.data: 76 | filters.append(User.role_id == form.role_id.data) 77 | return cls.pagination( 78 | page_num=form.pageNum.data, 79 | page_size=form.pageSize.data, 80 | filters=filters, 81 | order_by=cls.created_time.desc()) 82 | 83 | def to_dict(self, *args, **kwargs): 84 | return super(User, self).to_dict(pop_list=['password_hash']) 85 | 86 | def can(self, permission_name): 87 | """ 判断当前用户是否有当前请求的权限 """ 88 | permission = Permission.query.filter_by(name=permission_name).first() 89 | return permission is not None and self.role is not None and permission in self.role.permission 90 | 91 | 92 | @login_manager.user_loader 93 | def load_user(user_id): 94 | return User.query.get(int(user_id)) 95 | 96 | 97 | def login_required(func): 98 | """ 校验用户的登录状态 token""" 99 | 100 | @wraps(func) 101 | def decorated_view(*args, **kwargs): 102 | # 前端拦截器检测到响应为 '登录超时,请重新登录' ,自动跳转到登录页 103 | return func(*args, **kwargs) if User.parse_token(request.headers.get('X-Token')) else restful.fail('登录超时,请重新登录') 104 | 105 | return decorated_view 106 | 107 | 108 | def permission_required(permission_name): 109 | """ 校验当前用户是否有访问当前接口的权限 """ 110 | 111 | def decorator(func): 112 | @wraps(func) 113 | def decorated_function(*args, **kwargs): 114 | return restful.forbidden('没有该权限') if not current_user.can(permission_name) else func(*args, **kwargs) 115 | 116 | return decorated_function 117 | 118 | return decorator 119 | 120 | 121 | def admin_required(func): 122 | """ 校验是否为管理员权限 """ 123 | return permission_required('ADMINISTER')(func) 124 | -------------------------------------------------------------------------------- /app/ucenter/user/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : views.py 7 | # @Software: PyCharm 8 | from flask_login import login_user, logout_user, current_user 9 | 10 | from app.utils import restful 11 | from app.utils.required import admin_required, login_required, generate_reset_token 12 | from app.ucenter import ucenter 13 | from app.baseView import AdminMethodView 14 | from app.baseModel import db 15 | from .models import User, Role 16 | from .forms import (CreateUserForm, EditUserForm, ChangePasswordForm, LoginForm, FindUserForm, GetUserEditForm, 17 | DeleteUserForm, ChangeStatusUserForm) 18 | 19 | 20 | @ucenter.route('/role/list', methods=['GET']) 21 | @login_required 22 | def role_list(): 23 | """ 角色列表 """ 24 | return restful.success(data=[{'id': role.id, 'name': role.name} for role in Role.get_all()]) 25 | 26 | 27 | @ucenter.route('/list', methods=['GET']) 28 | @login_required 29 | def user_list(): 30 | """ 用户列表 """ 31 | form = FindUserForm() 32 | if form.validate(): 33 | return restful.success(data=User.make_pagination(form)) 34 | return restful.fail(form.get_error()) 35 | 36 | 37 | @ucenter.route('/login', methods=['POST']) 38 | def login(): 39 | """ 登录 """ 40 | form = LoginForm() 41 | if form.validate(): 42 | user = form.user 43 | login_user(user, remember=True) 44 | user_info, token = user.to_dict(), generate_reset_token(user) 45 | user_info['token'] = token 46 | return restful.success('登录成功', user_info) 47 | return restful.fail(msg=form.get_error()) 48 | 49 | 50 | @ucenter.route('/logout', methods=['GET']) 51 | # @login_required 52 | def logout(): 53 | """ 登出 """ 54 | logout_user() 55 | return restful.success(msg='登出成功') 56 | 57 | 58 | @ucenter.route('/password', methods=['PUT']) 59 | @login_required 60 | def user_password(): 61 | """ 修改密码 """ 62 | form = ChangePasswordForm() 63 | if form.validate(): 64 | with db.auto_commit(): 65 | current_user.password = form.newPassword.data 66 | return restful.success(f'密码已修改为 {form.newPassword.data}') 67 | return restful.fail(msg=form.get_error()) 68 | 69 | 70 | @ucenter.route('/status', methods=['PUT']) 71 | @admin_required 72 | @login_required 73 | def user_status(): 74 | """ 改变用户状态 """ 75 | form = ChangeStatusUserForm() 76 | if form.validate(): 77 | user = form.user 78 | with db.auto_commit(): 79 | user.status = 0 if user.status == 1 else 1 80 | return restful.success(f'{"禁用" if user.status == 0 else "启用"}成功') 81 | return restful.fail(form.get_error()) 82 | 83 | 84 | class UserView(AdminMethodView): 85 | """ 用户管理 """ 86 | 87 | def get(self): 88 | form = GetUserEditForm() 89 | if form.validate(): 90 | data = {'account': form.user.account, 'name': form.user.name, 'role_id': form.user.role_id} 91 | return restful.success(data=data) 92 | return restful.fail(form.get_error()) 93 | 94 | def post(self): 95 | form = CreateUserForm() 96 | if form.validate(): 97 | user = User().create(form.data) 98 | return restful.success(f'用户 {form.name.data} 新增成功', user.to_dict()) 99 | return restful.fail(msg=form.get_error()) 100 | 101 | def put(self): 102 | form = EditUserForm() 103 | if form.validate(): 104 | form.password.data = form.password.data or form.user.password # 若密码字段有值则修改密码,否则不修改密码 105 | form.user.update(form.data) 106 | return restful.success(f'用户 {form.user.name} 修改成功', form.user.to_dict()) 107 | return restful.fail(msg=form.get_error()) 108 | 109 | def delete(self): 110 | form = DeleteUserForm() 111 | if form.validate(): 112 | form.user.delete() 113 | return restful.success('删除成功') 114 | return restful.fail(form.get_error()) 115 | 116 | 117 | ucenter.add_url_rule('/', view_func=UserView.as_view('user')) 118 | -------------------------------------------------------------------------------- /app/ui_test/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | from flask import Blueprint, current_app, request 5 | from flask_login import current_user 6 | 7 | from app.utils.log import logger 8 | 9 | ui_test = Blueprint('uiTest', __name__) 10 | ui_test.logger = logger 11 | 12 | from . import (errors) 13 | # from app.ui_test.project import views 14 | 15 | 16 | @ui_test.before_request 17 | def before_request(): 18 | """ 前置钩子函数, 每个请求进来先经过此函数""" 19 | name = current_user.name if hasattr(current_user, 'name') else '' 20 | current_app.logger.info( 21 | f'[{request.remote_addr}] [{name}] [{request.method}] [{request.url}]: \n请求参数:{request.json}') 22 | 23 | 24 | @ui_test.after_request 25 | def after_request(response_obj): 26 | """ 后置钩子函数,每个请求最后都会经过此函数 """ 27 | if 'download' in request.path: 28 | return response_obj 29 | result = copy.copy(response_obj.response) 30 | if isinstance(result[0], bytes): 31 | result[0] = bytes.decode(result[0]) 32 | # 减少日志数据打印,跑用例的数据均不打印到日志 33 | if 'apiMsg/run' not in request.path and 'report/run' not in request.path and 'report/list' not in request.path: 34 | current_app.logger.info(f'{request.method}==>{request.url}, 返回数据:{json.loads(result[0])}') 35 | return response_obj 36 | -------------------------------------------------------------------------------- /app/ui_test/errors.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:10 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : errors.py 7 | # @Software: PyCharm 8 | import traceback 9 | 10 | import requests 11 | from flask import current_app, request 12 | 13 | from ..utils import restful 14 | from . import ui_test 15 | from config.config import conf 16 | 17 | 18 | @ui_test.app_errorhandler(404) 19 | def page_not_found(e): 20 | """ 捕获404的所有异常 """ 21 | # current_app.logger.exception(f'404错误url: {request.path}') 22 | return restful.url_not_find(msg=f'接口 {request.path} 不存在') 23 | 24 | 25 | @ui_test.app_errorhandler(Exception) 26 | def error_handler(e): 27 | """ 捕获所有服务器内部的异常 """ 28 | # 把错误发送到 即时达推送 的 系统错误 通道 29 | try: 30 | current_app.logger.error(f'系统出错了: {e}') 31 | requests.post( 32 | url=conf['error_push']['url'], 33 | json={ 34 | 'key': conf['error_push']['key'], 35 | 'head': f'{conf["SECRET_KEY"]}报错了', 36 | 'body': f'{e}' 37 | } 38 | ) 39 | except: 40 | pass 41 | current_app.logger.exception(f'触发错误url: {request.path}\n{traceback.format_exc()}') 42 | return restful.error(f'服务器异常: {e}') 43 | -------------------------------------------------------------------------------- /app/ui_test/page/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/ui_test/page/__init__.py -------------------------------------------------------------------------------- /app/ui_test/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/ui_test/project/__init__.py -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/2/25 17:42 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /app/utils/gitOperation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/17 11:15 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : gitOperation.py 7 | # @Software: PyCharm 8 | import os 9 | 10 | from git.repo import Repo 11 | from git.repo.fun import is_git_dir 12 | 13 | # from app.utils.globalVariable import GIT_FILE_ADDRESS 14 | 15 | GIT_FILE_ADDRESS = os.path.abspath(os.path.join(os.path.abspath('...'), '../../../' + r'/git_files/')) 16 | print(GIT_FILE_ADDRESS) 17 | 18 | 19 | class GitRepository: 20 | """ git仓库管理 """ 21 | 22 | def __init__(self, local_path, repo_url, branch='master'): 23 | self.local_path = local_path 24 | self.repo_url = repo_url 25 | self.repo = None 26 | self.initial(repo_url, branch) 27 | 28 | def initial(self, repo_url, branch): 29 | """ 初始化git仓库 """ 30 | if not os.path.exists(self.local_path): 31 | os.makedirs(self.local_path) 32 | 33 | git_local_path = os.path.join(self.local_path, '.git') 34 | if not is_git_dir(git_local_path): 35 | self.repo = Repo.clone_from(repo_url, to_path=self.local_path, branch=branch) 36 | else: 37 | self.repo = Repo(self.local_path) 38 | 39 | def pull(self): 40 | """ 从线上拉最新代码 """ 41 | self.repo.git.pull() 42 | 43 | def branches(self): 44 | """ 获取所有分支 """ 45 | branches = self.repo.remote().refs 46 | return [item.remote_head for item in branches if item.remote_head not in ['HEAD', ]] 47 | 48 | def commits(self): 49 | """ 获取所有提交记录 """ 50 | commit_log = self.repo.git.log( 51 | '--pretty={"commit":"%h","author":"%an","summary":"%s","date":"%cd"}', 52 | max_count=50, 53 | date='format:%Y-%m-%d %H:%M' 54 | ) 55 | log_list = commit_log.split("\n") 56 | return [eval(item) for item in log_list] 57 | 58 | def tags(self): 59 | """ 获取所有tag """ 60 | return [tag.name for tag in self.repo.tags] 61 | 62 | def change_to_branch(self, branch): 63 | """ 切换分支 """ 64 | self.repo.git.checkout(branch) 65 | 66 | def change_to_commit(self, branch, commit): 67 | """ 切换commit """ 68 | self.change_to_branch(branch=branch) 69 | self.repo.git.reset('--hard', commit) 70 | 71 | def change_to_tag(self, tag): 72 | """ 切换tag """ 73 | self.repo.git.checkout(tag) 74 | 75 | 76 | if __name__ == '__main__': 77 | # repo = GitRepository( 78 | # GIT_FILE_ADDRESS, 79 | # 'https://codeup.aliyun.com/5fbe3118672533690be72b12/xintech-fms/xintech-fms-admin-front.git' 80 | # ) 81 | # branch_list = repo.branches() 82 | # print(branch_list) 83 | # repo.change_to_branch('dev') 84 | # repo.pull() 85 | 86 | services_path = GIT_FILE_ADDRESS + '/src/services' 87 | for path in os.listdir(services_path): 88 | print(f'path: {path}, isfile: {os.path.isfile(services_path + "/" + path)}') 89 | -------------------------------------------------------------------------------- /app/utils/httprunner/__about__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'HttpRunner' 2 | __description__ = 'One-stop solution for HTTP(S) testing.' 3 | __url__ = 'https://github.com/HttpRunner/HttpRunner' 4 | __version__ = '2.0.6' 5 | __author__ = 'debugtalk' 6 | __author_email__ = 'mail@debugtalk.com' 7 | __license__ = 'Apache-2.0' 8 | __copyright__ = 'Copyright 2017 debugtalk' 9 | __cake__ = u'\u2728 \U0001f370 \u2728' -------------------------------------------------------------------------------- /app/utils/httprunner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/utils/httprunner/__init__.py -------------------------------------------------------------------------------- /app/utils/httprunner/compat.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """ 4 | httprunner.compat 5 | ~~~~~~~~~~~~~~~~~ 6 | 7 | This module handles import compatibility issues between Python 2 and 8 | Python 3. 9 | """ 10 | from collections import OrderedDict 11 | 12 | try: 13 | import simplejson as json 14 | except ImportError: 15 | import json 16 | 17 | import sys 18 | 19 | # ------- 20 | # Pythons 21 | # ------- 22 | 23 | # Syntax sugar. 24 | _ver = sys.version_info 25 | 26 | #: Python 2.x? 27 | is_py2 = (_ver[0] == 2) 28 | 29 | #: Python 3.x? 30 | is_py3 = (_ver[0] == 3) 31 | 32 | 33 | # --------- 34 | # Specifics 35 | # --------- 36 | 37 | try: 38 | JSONDecodeError = json.JSONDecodeError 39 | except AttributeError: 40 | JSONDecodeError = ValueError 41 | 42 | if is_py2: 43 | builtin_str = str 44 | bytes = str 45 | str = unicode 46 | basestring = basestring 47 | numeric_types = (int, long, float) 48 | integer_types = (int, long) 49 | 50 | FileNotFoundError = IOError 51 | 52 | elif is_py3: 53 | builtin_str = str 54 | str = str 55 | bytes = bytes 56 | basestring = (str, bytes) 57 | numeric_types = (int, float) 58 | integer_types = (int,) 59 | 60 | FileNotFoundError = FileNotFoundError 61 | -------------------------------------------------------------------------------- /app/utils/httprunner/exceptions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from .compat import JSONDecodeError, FileNotFoundError 4 | 5 | """ 失败类型的异常,这些异常会将测试标记为失败 """ 6 | 7 | 8 | class MyBaseFailure(Exception): 9 | pass 10 | 11 | 12 | class ValidationFailure(MyBaseFailure): 13 | pass 14 | 15 | 16 | class ExtractFailure(MyBaseFailure): 17 | pass 18 | 19 | 20 | class SetupHooksFailure(MyBaseFailure): 21 | pass 22 | 23 | 24 | class TeardownHooksFailure(MyBaseFailure): 25 | pass 26 | 27 | 28 | """ 错误类型异常,这些异常将把测试标记为错误 """ 29 | 30 | 31 | class MyBaseError(Exception): 32 | pass 33 | 34 | 35 | class FileFormatError(MyBaseError): 36 | pass 37 | 38 | 39 | class ParamsError(MyBaseError): 40 | pass 41 | 42 | 43 | class NotFoundError(MyBaseError): 44 | pass 45 | 46 | 47 | class FileNotFound(FileNotFoundError, NotFoundError): 48 | pass 49 | 50 | 51 | class FunctionNotFound(NotFoundError): 52 | pass 53 | 54 | 55 | class VariableNotFound(NotFoundError): 56 | pass 57 | 58 | 59 | class EnvNotFound(NotFoundError): 60 | pass 61 | 62 | 63 | class CSVNotFound(NotFoundError): 64 | pass 65 | 66 | 67 | class ApiNotFound(NotFoundError): 68 | pass 69 | 70 | 71 | class TestcaseNotFound(NotFoundError): 72 | pass 73 | -------------------------------------------------------------------------------- /app/utils/httprunner/locusts.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import io 4 | import multiprocessing 5 | import os 6 | import sys 7 | 8 | from .logger import color_print 9 | from . import loader 10 | 11 | 12 | def parse_locustfile(file_path): 13 | """ parse testcase file and return locustfile path. 14 | if file_path is a Python file, assume it is a locustfile 15 | if file_path is a YAML/JSON file, convert it to locustfile 16 | """ 17 | if not os.path.isfile(file_path): 18 | color_print("file path invalid, exit.", "RED") 19 | sys.exit(1) 20 | 21 | file_suffix = os.path.splitext(file_path)[1] 22 | if file_suffix == ".py": 23 | locustfile_path = file_path 24 | elif file_suffix in ['.yaml', '.yml', '.json']: 25 | locustfile_path = gen_locustfile(file_path) 26 | else: 27 | # '' or other suffix 28 | color_print("file type should be YAML/JSON/Python, exit.", "RED") 29 | sys.exit(1) 30 | 31 | return locustfile_path 32 | 33 | 34 | def gen_locustfile(testcase_file_path): 35 | """ generate locustfile from template. 36 | """ 37 | locustfile_path = 'locustfile.py' 38 | template_path = os.path.join( 39 | os.path.dirname(os.path.realpath(__file__)), 40 | "templates", 41 | "locustfile_template" 42 | ) 43 | 44 | with io.open(template_path, encoding='utf-8') as template: 45 | with io.open(locustfile_path, 'w', encoding='utf-8') as locustfile: 46 | template_content = template.read() 47 | template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path) 48 | locustfile.write(template_content) 49 | 50 | return locustfile_path 51 | 52 | 53 | def start_locust_main(): 54 | from locust.main import main 55 | main() 56 | 57 | 58 | def start_master(sys_argv): 59 | sys_argv.append("--master") 60 | sys.argv = sys_argv 61 | start_locust_main() 62 | 63 | 64 | def start_slave(sys_argv): 65 | if "--slave" not in sys_argv: 66 | sys_argv.extend(["--slave"]) 67 | 68 | sys.argv = sys_argv 69 | start_locust_main() 70 | 71 | 72 | def run_locusts_with_processes(sys_argv, processes_count): 73 | processes = [] 74 | manager = multiprocessing.Manager() 75 | 76 | for _ in range(processes_count): 77 | p_slave = multiprocessing.Process(target=start_slave, args=(sys_argv,)) 78 | p_slave.daemon = True 79 | p_slave.start() 80 | processes.append(p_slave) 81 | 82 | try: 83 | if "--slave" in sys_argv: 84 | [process.join() for process in processes] 85 | else: 86 | start_master(sys_argv) 87 | except KeyboardInterrupt: 88 | manager.shutdown() 89 | -------------------------------------------------------------------------------- /app/utils/httprunner/logger.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import logging 4 | import sys 5 | 6 | from colorama import Fore, init 7 | from colorlog import ColoredFormatter 8 | 9 | init(autoreset=True) 10 | 11 | log_colors_config = { 12 | 'DEBUG': 'cyan', 13 | 'INFO': 'green', 14 | 'WARNING': 'yellow', 15 | 'ERROR': 'red', 16 | 'CRITICAL': 'red', 17 | } 18 | logger = logging.getLogger("httprunner") 19 | 20 | 21 | def setup_logger(log_level, log_file=None): 22 | """setup root logger with ColoredFormatter.""" 23 | level = getattr(logging, log_level.upper(), None) 24 | if not level: 25 | color_print("Invalid log level: %s" % log_level, "RED") 26 | sys.exit(1) 27 | 28 | # hide traceback when log level is INFO/WARNING/ERROR/CRITICAL 29 | if level >= logging.INFO: 30 | sys.tracebacklimit = 0 31 | 32 | formatter = ColoredFormatter( 33 | u"%(log_color)s%(bg_white)s%(levelname)-8s%(reset)s %(message)s", 34 | datefmt=None, 35 | reset=True, 36 | log_colors=log_colors_config 37 | ) 38 | 39 | handler = logging.FileHandler(log_file, encoding="utf-8") if log_file else logging.StreamHandler() 40 | 41 | handler.setFormatter(formatter) 42 | logger.addHandler(handler) 43 | logger.setLevel(level) 44 | 45 | 46 | def coloring(text, color="WHITE"): 47 | fore_color = getattr(Fore, color.upper()) 48 | return fore_color + text 49 | 50 | 51 | def color_print(msg, color="WHITE"): 52 | fore_color = getattr(Fore, color.upper()) 53 | print(fore_color + msg) 54 | 55 | 56 | def log_with_color(level): 57 | """ 不同等级的日志用不同的颜色输出 """ 58 | def wrapper(text): 59 | color = log_colors_config[level.upper()] 60 | getattr(logger, level.lower())(coloring(text, color)) 61 | 62 | return wrapper 63 | 64 | 65 | log_debug = log_with_color("debug") 66 | log_info = log_with_color("info") 67 | log_warning = log_with_color("warning") 68 | log_error = log_with_color("error") 69 | log_critical = log_with_color("critical") 70 | -------------------------------------------------------------------------------- /app/utils/httprunner/templates/locustfile_template: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | import zmq 5 | from httprunner.exceptions import MyBaseError, MyBaseFailure 6 | from httprunner.api import prepare_locust_tests 7 | from httprunner.runner import Runner 8 | from locust import HttpLocust, TaskSet, task 9 | from locust.events import request_failure 10 | 11 | logging.getLogger().setLevel(logging.CRITICAL) 12 | logging.getLogger('locust.main').setLevel(logging.INFO) 13 | logging.getLogger('locust.runners').setLevel(logging.INFO) 14 | 15 | 16 | class WebPageTasks(TaskSet): 17 | def on_start(self): 18 | self.test_runner = Runner(self.locust.config, self.locust.functions, self.client) 19 | 20 | @task 21 | def test_any(self): 22 | test_dict = random.choice(self.locust.tests) 23 | try: 24 | self.test_runner.run_test(test_dict) 25 | except (AssertionError, MyBaseError, MyBaseFailure) as ex: 26 | request_failure.fire( 27 | request_type=self.test_runner.exception_request_type, 28 | name=self.test_runner.exception_name, 29 | response_time=0, 30 | exception=ex 31 | ) 32 | 33 | 34 | class WebPageUser(HttpLocust): 35 | task_set = WebPageTasks 36 | min_wait = 10 37 | max_wait = 30 38 | 39 | file_path = "$TESTCASE_FILE" 40 | locust_tests = prepare_locust_tests(file_path) 41 | functions = locust_tests["functions"] 42 | tests = locust_tests["tests"] 43 | config = {} 44 | 45 | host = config.get('base_url', '') 46 | -------------------------------------------------------------------------------- /app/utils/httprunner/validator.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import os 3 | import types 4 | 5 | """ validate data format 6 | TODO: refactor with JSON schema validate 7 | """ 8 | 9 | 10 | def is_testcase(data_structure): 11 | """ check if data_structure is a testcase. 12 | 13 | Args: 14 | data_structure (dict): testcase should always be in the following data structure: 15 | 16 | { 17 | "config": { 18 | "name": "desc1", 19 | "variables": [], # optional 20 | "request": {} # optional 21 | }, 22 | "teststeps": [ 23 | test_dict1, 24 | { # test_dict2 25 | 'name': 'test step desc2', 26 | 'variables': [], # optional 27 | 'extract': [], # optional 28 | 'validate': [], 29 | 'request': {}, 30 | 'function_meta': {} 31 | } 32 | ] 33 | } 34 | 35 | Returns: 36 | bool: True if data_structure is valid testcase, otherwise False. 37 | 38 | """ 39 | # TODO: replace with JSON schema validation 40 | if not isinstance(data_structure, dict): 41 | return False 42 | 43 | if "teststeps" not in data_structure: 44 | return False 45 | 46 | if not isinstance(data_structure["teststeps"], list): 47 | return False 48 | 49 | return True 50 | 51 | 52 | def is_testcases(data_structure): 53 | """ 判断 data_structure 是 testcase 还是 testcases 列表 54 | Args: 55 | data_structure (dict): testcase(s)为以下固定结构: 56 | { 57 | "project_mapping": { 58 | "PWD": "XXXXX", 59 | "functions": {}, 60 | "env": {} 61 | }, 62 | "testcases": [ 63 | { # testcase data structure 64 | "config": { 65 | "name": "desc1", 66 | "path": "testcase1_path", 67 | "variables": [], # optional 68 | }, 69 | "teststeps": [ 70 | # test data structure 71 | { 72 | 'name': 'test step desc1', 73 | 'variables': [], # optional 74 | 'extract': [], # optional 75 | 'validate': [], 76 | 'request': {} 77 | }, 78 | test_dict_2 # another test dict 79 | ] 80 | }, 81 | testcase_dict_2 # another testcase dict 82 | ] 83 | } 84 | 85 | Returns: 86 | 如果是testcase 或者 testcases 列表,则返回True,否则返回False 87 | """ 88 | if not isinstance(data_structure, dict): 89 | return False 90 | return True 91 | 92 | 93 | def is_testcase_path(path): 94 | """ 判断path是否为 文件路径 或 路径列表,如果路径是有效的文件路径或路径列表,返回True,否则返回False """ 95 | if not isinstance(path, (str, list)): 96 | return False 97 | 98 | if isinstance(path, list): 99 | for p in path: 100 | if not is_testcase_path(p): 101 | return False 102 | 103 | if isinstance(path, str): 104 | if not os.path.exists(path): 105 | return False 106 | 107 | return True 108 | 109 | 110 | # 验证变量和函数 111 | 112 | def is_function(item): 113 | """ 判断传进来的 item对象 是否为函数 """ 114 | return isinstance(item, types.FunctionType) 115 | 116 | 117 | def is_variable(tup): 118 | """ 接受(name,object)元组,如果它是变量,则返回True """ 119 | name, item = tup 120 | # if callable(item): # 类或者方法 121 | # return False 122 | # 123 | # if isinstance(item, types.ModuleType): # 模块 124 | # return False 125 | # 126 | # if name.startswith("_"): # 私有属性 127 | # return False 128 | 129 | if callable(item) or isinstance(item, types.ModuleType) or name.startswith("_"): # 类或者方法、模块、私有属性 130 | return False 131 | 132 | return True 133 | -------------------------------------------------------------------------------- /app/utils/jsonUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/2/4 10:50 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : json_event.py 7 | # @Software: PyCharm 8 | import json 9 | 10 | 11 | class JsonUtil: 12 | """ 处理json事件,主要是在dumps时处理编码问题 """ 13 | 14 | @classmethod 15 | def dump(cls, obj, fp, *args, **kwargs): 16 | """ json.dump """ 17 | kwargs.setdefault('ensure_ascii', False) 18 | kwargs.setdefault('indent', 4) 19 | return json.dump(obj, fp, *args, **kwargs) 20 | 21 | @classmethod 22 | def dumps(cls, obj, *args, **kwargs): 23 | """ json.dumps """ 24 | kwargs.setdefault('ensure_ascii', False) 25 | kwargs.setdefault('indent', 4) 26 | return json.dumps(obj, *args, **kwargs) 27 | 28 | @classmethod 29 | def loads(cls, obj, *args, **kwargs): 30 | """ json.loads """ 31 | return json.loads(obj, *args, **kwargs) 32 | 33 | @classmethod 34 | def load(cls, fp, *args, **kwargs): 35 | """ json.load """ 36 | return json.load(fp, *args, **kwargs) 37 | 38 | @classmethod 39 | def field_to_json(cls, dict_data: dict, *args): 40 | """ 把字典中已存在的key的值转为json """ 41 | for key in args: 42 | if key in dict_data: 43 | dict_data[key] = cls.dumps(dict_data[key]) 44 | return dict_data 45 | 46 | 47 | if __name__ == '__main__': 48 | d = { 49 | 'a': {'a1': 'b2'}, 50 | 'b': {'b1', 'b2'} 51 | } 52 | -------------------------------------------------------------------------------- /app/utils/makeUserTools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/8/28 14:56 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : makeUserTools.py 7 | # @Software: PyCharm 8 | from random import randint, choice 9 | 10 | 11 | def get_organization_code(): 12 | """ 生成组织机构代码 """ 13 | ww, cc, dd = [3, 7, 9, 10, 5, 8, 4, 2], [], 0 14 | 15 | for i in range(8): 16 | cc.append(randint(1, 9)) 17 | dd = dd + cc[i] * ww[i] 18 | for i in range(len(cc)): 19 | cc[i] = str(cc[i]) 20 | C9 = 11 - dd % 11 21 | cc.append('X' if C9 == 10 else str(C9)) 22 | return "".join(cc) 23 | 24 | 25 | def get_credit_code(): 26 | """ 27 | 生成统一社会信用代码 28 | 统一社会信用代码规则:https://wenku.baidu.com/view/0b6dfd98162ded630b1c59eef8c75fbfc67d944f.html 29 | """ 30 | 31 | # 第一位,登记管理部门代码 32 | num1 = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'Y'] 33 | 34 | # 第二位,机构类别代码 35 | num2 = ['1', '2', '3', '9', 'I'] 36 | 37 | # 第3-8位,登记管理机关行政区域划码:写死为北京市的 38 | # 详见:https://wenku.baidu.com/view/a3576ab13968011ca30091ec.html 39 | num3 = ['110100', '110101', '110102', '110103', '110104', '110105', '110106', '110107', '110108', '110109', 40 | '110110', '110111', '110112', '110113', '110114', '110115', '110116', '110117', '110200', '110228', 41 | '110229'] 42 | 43 | # 第9-17位,组织机构代码 44 | num4 = get_organization_code() 45 | 46 | # 第18位,校验码,数字或大写英文字母 47 | num5 = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 48 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 49 | 'V', 'W', 'X', 'Y', 'Z'] 50 | 51 | return choice(num1) + choice(num2) + choice(num3) + num4 + choice(num5) 52 | 53 | 54 | if __name__ == '__main__': 55 | print(get_credit_code()) 56 | -------------------------------------------------------------------------------- /app/utils/makeXmind.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/16 17:23 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : makeXmind.py 7 | # @Software: PyCharm 8 | import xmind 9 | 10 | 11 | def make_xmind(file_name, all_data): 12 | """ 创建xmind文件 """ 13 | workbook = xmind.load(file_name) 14 | first_sheet = workbook.getPrimarySheet() # 获取第一个画布 15 | first_sheet.setTitle(all_data.get("nodeData", {}).get("topic", {})) # 设置画布名称 16 | root_topic = first_sheet.getRootTopic() # 获取画布中心主题,默认创建画布时会新建一个空白中心主题 17 | root_topic.setTitle(all_data.get("nodeData", {}).get("topic", {})) # 设置主题名称 18 | 19 | def make_data(topic, tree_data): 20 | for data in tree_data: 21 | sub_topic = topic.addSubTopic() 22 | sub_topic.setTitle(data.get("topic")) 23 | make_data(sub_topic, data.get("children", [])) 24 | 25 | make_data(root_topic, all_data.get("nodeData", {}).get("children", [])) 26 | xmind.save(workbook=workbook, path=file_name) 27 | -------------------------------------------------------------------------------- /app/utils/parseCron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/2/25 17:39 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : parseCron.py 7 | # @Software: PyCharm 8 | 9 | def parse_cron(expression): 10 | """ 解析定时任务的时间 11 | 入参 '0,30 1-55 * * * * ' 12 | 返回 {'second': '0,30', 'minute': '1-55', 'hour': '*', 'day': '*', 'month': '*', 'day_of_week': '*'} 13 | """ 14 | args = {} 15 | expression = expression.split(' ') 16 | if expression[0] != '?': 17 | args['second'] = expression[0] 18 | if expression[1] != '?': 19 | args['minute'] = expression[1] 20 | if expression[2] != '?': 21 | args['hour'] = expression[2] 22 | if expression[3] != '?': 23 | args['day'] = expression[3] 24 | if expression[4] != '?': 25 | args['month'] = expression[4] 26 | if expression[5] != '?': 27 | args['day_of_week'] = expression[5] 28 | return args 29 | -------------------------------------------------------------------------------- /app/utils/parseExcel.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/3/11 16:58 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : parseExcel.py 7 | # @Software: PyCharm 8 | import xlrd 9 | 10 | 11 | def parse_file_content(file_contents): 12 | """ 从请求流中解析Excel文件 """ 13 | sheet, all_data = xlrd.open_workbook(file_contents=file_contents).sheet_by_index(0), [] 14 | for row in range(1, sheet.nrows): 15 | row_data = {} 16 | for col in range(sheet.ncols): 17 | row_data[sheet.cell_value(0, col)] = sheet.cell_value(row, col) # {'列title': '列值'} 18 | all_data.append(row_data) 19 | return all_data 20 | 21 | 22 | if __name__ == '__main__': 23 | pass 24 | -------------------------------------------------------------------------------- /app/utils/regexp.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | variable_regexp = r"\$([\w_]+)" # 变量 4 | function_regexp = r"\$\{([\w_]+\([\$\w\.\-/_ =,]*\))\}" # 自定义函数 5 | function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w\.\-/_ =,]*)\)$") 6 | -------------------------------------------------------------------------------- /app/utils/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/app/utils/report/__init__.py -------------------------------------------------------------------------------- /app/utils/report/report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : report.py 7 | # @Software: PyCharm 8 | 9 | import io 10 | import os 11 | from flask import render_template 12 | from jinja2 import Template 13 | 14 | 15 | def render_html_report(summary): 16 | """ httprunner原生模板 渲染html报告文件 """ 17 | report_template = os.path.join(os.path.abspath(os.path.dirname(__file__)), r"report_template.html") 18 | # report_template = os.path.join(os.path.abspath(os.path.dirname(__file__)), r"extent_report_template.html") 19 | with io.open(report_template, "r", encoding='utf-8') as fp_r: 20 | template_content = fp_r.read() 21 | rendered_content = Template(template_content, extensions=["jinja2.ext.loopcontrols"]).render(summary) 22 | 23 | return rendered_content 24 | -------------------------------------------------------------------------------- /app/utils/required.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : required.py 7 | # @Software: PyCharm 8 | from functools import wraps 9 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 10 | 11 | from flask import current_app, request 12 | from flask_login import current_user 13 | 14 | from app.utils import restful 15 | from config.config import conf 16 | 17 | 18 | def generate_reset_token(user, expiration=conf['token_time_out']): 19 | """ 生成token,默认有效期一个小时 """ 20 | return Serializer(current_app.config['SECRET_KEY'], expiration).dumps( 21 | {'id': user.id, 'name': user.name}).decode('utf-8') 22 | 23 | 24 | def parse_token(token): 25 | """ 校验token是否过期,或者是否合法 """ 26 | try: 27 | data = Serializer(current_app.config['SECRET_KEY']).loads(token.encode('utf-8')) 28 | return data 29 | except: 30 | return False 31 | 32 | 33 | def login_required(func): 34 | """ 校验用户的登录状态 token""" 35 | 36 | @wraps(func) 37 | def decorated_view(*args, **kwargs): 38 | # 前端拦截器检测到响应为 '登录超时,请重新登录' ,自动跳转到登录页 39 | return func(*args, **kwargs) if parse_token(request.headers.get('X-Token')) else restful.fail('登录超时,请重新登录') 40 | 41 | return decorated_view 42 | 43 | 44 | def permission_required(permission_name): 45 | """ 校验当前用户是否有访问当前接口的权限 """ 46 | 47 | def decorator(func): 48 | @wraps(func) 49 | def decorated_function(*args, **kwargs): 50 | return restful.forbidden('没有该权限') if not current_user.can(permission_name) else func(*args, **kwargs) 51 | 52 | return decorated_function 53 | 54 | return decorator 55 | 56 | 57 | def admin_required(func): 58 | """ 校验是否为管理员权限 """ 59 | return permission_required('ADMINISTER')(func) 60 | -------------------------------------------------------------------------------- /app/utils/restful.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : restful.py 7 | # @Software: PyCharm 8 | from flask import jsonify 9 | 10 | 11 | class HttpCode: 12 | """ 定义一些约定好的业务处理状态 """ 13 | 14 | success = {'code': 200, 'message': '处理成功'} 15 | fail = {'code': 400, 'message': '处理失败'} 16 | forbidden = {'code': 403, 'message': '权限不足'} 17 | not_find = {'code': 404, 'message': 'url不存在'} 18 | error = {'code': 500, 'message': '系统出错了,请联系开发人员查看'} 19 | 20 | 21 | def restful_result(code, message, data, **kwargs): 22 | """ 统一返 result风格 """ 23 | return jsonify({'status': code, 'message': message, 'data': data, **kwargs}) 24 | 25 | 26 | def success(msg=HttpCode.success['message'], data=None, **kwargs): 27 | """ 业务处理成功的响应 """ 28 | return restful_result(code=HttpCode.success['code'], message=msg, data=data, **kwargs) 29 | 30 | 31 | def get_success(data=None, **kwargs): 32 | """ 数据获取成功的响应 """ 33 | return success(msg='获取成功', data=data, **kwargs) 34 | 35 | 36 | def fail(msg=HttpCode.fail['message'], data=None, **kwargs): 37 | """ 业务处理失败的响应 """ 38 | return restful_result(code=HttpCode.fail['code'], message=msg, data=data, **kwargs) 39 | 40 | 41 | def forbidden(msg=HttpCode.forbidden['message'], data=None, **kwargs): 42 | """ 权限不足的响应 """ 43 | return restful_result(code=HttpCode.forbidden['code'], message=msg, data=data, **kwargs) 44 | 45 | 46 | def url_not_find(msg=HttpCode.not_find['message'], data=None, **kwargs): 47 | """ url不存在的响应 """ 48 | return restful_result(code=HttpCode.not_find['code'], message=msg, data=data, **kwargs) 49 | 50 | 51 | def error(msg=HttpCode.error['message'], data=None, **kwargs): 52 | """ 系统发送错误的响应 """ 53 | return restful_result(code=HttpCode.error['code'], message=msg, data=data, **kwargs) 54 | -------------------------------------------------------------------------------- /app/utils/sendEmail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : SendEmail.py 7 | # @Software: PyCharm 8 | import smtplib 9 | from email.mime.multipart import MIMEMultipart 10 | from email.mime.text import MIMEText 11 | from email.header import Header 12 | 13 | 14 | class SendEmail: 15 | """ 发送测试报告到邮箱 """ 16 | 17 | def __init__(self, email_server, username, password, to_list, file): 18 | self.email_server = email_server 19 | self.username = username 20 | self.password = password 21 | self.to_list = to_list 22 | self.file = file 23 | 24 | def send_email(self): 25 | """ 使用第三方SMTP服务发送邮件 """ 26 | message = MIMEMultipart() 27 | body = MIMEText(_text=self.file, _subtype='html', _charset='utf-8') # 邮件正文内容为报告附件body 28 | message.attach(body) 29 | message['From'] = Header("测试报告", 'utf-8') 30 | message['To'] = Header(''.join(self.to_list), 'utf-8') 31 | subject = '接口自动化测试报告邮件' if '>失败' not in self.file else '接口自动化测试报告邮件,有执行失败的用例,请查看附件或登录平台查看' 32 | message['Subject'] = Header(subject, 'utf-8') 33 | 34 | # 添加附件 35 | att = MIMEText(self.file, "base64", "utf-8") 36 | att["Content-Type"] = "application/octet-stream" 37 | att["Content-Disposition"] = 'attachment; filename= "report.html"' 38 | message.attach(att) 39 | 40 | try: 41 | # 发送邮件 42 | print(f'{"=" * 30} 开始发送邮件,发件箱为 {self.username} {"=" * 30}') 43 | service = smtplib.SMTP_SSL(host=self.email_server, port=smtplib.SMTP_SSL_PORT) 44 | service.login(user=self.username, password=self.password) # 登录 45 | service.sendmail(from_addr=self.username, to_addrs=self.to_list, msg=message.as_string()) 46 | print(f'{"=" * 30} 邮件发送成功 {"=" * 30}') 47 | service.close() 48 | except Exception as error: 49 | print(f'发送邮件出错,错误信息为:\n {error}') 50 | -------------------------------------------------------------------------------- /app/utils/yamlUtil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : yamlUtil.py 7 | # @Software: PyCharm 8 | import yaml 9 | 10 | 11 | def load(file): 12 | """ 读取yaml """ 13 | with open(file, 'r', encoding='utf-8') as fr: 14 | data = yaml.load(fr, yaml.FullLoader) 15 | return data 16 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/5/23 9:22 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : config.py 7 | # @Software: PyCharm 8 | 9 | import os 10 | import email 11 | import six 12 | 13 | import urllib3.fields as f 14 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore 15 | 16 | from app.utils.yamlUtil import load 17 | from app.utils.httprunner import built_in as assert_func_file 18 | 19 | # 从 httpRunner.built_in 中获取断言方式并映射为字典和列表,分别给前端和运行测试用例时反射断言 20 | assert_mapping, assert_mapping_list = {}, [] 21 | for func in dir(assert_func_file): 22 | if func.startswith('_') and not func.startswith('__'): 23 | doc = getattr(assert_func_file, func).__doc__.strip() # 函数注释 24 | assert_mapping.setdefault(doc, func) 25 | assert_mapping_list.append({'value': doc}) 26 | 27 | basedir, conf = os.path.abspath('.'), load(os.path.abspath('.') + '/config/config.yaml') 28 | 29 | 30 | def my_format_header_param(name, value): 31 | if not any(ch in value for ch in '"\\\r\n'): 32 | result = '%s="%s"' % (name, value) 33 | try: 34 | result.encode('utf-8') 35 | except (UnicodeEncodeError, UnicodeDecodeError): 36 | pass 37 | else: 38 | return result 39 | if not six.PY3 and isinstance(value, six.text_type): # Python 2: 40 | value = value.encode('utf-8') 41 | value = email.utils.encode_rfc2231(value, 'utf-8') 42 | value = '%s*=%s' % (name, value) 43 | return value 44 | 45 | 46 | # 猴子补丁,修复request上传文件时,不能传中文 47 | f.format_header_param = my_format_header_param 48 | 49 | 50 | class ProductionConfig: 51 | """ 生产环境数据库 """ 52 | SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://' \ 53 | f'{conf["db"]["user"]}:' \ 54 | f'{conf["db"]["password"]}@' \ 55 | f'{conf["db"]["host"]}:' \ 56 | f'{conf["db"]["port"]}/' \ 57 | f'{conf["db"]["database"]}?charset=utf8mb4' 58 | SCHEDULER_JOBSTORES = { 59 | 'default': SQLAlchemyJobStore(url=SQLALCHEMY_DATABASE_URI, engine_options={'pool_pre_ping': True}) 60 | } 61 | SQLALCHEMY_TRACK_MODIFICATIONS = False 62 | SQLALCHEMY_POOL_SIZE = 1000 63 | SQLALCHEMY_POOL_RECYCLE = 1800 64 | 65 | SECRET_KEY = conf['SECRET_KEY'] 66 | basedir = os.path.abspath(os.path.dirname(__file__)) 67 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 68 | CSRF_ENABLED = True 69 | UPLOAD_FOLDER = '/upload' 70 | SCHEDULER_API_ENABLED = True 71 | 72 | @staticmethod 73 | def init_app(app): 74 | pass 75 | 76 | 77 | if __name__ == '__main__': 78 | pass 79 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | SECRET_KEY: '123qwe' # 任意字符串 2 | 3 | # 数据库信息 4 | db: 5 | host: '' 6 | port: 3306 7 | user: '' 8 | password: '' 9 | database: '' 10 | 11 | # token超时时间,单位为秒 12 | token_time_out: 36000 13 | 14 | # 分页信息 15 | page: 16 | pageNum: 1 17 | pageSize: 20 18 | 19 | # 企业群消息机器人地址,用于接收报告 20 | webhook: '' 21 | 22 | # 即时通讯消息里面映射的前端地址 23 | report_addr: 'http://xx.xx.xx.xx/#/apiTest/reportShow?id=' # 展示测试报告页面的前端地址 24 | diff_addr: 'http://xx.xx.xx.xx/#/testManage/diffRecordShow?id=' # 展示监控报告页面的前端地址 25 | error_addr: 'http://xx.xx.xx.xx/#/apiTest/errorRecord' # 展示自定义函数错误记录的前端地址 26 | 27 | # 即时达推送 的 系统错误通道 28 | error_push: 29 | url: 'http://push.ijingniu.cn/send' 30 | key: '' 31 | -------------------------------------------------------------------------------- /gunicornConfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : gunicornConfig.py 7 | # @Software: PyCharm 8 | 9 | import gevent.monkey 10 | import multiprocessing 11 | 12 | """ 13 | gunicorn的配置文件 14 | """ 15 | 16 | gevent.monkey.patch_all() # gevent的猴子魔法 变成非阻塞 17 | 18 | bind = '0.0.0.0:8024' # 访问地址 19 | 20 | workers = multiprocessing.cpu_count() * 2 + 1 # 启动的进程数,cpu个数 * 2 + 1 21 | worker_class = 'gevent' # 使用gevent模式,还可以使用sync 模式,默认的是sync模式 22 | threads = 20 # 每个进程开启的线程数 23 | x_forwarded_for_header = 'X_FORWARDED-FOR' 24 | -------------------------------------------------------------------------------- /images/api/form-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/form-data.png -------------------------------------------------------------------------------- /images/api/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/json.png -------------------------------------------------------------------------------- /images/api/xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/xml.png -------------------------------------------------------------------------------- /images/api/头部信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/头部信息.png -------------------------------------------------------------------------------- /images/api/拖拽排序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/拖拽排序.png -------------------------------------------------------------------------------- /images/api/接口概要信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/接口概要信息.png -------------------------------------------------------------------------------- /images/api/提取信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/提取信息.png -------------------------------------------------------------------------------- /images/api/数据提取.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/数据提取.png -------------------------------------------------------------------------------- /images/api/断言.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/断言.png -------------------------------------------------------------------------------- /images/api/新增接口入口.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/新增接口入口.png -------------------------------------------------------------------------------- /images/api/查询字符串参数.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/查询字符串参数.png -------------------------------------------------------------------------------- /images/api/测试报告.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/测试报告.png -------------------------------------------------------------------------------- /images/api/请求信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/请求信息.png -------------------------------------------------------------------------------- /images/api/请求方式配置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/请求方式配置.png -------------------------------------------------------------------------------- /images/api/调试接口.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/调试接口.png -------------------------------------------------------------------------------- /images/api/运行接口.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/运行接口.png -------------------------------------------------------------------------------- /images/api/返回信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/返回信息.png -------------------------------------------------------------------------------- /images/api/返回结果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/api/返回结果.png -------------------------------------------------------------------------------- /images/case/修改用例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/修改用例.png -------------------------------------------------------------------------------- /images/case/公用变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/公用变量.png -------------------------------------------------------------------------------- /images/case/复制用例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/复制用例.png -------------------------------------------------------------------------------- /images/case/头部信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/头部信息.png -------------------------------------------------------------------------------- /images/case/引用用例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/引用用例.png -------------------------------------------------------------------------------- /images/case/拖拽排序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/拖拽排序.png -------------------------------------------------------------------------------- /images/case/添加用例入口.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/添加用例入口.png -------------------------------------------------------------------------------- /images/case/用例信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/用例信息.png -------------------------------------------------------------------------------- /images/case/运行用例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/运行用例.png -------------------------------------------------------------------------------- /images/case/运行用例结果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/case/运行用例结果.png -------------------------------------------------------------------------------- /images/config/全局参数设置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/config/全局参数设置.png -------------------------------------------------------------------------------- /images/config/配置类型管理.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/config/配置类型管理.png -------------------------------------------------------------------------------- /images/config/配置类型管理_数据库.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/config/配置类型管理_数据库.png -------------------------------------------------------------------------------- /images/file/文件管理.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/file/文件管理.png -------------------------------------------------------------------------------- /images/funcFile/使用自定义函数.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/funcFile/使用自定义函数.png -------------------------------------------------------------------------------- /images/funcFile/引用函数文件_用例管理处.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/funcFile/引用函数文件_用例管理处.png -------------------------------------------------------------------------------- /images/funcFile/引用函数文件_项目管理处.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/funcFile/引用函数文件_项目管理处.png -------------------------------------------------------------------------------- /images/funcFile/新建函数文件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/funcFile/新建函数文件.png -------------------------------------------------------------------------------- /images/funcFile/调试函数.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/funcFile/调试函数.png -------------------------------------------------------------------------------- /images/home/首页统计图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/home/首页统计图.png -------------------------------------------------------------------------------- /images/module/模块内容.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/module/模块内容.png -------------------------------------------------------------------------------- /images/module/模块操作.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/module/模块操作.png -------------------------------------------------------------------------------- /images/module/添加模块.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/module/添加模块.png -------------------------------------------------------------------------------- /images/project/使用公用变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/project/使用公用变量.png -------------------------------------------------------------------------------- /images/project/公用变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/project/公用变量.png -------------------------------------------------------------------------------- /images/project/头部信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/project/头部信息.png -------------------------------------------------------------------------------- /images/project/添加项目.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/project/添加项目.png -------------------------------------------------------------------------------- /images/report/测试报告.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/report/测试报告.png -------------------------------------------------------------------------------- /images/step/保存步骤.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/step/保存步骤.png -------------------------------------------------------------------------------- /images/step/接口转步骤.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/step/接口转步骤.png -------------------------------------------------------------------------------- /images/step/数据驱动.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/step/数据驱动.png -------------------------------------------------------------------------------- /images/step/步骤信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/step/步骤信息.png -------------------------------------------------------------------------------- /images/task/webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/webhook.png -------------------------------------------------------------------------------- /images/task/任务名称.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/任务名称.png -------------------------------------------------------------------------------- /images/task/任务概要信息.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/任务概要信息.png -------------------------------------------------------------------------------- /images/task/修改任务.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/修改任务.png -------------------------------------------------------------------------------- /images/task/修改任务状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/修改任务状态.png -------------------------------------------------------------------------------- /images/task/发送报告.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/发送报告.png -------------------------------------------------------------------------------- /images/task/定时任务编辑框.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/定时任务编辑框.png -------------------------------------------------------------------------------- /images/task/微信群.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/微信群.png -------------------------------------------------------------------------------- /images/task/新增定时任务.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/新增定时任务.png -------------------------------------------------------------------------------- /images/task/时间配置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/时间配置.png -------------------------------------------------------------------------------- /images/task/运行定时任务.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/运行定时任务.png -------------------------------------------------------------------------------- /images/task/选择用例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/选择用例.png -------------------------------------------------------------------------------- /images/task/邮件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/邮件.png -------------------------------------------------------------------------------- /images/task/邮箱服务器配置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/邮箱服务器配置.png -------------------------------------------------------------------------------- /images/task/都接收.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/都接收.png -------------------------------------------------------------------------------- /images/task/钉钉群.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/images/task/钉钉群.png -------------------------------------------------------------------------------- /job.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/11/17 15:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : job.py 7 | # @Software: PyCharm 8 | import os 9 | import json 10 | from threading import Thread 11 | 12 | from flask import request 13 | from flask.views import MethodView 14 | from flask_apscheduler import APScheduler 15 | 16 | from app.utils import restful 17 | from app.utils.sendReport import async_send_report 18 | from app.utils.parseCron import parse_cron 19 | from app.api_test.sets.models import Set, db 20 | from app.api_test.task.models import Task 21 | from app.ucenter.user.models import User 22 | from app import create_app 23 | from app.utils.runHttpRunner import RunCase 24 | 25 | os.environ['TZ'] = 'Asia/Shanghai' 26 | 27 | job = create_app() 28 | 29 | # 注册并启动定时任务 30 | scheduler = APScheduler() 31 | scheduler.init_app(job) 32 | scheduler.start() 33 | 34 | 35 | def aps_test(case_ids, task, user_id=None): 36 | """ 运行定时任务, 并发送测试报告 """ 37 | user = User.get_first(id=user_id) 38 | runner = RunCase( 39 | project_id=task.project_id, 40 | run_name=task.name, 41 | case_id=case_ids, 42 | task=task, 43 | performer=user.name, 44 | create_user=user.id) 45 | jump_res = runner.run_case() 46 | 47 | # 多线程发送测试报告 48 | async_send_report(content=task.loads(jump_res), **task.to_dict(), report_id=runner.report_id) 49 | 50 | db.session.rollback() # 把连接放回连接池 51 | return runner.report_id 52 | 53 | 54 | def async_aps_test(*args): 55 | """ 多线程执行定时任务 """ 56 | Thread(target=aps_test, args=args).start() 57 | 58 | 59 | class JobStatus(MethodView): 60 | """ 任务状态修改 """ 61 | 62 | def post(self): 63 | """ 添加定时任务 """ 64 | user_id, task_id = request.json.get('userId'), request.json.get('taskId') 65 | task = Task.get_first(id=task_id) 66 | cases_id = Set.get_case_id(task.project_id, json.loads(task.set_id), json.loads(task.case_id)) 67 | try: 68 | # 把定时任务添加到apscheduler_jobs表中 69 | scheduler.add_job(func=async_aps_test, # 异步执行任务 70 | trigger='cron', 71 | misfire_grace_time=60, 72 | coalesce=False, 73 | args=[cases_id, task, user_id], 74 | id=str(task.id), 75 | **parse_cron(task.cron)) 76 | task.status = '启用中' 77 | db.session.commit() 78 | return restful.success(f'定时任务 {task.name} 启动成功') 79 | except Exception as error: 80 | return restful.error(f'定时任务启动失败', data=error) 81 | 82 | def delete(self): 83 | """ 删除定时任务 """ 84 | task = Task.get_first(id=request.json.get('taskId')) 85 | with db.auto_commit(): 86 | task.status = '禁用中' 87 | scheduler.remove_job(str(task.id)) # 移除任务 88 | return restful.success(f'任务 {task.name} 禁用成功') 89 | 90 | 91 | job.add_url_rule('/api/job/status', view_func=JobStatus.as_view('jobStatus')) 92 | 93 | if __name__ == '__main__': 94 | job.run(host='0.0.0.0', port=8025) 95 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2020/9/25 17:13 4 | # @Author : ZhongYeHai 5 | # @Site : 6 | # @File : manage.py 7 | # @Software: PyCharm 8 | 9 | from app import create_app 10 | 11 | app = create_app() 12 | 13 | if __name__ == '__main__': 14 | app.run(host='0.0.0.0', port=8024, debug=False) 15 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes 1; 4 | 5 | error_log /test/api-test/error.log warn; # 路径自定义 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /usr/local/nginx/conf/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /test/api-test/access.log main; # 路径自定义 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | # 开启gzip 30 | gzip on; 31 | # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩 32 | gzip_min_length 1k; 33 | # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用CPU时间。一般设置1和2 34 | gzip_comp_level 4; 35 | gzip_static on;#是否开启gzip静态资源 36 | # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。 37 | gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; 38 | # 是否在http header中添加Vary: Accept-Encoding,建议开启 39 | gzip_vary on; 40 | # 禁用IE 6 gzip 41 | gzip_disable "MSIE [1-6]\."; 42 | # 设置缓存路径并且使用一块最大100M的共享内存,用于硬盘上的文件索引,包括文件名和请求次数,每个文件在1天内若不活跃(无请求)则从硬盘上淘汰,硬盘缓存最大10G,满了则根据LRU算法自动清除缓存。 43 | proxy_cache_path /home/nginx levels=1:2 keys_zone=imgcache:100m inactive=1d max_size=10g; 44 | 45 | 46 | #include /etc/nginx/conf.d/*.conf; 47 | server { 48 | listen 80; # nginx监听端口 49 | server_name api-test; 50 | charset utf-8; 51 | client_max_body_size 75M; 52 | 53 | # /开头的请求的转发地址 54 | location / { 55 | root /test/api-test/front/; # 前端dist包的位置 56 | index index.html; 57 | } 58 | 59 | # /api开头的请求的转发地址 60 | location ^~ /api{ 61 | proxy_pass http://139.196.100.202:8024/api; # 此处需写明ip地址,用127.0.0.1时无效 62 | } 63 | 64 | location /files { 65 | alias /home/files/; 66 | autoindex on; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/requirements.txt -------------------------------------------------------------------------------- /template/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/template/__init__.py -------------------------------------------------------------------------------- /template/接口导入模板.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhongyehai/api-test-api/e675390058c11ed021646b3c17ccef569582b6f3/template/接口导入模板.xls --------------------------------------------------------------------------------