diff --git a/README.md b/README.md index ad974799b29bc4d4351b334c9e8a0cbca5b3b99d..f99df3440ed97588c916b0afc41185144a3fc3f1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ QQ 529548204 allure 框架采用python的pytest模块, -搭配requests以及allure测试报告,可以发送钉钉通知邮件通知 +搭配requests以及allure测试报告,可以发送钉钉通知邮件通知, +支持自定义接口加密,util.tools.encryption文件内自己编辑加密规则, 可以根据yaml测试数据自动生成用例, 支持接口关联, 支持类似jmeter的函数助手, @@ -115,15 +116,19 @@ QQ 529548204 ### 1.3 参数介绍 `file` : 通过case外关键字file判断是否需要上传文件 如果需要则格式为:`{上传文件的参数名:文件路径}` -`param`:包含两种请求格式 -(1)json格式 `{ +`param`:包含两种请求格式 +(1)json格式 : +`{ "username": "finsiot","password": "$caches(pwd)$" # 读取缓存值 - }` -(2)param格式 `username=admin&password=123` + }`(同样适用于xml格式 会根据请求头***application/xml或者text/xml*** 将字典转换成xml类型) +(2)param格式 +`username=admin&password=123` -`urlparam`:`{ +`urlparam`为路径参数:`{ id: 123 - }` + }` + +会根据字典转换成 路径参数 **address**中 `v1/api/$url(id)$/`中会根据id替换为123 @@ -186,7 +191,7 @@ QQ 529548204 time: 2 # 响应时间断言 code: 200 ``` -### 1.4 生成随机数据介绍 +### 1.5 生成随机数据介绍 部分数据采用faker库生成 ```python int_num = "$RandomPosInt(1,333)$" # 267 @@ -204,7 +209,7 @@ QQ 529548204 name = "$faker(name)$" # 许云 ``` -### 1.4 后置请求处理 +### 1.6 后置请求处理 使用场景: 新增数据A之后一系列用例执行完需要后置来还原数据形成业务闭环. datatype 有3种类型 param json 和urlparam dataname为后置关联的参数中参数名称 @@ -396,6 +401,12 @@ password = ;database = database = charset = utf8 +[redis] +host = 127.0.0.1 +port = 6379 +db = 1 +password = 123456 +charset = UTF-8 [dingding] webhook = secret = @@ -409,14 +420,20 @@ http=127.0.0.1:4444;https=127.0.0.1:4444;ftp=127.0.0.1:4444 HTTPS: http://mitm.it/ 开启代理后下载证书安装 执行脚本 recording.py 每个请求将会在 ./test_suite/recording 文件夹中建立文件 每次执行录制时会覆盖原文件 - -# 五、操作方法 +# 五、接口加密(测试版) +加签加密法: +在config中`encryption`增加sign的值 为签 +需要在util.tools.encryption文件内自己编辑加密规则 +![img_2.png](pic/img_2.png) +需要加密的接口 YAML数据中encryption的值为true 会根据此在请求用例前增加加密装饰器 +![img_1.png](pic/img_1.png) +# 六、操作方法 1. 新建config/config.ini文件 格式如上例子 2. 执行**util/scripts/newProject.py** 根据testname生成测试项目基础目录 3. 在生成的**test_suite/datas/*testname*** 文件夹下增加yaml测试用例 4. 执行**util/scripts/writeCase.py**生成测试脚本 关于token 需要根据自己项目情况修改yaml文件中token关键字 如果不需要token值为false 需要token则改为需要的类型 5. 执行**setupMain.py**开始测试(单用例调试时需要执行**util\tools\readYamlFile.py**将测试数据存入redis) -# 六、代码展示 +# 七、代码展示 test_login.py ![自动生成吃的测试用例](https://img-blog.csdnimg.cn/39ebf79842e14cf791af0fa88433eab3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6bq76L6j54Or5YS_,size_20,color_FFFFFF,t_70,g_se,x_16) login.yml diff --git a/common/basePage.py b/common/basePage.py index 291540ff798679d135ce93e2c2eb9a57a887b720..344a0f997bccd613d2f35b59237fbf6c90915893 100644 --- a/common/basePage.py +++ b/common/basePage.py @@ -3,6 +3,7 @@ import json import logging import os import random +import xmltodict import allure import requests @@ -101,6 +102,12 @@ class apiSend(object): if data_random: data_random = json.loads(data_random) response = requests.post(url=url, json=data_random, headers=header, timeout=timeout) + elif 'application/xml' in str(header.values()) or 'text/xml' in str(header.values()): + if data_random: + data_random = json.loads(data_random) + xmldata = xmltodict.unparse({'xml': data_random}) + # dict转成xml + response = requests.post(url=url, data=xmldata, headers=header, timeout=timeout) else: response = requests.post(url=url, data=data_random, headers=header, timeout=timeout) try: @@ -193,6 +200,12 @@ class apiSend(object): if data_random: data_random = json.loads(data_random) response = requests.put(url=url, json=data_random, headers=header, timeout=timeout, files=files) + elif 'application/xml' in str(header.values()) or 'text/xml' in str(header.values()): + if data_random: + data_random = json.loads(data_random) + xmldata = xmltodict.unparse({'xml': data_random}) + # dict转成xml + response = requests.post(url=url, data=xmldata, headers=header, timeout=timeout) else: response = requests.put(url=url, data=data_random, headers=header, timeout=timeout, files=files) try: diff --git a/config/confManage.py b/config/confManage.py index aa2981086eb451081554dbdd0fd5e7efa3182200..f75cb85a0d4956e9e9b7b7e9f87f546ad3170067 100644 --- a/config/confManage.py +++ b/config/confManage.py @@ -83,7 +83,17 @@ def dingding_manage(dingding): pass return dingding - +def encryption_manage(encryption): + try: + relevance_list = re.findall(r"\${(.*?)}\$", encryption) + for n in relevance_list: + pattern = re.compile(r'\${' + n + r'}\$') + dir_cf = Config() + dir_relevance = dir_cf.read_encryption() + encryption = re.sub(pattern, dir_relevance[n], encryption, count=1) + except TypeError: + pass + return encryption if __name__ == '__main__': # print(dir_manage("${pro_dir}$")) # print(db_manage("${user}$")) @@ -91,6 +101,6 @@ if __name__ == '__main__': # print(db_manage("${database}$")) # print(db_manage("${charset}$")) # print(int(db_manage("${port}$"))) - print(host_manage("${host}$${host_117}$")) + print(encryption_manage("${sign}$")) # print("${{{haha}}}$".format(**{"haha":"123"})) # print(host_manage("${{{haha}}}$".format(**{"haha":"host2"}))) diff --git a/config/confRead.py b/config/confRead.py index 240f9d32291ff8832d2ecb35bef365dec719fc77..9974de624ea068176f02abc4f103bac7abe2bfb5 100644 --- a/config/confRead.py +++ b/config/confRead.py @@ -42,9 +42,20 @@ class Config(object): return self.config[dbname] def read_dingding(self): + """ + 读取钉钉配置 + :return: + """ self.config.read(self.conf_path, encoding='utf-8') return self.config['dingding'] + def read_encryption(self): + """ + 读取加密配置签名 + :return: + """ + self.config.read(self.conf_path, encoding='utf-8') + return self.config['encryption'] if __name__ == '__main__': c = Config() diff --git a/config/config.ini b/config/config.ini index 57fdeab691d92a7e0c8c8bcaf1a84ca5c5d94bcc..8e04794df10ab3c4f678ab56d3d3401811131498 100644 --- a/config/config.ini +++ b/config/config.ini @@ -34,6 +34,14 @@ user = root password = test@123 database = test charset = utf8 +[redis] +host = 127.0.0.1 +port = 6379 +db = 2 +password = 123456 +charset = UTF-8 [dingding] webhook = https://oapi.dingtalk.com/robot/send?access_token=2e7987d965c9ce0fd3d7a703d9653f9f12237dd9b5f07a21a2e1cb84cc2bbb7e -secret = SEC8867b2fed306092b1005e4c85ca62de803fb3ae2fb34ee6ab6ea90a595aa57a0 \ No newline at end of file +secret = SEC8867b2fed306092b1005e4c85ca62de803fb3ae2fb34ee6ab6ea90a595aa57a0 +[encryption] +sign = 12345hhhhhh \ No newline at end of file diff --git a/pic/img_1.png b/pic/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c72ab59babe3d162d4933171daeff7dcc236fe Binary files /dev/null and b/pic/img_1.png differ diff --git a/pic/img_2.png b/pic/img_2.png new file mode 100644 index 0000000000000000000000000000000000000000..5c313694214161b7617d0f8602ed1fffe868caea Binary files /dev/null and b/pic/img_2.png differ diff --git a/requirements.txt b/requirements.txt index e0f62c985b660cca9140f87ae1822e30113cda1b..8b0184b2fe016513323a31e690efbe9b9fd77b83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -allure-pytest==2.9.43 -allure-python-commons==2.9.43 +allure-pytest==2.9.45 +allure-python-commons==2.9.45 asgiref==3.4.1 atomicwrites==1.4.0 attrs==21.2.0 @@ -8,11 +8,15 @@ Brotli==1.0.9 certifi==2021.5.30 cffi==1.15.0 chardet==4.0.0 +charset-normalizer==2.0.12 click==8.0.3 colorama==0.4.4 coverage==5.5 -cryptography==3.4.8 +cryptography==36.0.2 +Deprecated==1.2.13 DingtalkChatbot==1.5.3 +execnet==1.9.0 +Faker==13.3.2 Flask==2.0.2 h11==0.12.0 h2==4.1.0 @@ -26,7 +30,7 @@ jsonpath==0.82 kaitaistruct==0.9 ldap3==2.9.1 MarkupSafe==2.0.1 -mitmproxy==7.0.4 +mitmproxy==8.0.0 msgpack==1.0.3 packaging==20.9 passlib==1.7.4 @@ -38,14 +42,18 @@ pyasn1==0.4.8 pycparser==2.21 pydivert==2.1.0 PyMySQL==1.0.2 -pyOpenSSL==20.0.1 +pyOpenSSL==22.0.0 pyparsing==2.4.7 pyperclip==1.8.2 -pytest==6.2.4 +pytest==7.1.1 +pytest-forked==1.4.0 pytest-ordering==0.6 pytest-rerunfailures==10.0 +pytest-xdist==2.5.0 +python-dateutil==2.8.2 PyYAML==5.4.1 -requests==2.25.1 +redis==4.1.4 +requests==2.27.1 requests-toolbelt==0.9.1 ruamel.yaml==0.17.16 ruamel.yaml.clib==0.2.6 @@ -53,11 +61,12 @@ simplejson==3.17.2 six==1.16.0 sortedcontainers==2.4.0 toml==0.10.2 +tomli==2.0.1 tornado==6.1 urllib3==1.26.5 urwid==2.1.2 Werkzeug==2.0.2 +wrapt==1.13.3 wsproto==1.0.0 +xmltodict==0.13.0 zstandard==0.15.2 -faker==13.3.2 -pytest-xdist==2.5.0 diff --git a/test_suite/testcase/demo/__init__.py b/test_suite/testcase/demo/__init__.py index 2a663d32aa89fbb91ebb65f83ba5a302f46673f8..fa4d31e12f3a56ded66e89287e73fa58c9fa4086 100644 --- a/test_suite/testcase/demo/__init__.py +++ b/test_suite/testcase/demo/__init__.py @@ -7,13 +7,13 @@ import pytest from common.checkResult import asserting from util.tools.log import Log -from util.tools.readYamlFile import ini_allyaml,readRedisData +from util.tools.readYamlFile import ini_allyaml, readRedisData from common.basePage import apisend from util.tools.iniRequests import relevance from util.tools.iniHeaders import iniheaders from util.tools.requestsTearDown import caseTearDown from util.tools.readYamlFile import readRedisData - +from util.tools.encryption import Encryption Log() __all__ = [ @@ -28,4 +28,5 @@ __all__ = [ 'relevance', 'iniheaders', 'caseTearDown', -] \ No newline at end of file + 'Encryption', +] diff --git a/util/scripts/newProject.py b/util/scripts/newProject.py index 8f562b975968b6f00e1a7b4266d2ff5083379f6f..b85ee15090ee1ffd9560a2862680cea66cb35bd3 100644 --- a/util/scripts/newProject.py +++ b/util/scripts/newProject.py @@ -38,6 +38,7 @@ from util.tools.iniRequests import relevance from util.tools.iniHeaders import iniheaders from util.tools.requestsTearDown import caseTearDown from util.tools.readYamlFile import readRedisData +from util.tools.encryption import Encryption Log() @@ -53,6 +54,7 @@ __all__ = [ 'relevance', 'iniheaders', 'caseTearDown', + 'Encryption', ]""") if "conftest.py" not in os.listdir(casepath): with open(casepath + "/" + r"{}".format("conftest.py"), 'w', encoding='utf-8') as f: diff --git a/util/scripts/recording.py b/util/scripts/recording.py index 34117e2dfc247a7c15d80f9ed2b7686759672b67..46212506beb6218afd1f5c02ace9ff22def52d7e 100644 --- a/util/scripts/recording.py +++ b/util/scripts/recording.py @@ -16,7 +16,8 @@ from util.tools.log import Log from util.tools.mkDir import mk_dir Log() -Host = host_manage("${HB}$") +hostname = "HB" +Host = host_manage(f"${{{hostname}}}$") class Counter: @@ -60,6 +61,7 @@ class Counter: # detail["order"] = self.num detail["case"] = [case] detail["file"] = False + detail["encryption"] = False # case 数据 case["info"] = name diff --git a/util/scripts/writeCase.py b/util/scripts/writeCase.py index 4d65cf805e0ebaee1529ad2194463d311ea163c9..e020e86f8d5e8484a819f3105979aa037bba2709 100644 --- a/util/scripts/writeCase.py +++ b/util/scripts/writeCase.py @@ -44,6 +44,9 @@ class Test_{filename}(object):""") @pytest.mark.parametrize('casedata', readRedisData("{item}")["case"], ids=[i["info"] for i in readRedisData("{item}")["case"]]) @pytest.mark.flaky(reruns=1, reruns_delay=1)""") + if filedata[item]["encryption"]: + f.write(f""" + @Encryption()""") if filedata[item]["token"] and filedata[item]["token"] == "Cookies": # 判断是否需要token 默认类型是 Authorization f.write(f""" diff --git a/util/tools/encryption.py b/util/tools/encryption.py new file mode 100644 index 0000000000000000000000000000000000000000..57ef12ab53fb4430235a1599f2fc32657c2949e7 --- /dev/null +++ b/util/tools/encryption.py @@ -0,0 +1,66 @@ +# coding:utf-8 +""" +@author: jing +@contact: 529548204@qq.com +@file: encryption.py +@time: 2022/5/17 14:22 +""" +import hashlib + +from functools import wraps + +from config.confManage import encryption_manage + + +class Encryption(object): + + def __init__(self, api_key=encryption_manage("${sign}$")): + self.api_key = api_key + self.data = {} + + def __call__(self, func): + @wraps(func) + def wrapped_function(*args, **kwargs): + # 打开logfile并写入 + # print(kwargs["casedata"]["data"]) + self.data = kwargs["casedata"]["data"] + kwargs["casedata"]["data"]["param"] = self.custom_encryption() + return func(*args, **kwargs) + + return wrapped_function + + def custom_encryption(self): + """ + 自定义加密规则 (规则需要自己写以下为例子) + 返回结果是加密后全部参数 可以是"a=1&b=2"也可以是{"a":1,"b":2} + :return: + """ + if "sign" in self.data["param"]: + # 弹出sign + self.data["param"].pop('sign') + dataList = [] + for key in sorted(self.data["param"]): + if self.data["param"][key]: + dataList.append("%s=%s" % (key, self.data["param"][key])) + lastdata = "&".join(dataList) + data = lastdata + "&key=" + self.api_key.strip() + md = hashlib.md5() + md.update(data.encode("utf-8")) + signature = md.hexdigest().upper() + self.data["param"]['sign'] = signature + dictxml = self.data["param"] + # dict转成xml + return dictxml + + +if __name__ == '__main__': + api_keys = "8ViypSUsKSbK26g7HDsKtZSCTUsTtEvB" + da = {'mch_id': 160467, 'nonce_str': 'eh0k52jvvvs51dbxr2pd', 'out_trade_no': 10002112123231, 'sign': '{{sign}}'} + + # s = logit(api_keys,da) + # s.send_post() + + # s.data_processing() + # s.md5() + # print(s.rewrite_xml()) + # s.rewrite_xml() diff --git a/util/tools/redisData.py b/util/tools/redisData.py index f634f466f13e12aa9efe2cdbb81c0ffb410d53b0..507cf799b2a36c1ce72f7417550e97cb2f9d8da2 100644 --- a/util/tools/redisData.py +++ b/util/tools/redisData.py @@ -9,16 +9,18 @@ import json import redis +from config.confManage import db_manage -class RedisHandler: + +class RedisHandler(object): """ redis 缓存读取封装 """ def __init__(self): - self.host = '127.0.0.1' - self.port = 6379 - self.db = 2 - self.password = 123456 - self.charset = 'UTF-8' + self.host = db_manage("${host}$", "redis") + self.port = db_manage("${port}$", "redis") + self.db = db_manage("${db}$", "redis") + self.password = db_manage("${password}$", "redis") + self.charset = db_manage("${charset}$", "redis") self.redis = redis.Redis(self.host, port=self.port, password=self.password, decode_responses=True, db=self.db) def set_string(self, name: str, value, ex=None, px=None, nx=False, xx=False) -> None: