mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-10 17:43:00 +08:00
Merge pull request #1294 from bbx-winner/master
feat: postman to case; refactor: hrp convert
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
- fix #1308: load `.env` file as environment variables
|
||||
- fix #1309: locate plugin file upward recursively until system root dir
|
||||
- refactor: move base_url to config env
|
||||
- feat: support converting Postman collection to HttpRunner testcase
|
||||
- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter`
|
||||
|
||||
## v4.1.0-beta (2022-05-21)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Copyright 2017 debugtalk
|
||||
### SEE ALSO
|
||||
|
||||
* [hrp boom](hrp_boom.md) - run load test with boomer
|
||||
* [hrp convert](hrp_convert.md) - convert JSON/YAML testcases to pytest/gotest scripts
|
||||
* [hrp convert](hrp_convert.md) - convert to JSON/YAML/gotest/pytest testcases
|
||||
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
|
||||
* [hrp pytest](hrp_pytest.md) - run API test with pytest
|
||||
* [hrp run](hrp_run.md) - run API test with go engine
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## hrp convert
|
||||
|
||||
convert JSON/YAML testcases to pytest/gotest scripts
|
||||
convert to JSON/YAML/gotest/pytest testcases
|
||||
|
||||
```
|
||||
hrp convert $path... [flags]
|
||||
@@ -9,9 +9,13 @@ hrp convert $path... [flags]
|
||||
### Options
|
||||
|
||||
```
|
||||
--gotest convert to gotest scripts (TODO)
|
||||
-h, --help help for convert
|
||||
--pytest convert to pytest scripts (default true)
|
||||
-h, --help help for convert
|
||||
-d, --output-dir string specify output directory, default to the same dir with har file
|
||||
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
|
||||
--to-gotest convert to gotest scripts (TODO)
|
||||
--to-json convert to JSON scripts (default)
|
||||
--to-pytest convert to pytest scripts
|
||||
--to-yaml convert to YAML scripts
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"name": "testcase description"
|
||||
},
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://postman-echo.com/get",
|
||||
"params": {
|
||||
"foo1": "HDnY8",
|
||||
"foo2": "34.5"
|
||||
},
|
||||
"headers": {
|
||||
"Accept-Encoding": "gzip",
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "HttpRunnerPlus"
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/get?foo1=HDnY8\u0026foo2=34.5",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"headers": {
|
||||
"Accept-Encoding": "gzip",
|
||||
"Content-Length": "28",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "Go-http-client/1.1"
|
||||
},
|
||||
"cookies": {
|
||||
"sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk"
|
||||
},
|
||||
"body": {
|
||||
"foo1": "HDnY8",
|
||||
"foo2": 12.3
|
||||
}
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/post",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"headers": {
|
||||
"Accept-Encoding": "gzip",
|
||||
"Content-Length": "20",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Host": "postman-echo.com",
|
||||
"User-Agent": "Go-http-client/1.1"
|
||||
},
|
||||
"cookies": {
|
||||
"sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw"
|
||||
},
|
||||
"body": "foo1=HDnY8\u0026foo2=12.3"
|
||||
},
|
||||
"validate": [
|
||||
{
|
||||
"check": "status_code",
|
||||
"assert": "equals",
|
||||
"expect": 200,
|
||||
"msg": "assert response status code"
|
||||
},
|
||||
{
|
||||
"check": "headers.\"Content-Type\"",
|
||||
"assert": "equals",
|
||||
"expect": "application/json; charset=utf-8",
|
||||
"msg": "assert response header Content-Type"
|
||||
},
|
||||
{
|
||||
"check": "body.data",
|
||||
"assert": "equals",
|
||||
"expect": "",
|
||||
"msg": "assert response body data"
|
||||
},
|
||||
{
|
||||
"check": "body.url",
|
||||
"assert": "equals",
|
||||
"expect": "https://postman-echo.com/post",
|
||||
"msg": "assert response body url"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
override: true
|
||||
headers:
|
||||
Content-Type: "application/x-www-form-urlencoded"
|
||||
cookies:
|
||||
488
examples/data/postman/postman_collection.json
Normal file
488
examples/data/postman/postman_collection.json
Normal file
@@ -0,0 +1,488 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "0417a445-b206-4ea2-b1d2-5441afd6c6b9",
|
||||
"name": "postman collection demo",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "folder1",
|
||||
"item": [
|
||||
{
|
||||
"name": "folder2",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get with params",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "k1",
|
||||
"value": "v1"
|
||||
},
|
||||
{
|
||||
"key": "k2",
|
||||
"value": "v2"
|
||||
},
|
||||
{
|
||||
"key": "k3",
|
||||
"value": "v3",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "get"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Get with params case1",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path?k1=v1&k2=v2",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "k1",
|
||||
"value": "v1"
|
||||
},
|
||||
{
|
||||
"key": "k2",
|
||||
"value": "v2"
|
||||
},
|
||||
{
|
||||
"key": "k3",
|
||||
"value": "v3",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "get"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Mon, 16 May 2022 12:12:28 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "508"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "ETag",
|
||||
"value": "W/\"1fc-x4EIPFQzoLX0HenCFPx6HNfG0lc\""
|
||||
},
|
||||
{
|
||||
"key": "Vary",
|
||||
"value": "Accept-Encoding"
|
||||
},
|
||||
{
|
||||
"key": "set-cookie",
|
||||
"value": "sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils; Path=/; HttpOnly"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k2=v2\"\n}"
|
||||
},
|
||||
{
|
||||
"name": "Get with params case2",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path?k1=v1&k3=v3",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "k1",
|
||||
"value": "v1"
|
||||
},
|
||||
{
|
||||
"key": "k2",
|
||||
"value": "v2",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "k3",
|
||||
"value": "v3"
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "get"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Mon, 16 May 2022 12:14:04 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "504"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "ETag",
|
||||
"value": "W/\"1f8-tMaKs4xmwr+3su3I8mcgR0p+ucw\""
|
||||
},
|
||||
{
|
||||
"key": "Vary",
|
||||
"value": "Accept-Encoding"
|
||||
},
|
||||
{
|
||||
"key": "set-cookie",
|
||||
"value": "sails.sid=s%3AMNuX_i0KgaP_KuuMpYB8RtCNipCGJWVw.4ETfPHxE81Omqb6Yli%2FezUU8CXyYBcN3%2Bxkx5htwh8Y; Path=/; HttpOnly"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k3\": \"v3\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k3=v3\"\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "folder3",
|
||||
"item": [
|
||||
{
|
||||
"name": "Post form-data",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "k1",
|
||||
"value": "v1",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "k2",
|
||||
"value": "v2",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "k3",
|
||||
"value": "v3",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "post"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Post x-www-form-urlencoded",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{
|
||||
"key": "k1",
|
||||
"value": "v1",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "k2",
|
||||
"value": "v2",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "k3",
|
||||
"value": "v3",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "post"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Post raw json",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "post"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Post raw text",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "have a nice day",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "post"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get request headers",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "User-Agent",
|
||||
"value": "HttpRunner",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "User-Name",
|
||||
"value": "bbx",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "close",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "headers"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Get request headers case1",
|
||||
"originalRequest": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "User-Agent",
|
||||
"value": "HttpRunner",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "User-Name",
|
||||
"value": "bbx",
|
||||
"type": "text",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "Cookie",
|
||||
"value": "Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "https://postman-echo.com/:path",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"postman-echo",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
":path"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "path",
|
||||
"value": "headers"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Mon, 16 May 2022 12:14:25 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json; charset=utf-8"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "541"
|
||||
},
|
||||
{
|
||||
"key": "Connection",
|
||||
"value": "keep-alive"
|
||||
},
|
||||
{
|
||||
"key": "ETag",
|
||||
"value": "W/\"21d-ld5UvFTaRM6lihVnvCj6mZm5Of0\""
|
||||
},
|
||||
{
|
||||
"key": "Vary",
|
||||
"value": "Accept-Encoding"
|
||||
}
|
||||
],
|
||||
"cookie": [],
|
||||
"body": "{\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"HttpRunner\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\"\n }\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
examples/data/postman/profile.yml
Normal file
4
examples/data/postman/profile.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
headers:
|
||||
User-Agent: "this header will be created or updated"
|
||||
cookies:
|
||||
Cookie1: "this cookie will be created or updated"
|
||||
5
examples/data/postman/profile_override.yml
Normal file
5
examples/data/postman/profile_override.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
override: true
|
||||
headers:
|
||||
Header1: "all original headers will be overridden"
|
||||
cookies:
|
||||
Cookie1: "all original cookies will be overridden"
|
||||
1
go.mod
1
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
github.com/go-openapi/spec v0.20.6
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/httprunner/funplugin v0.4.6
|
||||
|
||||
27
go.sum
27
go.sum
@@ -88,6 +88,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -131,6 +132,16 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
@@ -262,6 +273,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -288,16 +301,20 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
|
||||
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
@@ -348,6 +365,8 @@ github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5Vgl
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
@@ -837,8 +856,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@@ -854,6 +874,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -2,9 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/convert"
|
||||
@@ -12,37 +10,52 @@ import (
|
||||
|
||||
var convertCmd = &cobra.Command{
|
||||
Use: "convert $path...",
|
||||
Short: "convert JSON/YAML testcases to pytest/gotest scripts",
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
Short: "convert to JSON/YAML/gotest/pytest testcases",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !pytestFlag && !gotestFlag {
|
||||
return errors.New("please specify convertion type")
|
||||
var flagCount int
|
||||
var outputType convert.OutputType
|
||||
if toJSONFlag {
|
||||
flagCount++
|
||||
}
|
||||
|
||||
var err error
|
||||
if gotestFlag {
|
||||
err = convert.Convert2TestScripts("gotest", args...)
|
||||
} else {
|
||||
err = convert.Convert2TestScripts("pytest", args...)
|
||||
if toYAMLFlag {
|
||||
flagCount++
|
||||
outputType = convert.OutputTypeYAML
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("convert test scripts failed")
|
||||
os.Exit(1)
|
||||
if toGoTestFlag {
|
||||
flagCount++
|
||||
outputType = convert.OutputTypeGoTest
|
||||
}
|
||||
if toPyTestFlag {
|
||||
flagCount++
|
||||
outputType = convert.OutputTypePyTest
|
||||
}
|
||||
if flagCount > 1 {
|
||||
return errors.New("please specify at most one conversion flag")
|
||||
}
|
||||
convert.Run(outputType, outputDir, profilePath, args)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
pytestFlag bool
|
||||
gotestFlag bool
|
||||
toJSONFlag bool
|
||||
toYAMLFlag bool
|
||||
toGoTestFlag bool
|
||||
toPyTestFlag bool
|
||||
outputDir string
|
||||
profilePath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(convertCmd)
|
||||
convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts")
|
||||
convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)")
|
||||
convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts")
|
||||
convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)")
|
||||
convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)")
|
||||
convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts")
|
||||
convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
|
||||
convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers (except for auto-generated headers) and cookies")
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/har2case"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/convert"
|
||||
)
|
||||
|
||||
// har2caseCmd represents the har2case command
|
||||
@@ -19,54 +18,34 @@ var har2caseCmd = &cobra.Command{
|
||||
setLogLevel(logLevel)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var outputFiles []string
|
||||
for _, arg := range args {
|
||||
// must choose one
|
||||
if !genYAMLFlag && !genJSONFlag {
|
||||
return errors.New("please select convert format type")
|
||||
}
|
||||
var outputPath string
|
||||
var err error
|
||||
|
||||
har := har2case.NewHAR(arg)
|
||||
|
||||
// specify output dir
|
||||
if outputDir != "" {
|
||||
har.SetOutputDir(outputDir)
|
||||
}
|
||||
|
||||
// specify profile
|
||||
if profilePath != "" {
|
||||
har.SetProfile(profilePath)
|
||||
}
|
||||
|
||||
// generate json/yaml files
|
||||
if genYAMLFlag {
|
||||
outputPath, err = har.GenYAML()
|
||||
} else {
|
||||
outputPath, err = har.GenJSON() // default
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputFiles = append(outputFiles, outputPath)
|
||||
var flagCount int
|
||||
var har2caseOutputType convert.OutputType
|
||||
if har2caseGenJSONFlag {
|
||||
flagCount++
|
||||
}
|
||||
log.Info().Strs("output", outputFiles).Msg("convert testcase success")
|
||||
if har2caseGenYAMLFlag {
|
||||
flagCount++
|
||||
har2caseOutputType = convert.OutputTypeYAML
|
||||
}
|
||||
if flagCount > 1 {
|
||||
return errors.New("please specify at most one conversion flag")
|
||||
}
|
||||
convert.Run(har2caseOutputType, har2caseOutputDir, har2caseProfilePath, args)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
genJSONFlag bool
|
||||
genYAMLFlag bool
|
||||
outputDir string
|
||||
profilePath string
|
||||
har2caseGenJSONFlag bool
|
||||
har2caseGenYAMLFlag bool
|
||||
har2caseOutputDir string
|
||||
har2caseProfilePath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(har2caseCmd)
|
||||
har2caseCmd.Flags().BoolVarP(&genJSONFlag, "to-json", "j", true, "convert to JSON format")
|
||||
har2caseCmd.Flags().BoolVarP(&genYAMLFlag, "to-yaml", "y", false, "convert to YAML format")
|
||||
har2caseCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
|
||||
har2caseCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies")
|
||||
har2caseCmd.Flags().BoolVarP(&har2caseGenJSONFlag, "to-json", "j", false, "convert to JSON format (default)")
|
||||
har2caseCmd.Flags().BoolVarP(&har2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format")
|
||||
har2caseCmd.Flags().StringVarP(&har2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
|
||||
har2caseCmd.Flags().StringVarP(&har2caseProfilePath, "profile", "p", "", "specify profile path to override headers and cookies")
|
||||
}
|
||||
|
||||
@@ -285,7 +285,8 @@ func LoadFile(path string, structObj interface{}) (err error) {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read file failed")
|
||||
}
|
||||
|
||||
// remove BOM at the beginning of file
|
||||
file = bytes.TrimLeft(file, "\xef\xbb\xbf")
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".json", ".har":
|
||||
@@ -383,3 +384,9 @@ func readFile(path string) ([]byte, error) {
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func GetOutputNameWithoutExtension(path string) string {
|
||||
base := filepath.Base(path)
|
||||
ext := filepath.Ext(base)
|
||||
return base[0:len(base)-len(ext)] + "_test"
|
||||
}
|
||||
|
||||
77
hrp/internal/convert/README.md
Normal file
77
hrp/internal/convert/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# hrp convert
|
||||
|
||||
## 快速上手
|
||||
|
||||
```shell
|
||||
$ hrp convert -h
|
||||
convert to JSON/YAML/gotest/pytest testcases
|
||||
|
||||
Usage:
|
||||
hrp convert $path... [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for convert
|
||||
-d, --output-dir string specify output directory, default to the same dir with har file
|
||||
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
|
||||
--to-gotest convert to gotest scripts (TODO)
|
||||
--to-json convert to JSON scripts (default true)
|
||||
--to-pytest convert to pytest scripts
|
||||
--to-yaml convert to YAML scripts
|
||||
|
||||
Global Flags:
|
||||
--log-json set log to json format
|
||||
-l, --log-level string set log level (default "INFO")
|
||||
```
|
||||
|
||||
`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 文件或 curl/Apache ab 指令转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化。
|
||||
|
||||
该指令所有选项的详细说明如下:
|
||||
|
||||
1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入转化为对应形态的测试用例,四个选项中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例
|
||||
2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹
|
||||
3. `--profile` 后接 profile 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers` 和 `Cookies` 信息,profile 文件的后缀可以为 `json/yaml/yml`,下面给出两类 profile 配置文件的示例:
|
||||
- 根据 profile 替换指定的 `Headers` 和 `Cookies` 信息
|
||||
```yaml
|
||||
headers:
|
||||
Header1: "this header will be created or updated"
|
||||
cookies:
|
||||
Cookie1: "this cookie will be created or updated"
|
||||
```
|
||||
- 根据 profile 覆盖原有的 `Headers` 和 `Cookies` 信息
|
||||
```yaml
|
||||
override: true
|
||||
headers:
|
||||
Header1: "all original headers will be overridden"
|
||||
cookies:
|
||||
Cookie1: "all original cookies will be overridden"
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 输出的测试用例文件名格式为 `Postman 工程文件名称(不带拓展名)` + `_test` + `.json/.yaml/.go/.py 后缀`,如果该文件已经存在则会进行覆盖
|
||||
2. `hrp convert` 可以自动识别输入类型,因此不需要通过选项来手动制定输入类型,如遇到无法识别、不支持或转换失败的情况,则会输出错误日志并跳过,不会影响其他转换过程的正常进行
|
||||
3. 在 profile 文件中,指定 `override` 字段为 `false/true` 可以选择修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 profile 的默认修改模式为替换模式
|
||||
4. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎的请求体、断言格式细微差异,输出的 JSON/YAML 则统一采用 Golang 引擎的风格
|
||||
|
||||
|
||||
## 转换流程图
|
||||
|
||||
`hrp convert` 的转换过程流程图如下:
|
||||

|
||||
|
||||
## 开发进度
|
||||
|
||||
`hrp convert` 当前的开发进度如下:
|
||||
|
||||
| from \ to | JSON | YAML | GoTest | PyTest |
|
||||
|:---------:|:----:|:----:|:------:|:------:|
|
||||
| HAR | ✅ | ✅ | ❌ | ✅ |
|
||||
| Postman | ✅ | ✅ | ❌ | ✅ |
|
||||
| JMeter | ❌ | ❌ | ❌ | ❌ |
|
||||
| Swagger | ❌ | ❌ | ❌ | ❌ |
|
||||
| curl | ❌ | ❌ | ❌ | ❌ |
|
||||
| Apache ab | ❌ | ❌ | ❌ | ❌ |
|
||||
| JSON | ✅ | ✅ | ❌ | ✅ |
|
||||
| YAML | ✅ | ✅ | ❌ | ✅ |
|
||||
| GoTest | ❌ | ❌ | ❌ | ❌ |
|
||||
| PyTest | ❌ | ❌ | ❌ | ❌ |
|
||||
BIN
hrp/internal/convert/asset/flowgram.png
Normal file
BIN
hrp/internal/convert/asset/flowgram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
376
hrp/internal/convert/converter.go
Normal file
376
hrp/internal/convert/converter.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
suffixJSON = ".json"
|
||||
suffixYAML = ".yaml"
|
||||
suffixGoTest = ".go"
|
||||
suffixPyTest = ".py"
|
||||
)
|
||||
|
||||
type InputType int
|
||||
|
||||
const (
|
||||
InputTypeUnknown InputType = iota // default input type: unknown
|
||||
InputTypeHAR
|
||||
InputTypePostman
|
||||
InputTypeSwagger
|
||||
InputTypeJMeter
|
||||
InputTypeJSON
|
||||
InputTypeYAML
|
||||
InputTypeGoTest
|
||||
InputTypePyTest
|
||||
)
|
||||
|
||||
func (inputType InputType) String() string {
|
||||
switch inputType {
|
||||
case InputTypeHAR:
|
||||
return "har"
|
||||
case InputTypePostman:
|
||||
return "postman"
|
||||
case InputTypeSwagger:
|
||||
return "swagger"
|
||||
case InputTypeJMeter:
|
||||
return "jmeter"
|
||||
case InputTypeJSON:
|
||||
return "json testcase"
|
||||
case InputTypeYAML:
|
||||
return "yaml testcase"
|
||||
case InputTypeGoTest:
|
||||
return "gotest script"
|
||||
case InputTypePyTest:
|
||||
return "pytest script"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type OutputType int
|
||||
|
||||
const (
|
||||
OutputTypeJSON OutputType = iota // default output type: JSON
|
||||
OutputTypeYAML
|
||||
OutputTypeGoTest
|
||||
OutputTypePyTest
|
||||
)
|
||||
|
||||
func (outputType OutputType) String() string {
|
||||
switch outputType {
|
||||
case OutputTypeYAML:
|
||||
return "yaml"
|
||||
case OutputTypeGoTest:
|
||||
return "gotest"
|
||||
case OutputTypePyTest:
|
||||
return "pytest"
|
||||
default:
|
||||
return "json"
|
||||
}
|
||||
}
|
||||
|
||||
// TCaseConverter holds the common properties of case converter
|
||||
type TCaseConverter struct {
|
||||
InputPath string
|
||||
OutputDir string
|
||||
Profile *Profile
|
||||
InputType InputType
|
||||
OutputType OutputType
|
||||
CaseHAR *CaseHar
|
||||
CasePostman *CasePostman
|
||||
CaseSwagger *spec.Swagger
|
||||
TCase *hrp.TCase
|
||||
}
|
||||
|
||||
// Profile is used to override or update(create if not existed) original headers and cookies
|
||||
type Profile struct {
|
||||
Override bool `json:"override" yaml:"override"`
|
||||
Headers map[string]string `json:"headers" yaml:"headers"`
|
||||
Cookies map[string]string `json:"cookies" yaml:"cookies"`
|
||||
}
|
||||
|
||||
func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) {
|
||||
tCaseConverter = &TCaseConverter{
|
||||
InputPath: path,
|
||||
InputType: InputTypeUnknown,
|
||||
}
|
||||
extName := filepath.Ext(path)
|
||||
if extName == "" {
|
||||
log.Warn().Msg("extension name should be specified")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
switch extName {
|
||||
case ".har":
|
||||
caseHAR := new(CaseHar)
|
||||
err = builtin.LoadFile(path, caseHAR)
|
||||
if err == nil && !reflect.ValueOf(*caseHAR).IsZero() {
|
||||
tCaseConverter.InputType = InputTypeHAR
|
||||
tCaseConverter.CaseHAR = caseHAR
|
||||
}
|
||||
case ".json":
|
||||
tCase := new(hrp.TCase)
|
||||
err = builtin.LoadFile(path, tCase)
|
||||
if err == nil && !reflect.ValueOf(*tCase).IsZero() {
|
||||
tCaseConverter.InputType = InputTypeJSON
|
||||
tCaseConverter.TCase = tCase
|
||||
break
|
||||
}
|
||||
casePostman := new(CasePostman)
|
||||
err = builtin.LoadFile(path, casePostman)
|
||||
// deal with postman field name conflict with swagger
|
||||
descriptionBackup := casePostman.Info.Description
|
||||
casePostman.Info.Description = ""
|
||||
if err == nil && !reflect.ValueOf(*casePostman).IsZero() {
|
||||
tCaseConverter.InputType = InputTypePostman
|
||||
casePostman.Info.Description = descriptionBackup
|
||||
tCaseConverter.CasePostman = casePostman
|
||||
break
|
||||
}
|
||||
caseSwagger := new(spec.Swagger)
|
||||
err = builtin.LoadFile(path, caseSwagger)
|
||||
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
|
||||
tCaseConverter.InputType = InputTypeSwagger
|
||||
tCaseConverter.CaseSwagger = caseSwagger
|
||||
}
|
||||
case ".yaml", ".yml":
|
||||
tCase := new(hrp.TCase)
|
||||
err = builtin.LoadFile(path, tCase)
|
||||
if err == nil && !reflect.ValueOf(*tCase).IsZero() {
|
||||
tCaseConverter.InputType = InputTypeYAML
|
||||
tCaseConverter.TCase = tCase
|
||||
break
|
||||
}
|
||||
caseSwagger := new(spec.Swagger)
|
||||
err = builtin.LoadFile(path, caseSwagger)
|
||||
if err == nil && !reflect.ValueOf(*caseSwagger).IsZero() {
|
||||
tCaseConverter.InputType = InputTypeSwagger
|
||||
tCaseConverter.CaseSwagger = caseSwagger
|
||||
}
|
||||
case ".go": // TODO
|
||||
tCaseConverter.InputType = InputTypeGoTest
|
||||
case ".py": // TODO
|
||||
tCaseConverter.InputType = InputTypePyTest
|
||||
case ".jmx": // TODO
|
||||
tCaseConverter.InputType = InputTypeJMeter
|
||||
default:
|
||||
log.Warn().
|
||||
Str("input path", tCaseConverter.InputPath).
|
||||
Msgf("unsupported file type: %v", extName)
|
||||
}
|
||||
if tCaseConverter.InputType != InputTypeUnknown {
|
||||
log.Info().
|
||||
Str("input path", tCaseConverter.InputPath).
|
||||
Msgf("load case as: %s", tCaseConverter.InputType.String())
|
||||
} else {
|
||||
log.Error().Err(err).
|
||||
Str("input path", tCaseConverter.InputPath).
|
||||
Msgf("failed to load case")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *TCaseConverter) SetProfile(path string) {
|
||||
log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile")
|
||||
profile := new(Profile)
|
||||
err := builtin.LoadFile(path, profile)
|
||||
if err != nil {
|
||||
log.Warn().Str("path", path).
|
||||
Msg("failed to load profile, ignore!")
|
||||
return
|
||||
}
|
||||
c.Profile = profile
|
||||
}
|
||||
|
||||
func (c *TCaseConverter) SetOutputDir(dir string) {
|
||||
log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory")
|
||||
c.OutputDir = dir
|
||||
}
|
||||
|
||||
func (c *TCaseConverter) genOutputPath(suffix string) string {
|
||||
outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix
|
||||
if c.OutputDir != "" {
|
||||
return filepath.Join(c.OutputDir, outFileFullName)
|
||||
} else {
|
||||
return filepath.Join(filepath.Dir(c.InputPath), outFileFullName)
|
||||
}
|
||||
// TODO avoid outFileFullName conflict?
|
||||
}
|
||||
|
||||
func (c *TCaseConverter) ToPyTest() (string, error) {
|
||||
script := convertConfig(c.TCase.Config)
|
||||
println(script)
|
||||
return script, nil
|
||||
}
|
||||
|
||||
func convertConfig(config *hrp.TConfig) string {
|
||||
script := fmt.Sprintf("Config('%s')", config.Name)
|
||||
|
||||
if config.Variables != nil {
|
||||
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
|
||||
}
|
||||
if config.BaseURL != "" {
|
||||
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
|
||||
}
|
||||
if config.Export != nil {
|
||||
script += fmt.Sprintf(".export(*%v)", config.Export)
|
||||
}
|
||||
script += fmt.Sprintf(".verify(%v)", config.Verify)
|
||||
|
||||
return script
|
||||
}
|
||||
|
||||
func (c *TCaseConverter) ToGoTest() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format
|
||||
type ICaseConverter interface {
|
||||
Struct() *TCaseConverter
|
||||
ToJSON() (string, error)
|
||||
ToYAML() (string, error)
|
||||
ToGoTest() (string, error)
|
||||
ToPyTest() (string, error)
|
||||
}
|
||||
|
||||
func Run(outputType OutputType, outputDir, profilePath string, args []string) {
|
||||
// report event
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()),
|
||||
})
|
||||
|
||||
// identify input and load converters
|
||||
var iCaseConverters []ICaseConverter
|
||||
for _, arg := range args {
|
||||
tCaseConverter := NewTCaseConverter(arg)
|
||||
tCaseConverter.OutputType = outputType
|
||||
if outputDir != "" {
|
||||
tCaseConverter.SetOutputDir(outputDir)
|
||||
}
|
||||
if profilePath != "" {
|
||||
tCaseConverter.SetProfile(profilePath)
|
||||
}
|
||||
switch tCaseConverter.InputType {
|
||||
case InputTypeHAR:
|
||||
iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter))
|
||||
case InputTypePostman:
|
||||
iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter))
|
||||
case InputTypeJSON:
|
||||
iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter))
|
||||
case InputTypeYAML:
|
||||
iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter))
|
||||
case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest:
|
||||
log.Warn().
|
||||
Str("input path", tCaseConverter.InputPath).
|
||||
Msg("case type not supported yet, ignore!")
|
||||
default:
|
||||
log.Warn().
|
||||
Str("input path", tCaseConverter.InputPath).
|
||||
Msg("unknown case type, ignore!")
|
||||
}
|
||||
}
|
||||
|
||||
// start converting
|
||||
var outputFiles []string
|
||||
var err error
|
||||
for _, iCaseConverter := range iCaseConverters {
|
||||
log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting")
|
||||
var outputFile string
|
||||
switch iCaseConverter.Struct().OutputType {
|
||||
case OutputTypeYAML:
|
||||
outputFile, err = iCaseConverter.ToYAML()
|
||||
case OutputTypeGoTest:
|
||||
outputFile, err = iCaseConverter.ToGoTest()
|
||||
case OutputTypePyTest:
|
||||
outputFile, err = iCaseConverter.ToPyTest()
|
||||
default:
|
||||
outputFile, err = iCaseConverter.ToJSON()
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("input path", iCaseConverter.Struct().InputPath).
|
||||
Msg("error occurs during converting")
|
||||
continue
|
||||
}
|
||||
outputFiles = append(outputFiles, outputFile)
|
||||
}
|
||||
log.Info().Strs("output files", outputFiles).Msg("conversion completed")
|
||||
}
|
||||
|
||||
func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) {
|
||||
tCase := iCaseConverter.Struct().TCase
|
||||
if tCase == nil {
|
||||
return nil, errors.Errorf("empty json/yaml testcase occurs")
|
||||
}
|
||||
profile := iCaseConverter.Struct().Profile
|
||||
if profile == nil {
|
||||
return tCase, nil
|
||||
}
|
||||
for _, step := range tCase.TestSteps {
|
||||
// override original headers and cookies
|
||||
if profile.Override {
|
||||
step.Request.Headers = make(map[string]string)
|
||||
step.Request.Cookies = make(map[string]string)
|
||||
}
|
||||
// update (create if not existed) original headers and cookies
|
||||
if step.Request.Headers == nil {
|
||||
step.Request.Headers = make(map[string]string)
|
||||
}
|
||||
if step.Request.Cookies == nil {
|
||||
step.Request.Cookies = make(map[string]string)
|
||||
}
|
||||
for k, v := range profile.Headers {
|
||||
step.Request.Headers[k] = v
|
||||
}
|
||||
for k, v := range profile.Cookies {
|
||||
step.Request.Cookies[k] = v
|
||||
}
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func convertToPyTest(iCaseConverter ICaseConverter) (string, error) {
|
||||
// convert to temporary json testcase
|
||||
jsonPath, err := iCaseConverter.ToJSON()
|
||||
inputType := iCaseConverter.Struct().InputType
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String())
|
||||
}
|
||||
defer func() {
|
||||
if jsonPath != "" {
|
||||
if err = os.Remove(jsonPath); err != nil {
|
||||
log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// convert from temporary json testcase to pytest
|
||||
converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath))
|
||||
pyTestPath, err := converterJSON.MakePyTestScript()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ")
|
||||
}
|
||||
|
||||
// rename resultant pytest
|
||||
renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest)
|
||||
err = os.Rename(pyTestPath, renamedPyTestPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file")
|
||||
return pyTestPath, nil
|
||||
}
|
||||
return renamedPyTestPath, nil
|
||||
}
|
||||
60
hrp/internal/convert/converter_gotest.go
Normal file
60
hrp/internal/convert/converter_gotest.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
func convert2GoTestScripts(paths ...string) error {
|
||||
log.Warn().Msg("convert to gotest scripts is not supported yet")
|
||||
os.Exit(1)
|
||||
|
||||
// TODO
|
||||
var testCasePaths []hrp.ITestCase
|
||||
for _, path := range paths {
|
||||
testCasePath := hrp.TestCasePath(path)
|
||||
testCasePaths = append(testCasePaths, &testCasePath)
|
||||
}
|
||||
|
||||
testCases, err := hrp.LoadTestCases(testCasePaths...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load testcases")
|
||||
return err
|
||||
}
|
||||
|
||||
var pytestPaths []string
|
||||
for _, testCase := range testCases {
|
||||
tc := testCase.ToTCase()
|
||||
converter := TCaseConverter{
|
||||
TCase: tc,
|
||||
}
|
||||
pytestPath, err := converter.ToPyTest()
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("originPath", tc.Config.Path).
|
||||
Msg("convert to pytest failed")
|
||||
continue
|
||||
}
|
||||
log.Info().
|
||||
Str("pytestPath", pytestPath).
|
||||
Str("originPath", tc.Config.Path).
|
||||
Msg("convert to pytest success")
|
||||
pytestPaths = append(pytestPaths, pytestPath)
|
||||
}
|
||||
|
||||
// format pytest scripts with black
|
||||
python3, err := builtin.EnsurePython3Venv("black")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := append([]string{"-m", "black"}, pytestPaths...)
|
||||
return builtin.ExecCommand(python3, args...)
|
||||
}
|
||||
|
||||
//go:embed testcase.tmpl
|
||||
var testcaseTemplate string
|
||||
@@ -1,6 +1,22 @@
|
||||
package har2case
|
||||
package convert
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
// ==================== model definition starts here ====================
|
||||
|
||||
/*
|
||||
HTTP Archive (HAR) format
|
||||
@@ -8,8 +24,8 @@ https://w3c.github.io/web-performance/specs/HAR/Overview.html
|
||||
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
|
||||
*/
|
||||
|
||||
// Har is a container type for deserialization
|
||||
type Har struct {
|
||||
// CaseHar is a container type for deserialization
|
||||
type CaseHar struct {
|
||||
Log Log `json:"log"`
|
||||
}
|
||||
|
||||
@@ -338,3 +354,333 @@ type TestResult struct {
|
||||
Method string `json:"method"`
|
||||
HarFile string `json:"harfile"`
|
||||
}
|
||||
|
||||
// ==================== model definition ends here ====================
|
||||
|
||||
func NewConverterHAR(converter *TCaseConverter) *ConverterHAR {
|
||||
return &ConverterHAR{
|
||||
converter: converter,
|
||||
}
|
||||
}
|
||||
|
||||
type ConverterHAR struct {
|
||||
converter *TCaseConverter
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) Struct() *TCaseConverter {
|
||||
return c.converter
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) ToJSON() (string, error) {
|
||||
tCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||
err = builtin.Dump2JSON(tCase, jsonPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsonPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) ToYAML() (string, error) {
|
||||
tCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||
err = builtin.Dump2YAML(tCase, yamlPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return yamlPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) ToGoTest() (string, error) {
|
||||
//TODO implement me
|
||||
return "", errors.New("convert from har to gotest scripts is not supported yet")
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) ToPyTest() (string, error) {
|
||||
return convertToPyTest(c)
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) makeTestCase() (*hrp.TCase, error) {
|
||||
teststeps, err := c.prepareTestSteps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tCase := &hrp.TCase{
|
||||
Config: c.prepareConfig(),
|
||||
TestSteps: teststeps,
|
||||
}
|
||||
err = tCase.MakeCompat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) load() (*CaseHar, error) {
|
||||
har := c.converter.CaseHAR
|
||||
if har == nil {
|
||||
return nil, errors.New("empty har case occurs")
|
||||
}
|
||||
return har, nil
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) prepareConfig() *hrp.TConfig {
|
||||
return hrp.NewConfig("testcase description").
|
||||
SetVerifySSL(false)
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) prepareTestSteps() ([]*hrp.TStep, error) {
|
||||
har, err := c.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var steps []*hrp.TStep
|
||||
for _, entry := range har.Log.Entries {
|
||||
step, err := c.prepareTestStep(&entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (c *ConverterHAR) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
|
||||
log.Info().
|
||||
Str("method", entry.Request.Method).
|
||||
Str("url", entry.Request.URL).
|
||||
Msg("convert teststep")
|
||||
|
||||
step := &stepFromHAR{
|
||||
TStep: hrp.TStep{
|
||||
Request: &hrp.Request{},
|
||||
Validators: make([]interface{}, 0),
|
||||
},
|
||||
profile: c.converter.Profile,
|
||||
}
|
||||
if err := step.makeRequestMethod(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestURL(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestParams(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestCookies(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestHeaders(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestBody(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeValidate(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step.TStep, nil
|
||||
}
|
||||
|
||||
type stepFromHAR struct {
|
||||
hrp.TStep
|
||||
profile *Profile
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestMethod(entry *Entry) error {
|
||||
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestURL(entry *Entry) error {
|
||||
u, err := url.Parse(entry.Request.URL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("make request url failed")
|
||||
return err
|
||||
}
|
||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestParams(entry *Entry) error {
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
for _, param := range entry.Request.QueryString {
|
||||
s.Request.Params[param.Name] = param.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestCookies(entry *Entry) error {
|
||||
// use cookies from har
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
for _, cookie := range entry.Request.Cookies {
|
||||
s.Request.Cookies[cookie.Name] = cookie.Value
|
||||
}
|
||||
|
||||
if s.profile == nil {
|
||||
return nil
|
||||
}
|
||||
// override all cookies according to the profile
|
||||
if s.profile.Override {
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
}
|
||||
// create or update the cookies according to the profile
|
||||
for k, v := range s.profile.Cookies {
|
||||
s.Request.Cookies[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error {
|
||||
// use headers from har
|
||||
s.Request.Headers = make(map[string]string)
|
||||
for _, header := range entry.Request.Headers {
|
||||
if strings.EqualFold(header.Name, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.Request.Headers[header.Name] = header.Value
|
||||
}
|
||||
|
||||
if s.profile == nil {
|
||||
return nil
|
||||
}
|
||||
// override all headers according to the profile
|
||||
if s.profile.Override {
|
||||
s.Request.Headers = make(map[string]string)
|
||||
}
|
||||
// create or update the headers according to the profile
|
||||
for k, v := range s.profile.Headers {
|
||||
s.Request.Headers[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeRequestBody(entry *Entry) error {
|
||||
mimeType := entry.Request.PostData.MimeType
|
||||
if mimeType == "" {
|
||||
// GET/HEAD/DELETE without body
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST/PUT with body
|
||||
if strings.HasPrefix(mimeType, "application/json") {
|
||||
// post json
|
||||
var body interface{}
|
||||
if entry.Request.PostData.Text == "" {
|
||||
body = nil
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("make request body failed")
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.Request.Body = body
|
||||
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
|
||||
// post form
|
||||
var paramsList []string
|
||||
for _, param := range entry.Request.PostData.Params {
|
||||
paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
|
||||
}
|
||||
s.Request.Body = strings.Join(paramsList, "&")
|
||||
} else if strings.HasPrefix(mimeType, "text/plain") {
|
||||
// post raw data
|
||||
s.Request.Body = entry.Request.PostData.Text
|
||||
} else {
|
||||
// TODO
|
||||
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromHAR) makeValidate(entry *Entry) error {
|
||||
// make validator for response status code
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: "status_code",
|
||||
Assert: "equals",
|
||||
Expect: entry.Response.Status,
|
||||
Message: "assert response status code",
|
||||
})
|
||||
|
||||
// make validators for response headers
|
||||
for _, header := range entry.Response.Headers {
|
||||
// assert Content-Type
|
||||
if strings.EqualFold(header.Name, "Content-Type") {
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: "headers.\"Content-Type\"",
|
||||
Assert: "equals",
|
||||
Expect: header.Value,
|
||||
Message: "assert response header Content-Type",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// make validators for response body
|
||||
respBody := entry.Response.Content
|
||||
if respBody.Text == "" {
|
||||
// response body is empty
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(respBody.MimeType, "application/json") {
|
||||
var data []byte
|
||||
var err error
|
||||
// response body is json
|
||||
if respBody.Encoding == "base64" {
|
||||
// decode base64 text
|
||||
data, err = base64.StdEncoding.DecodeString(respBody.Text)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decode base64 error")
|
||||
}
|
||||
} else if respBody.Encoding == "" {
|
||||
// no encoding
|
||||
data = []byte(respBody.Text)
|
||||
} else {
|
||||
// other encoding type
|
||||
return nil
|
||||
}
|
||||
// convert to json
|
||||
var body interface{}
|
||||
if err = json.Unmarshal(data, &body); err != nil {
|
||||
return errors.Wrap(err, "json.Unmarshal body error")
|
||||
}
|
||||
jsonBody, ok := body.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("response body is not json, not matched with MimeType")
|
||||
}
|
||||
|
||||
// response body is json
|
||||
keys := make([]string, 0, len(jsonBody))
|
||||
for k := range jsonBody {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
// sort map keys to keep validators in stable order
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
value := jsonBody[key]
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
continue
|
||||
case []interface{}:
|
||||
continue
|
||||
default:
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: fmt.Sprintf("body.%s", key),
|
||||
Assert: "equals",
|
||||
Expect: v,
|
||||
Message: fmt.Sprintf("assert response body %s", key),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package har2case
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
harPath = "../../../examples/data/har/demo.har"
|
||||
harPath2 = "../../../examples/data/har/postman-echo.har"
|
||||
profilePath = "../../../examples/data/har/profile.yml"
|
||||
harPath = "../../../examples/data/har/demo.har"
|
||||
harPath2 = "../../../examples/data/har/postman-echo.har"
|
||||
harProfileOverridePath = "../../../examples/data/har/profile_override.yml"
|
||||
)
|
||||
|
||||
func TestGenJSON(t *testing.T) {
|
||||
jsonPath, err := NewHAR(harPath).GenJSON()
|
||||
var converterHAR = NewConverterHAR(NewTCaseConverter(harPath))
|
||||
var converterHAR2 = NewConverterHAR(NewTCaseConverter(harPath2))
|
||||
|
||||
func TestHAR2JSON(t *testing.T) {
|
||||
jsonPath, err := converterHAR.ToJSON()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -24,8 +27,8 @@ func TestGenJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenYAML(t *testing.T) {
|
||||
yamlPath, err := NewHAR(harPath2).GenYAML()
|
||||
func TestHAR2YAML(t *testing.T) {
|
||||
yamlPath, err := converterHAR2.ToYAML()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -35,8 +38,7 @@ func TestGenYAML(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadHAR(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
h, err := har.load()
|
||||
h, err := converterHAR.load()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -49,28 +51,28 @@ func TestLoadHAR(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadHARWithProfile(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
har.SetProfile(profilePath)
|
||||
_, err := har.load()
|
||||
tCaseConverter := NewTCaseConverter(harPath)
|
||||
tCaseConverter.SetProfile(harProfileOverridePath)
|
||||
h := NewConverterHAR(tCaseConverter)
|
||||
_, err := h.load()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if !assert.Equal(t,
|
||||
map[string]interface{}{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
har.profile["headers"]) {
|
||||
map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
h.converter.Profile.Headers) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t,
|
||||
map[string]interface{}{"UserName": "debugtalk"},
|
||||
har.profile["cookies"]) {
|
||||
map[string]string{"UserName": "debugtalk"},
|
||||
h.converter.Profile.Cookies) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTestCase(t *testing.T) {
|
||||
har := NewHAR(harPath)
|
||||
tCase, err := har.makeTestCase()
|
||||
func TestMakeTestCaseFromHAR(t *testing.T) {
|
||||
tCase, err := converterHAR.makeTestCase()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -135,21 +137,13 @@ func TestMakeTestCase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFilenameWithoutExtension(t *testing.T) {
|
||||
filename := getFilenameWithoutExtension(harPath2)
|
||||
if !assert.Equal(t, "postman-echo", filename) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestURL(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
URL: "http://127.0.0.1:8080/api/login",
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -160,7 +154,6 @@ func TestMakeRequestURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakeRequestHeaders(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -169,7 +162,7 @@ func TestMakeRequestHeaders(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -181,9 +174,10 @@ func TestMakeRequestHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
har.SetProfile(profilePath)
|
||||
func TestMakeRequestHeadersWithProfileOverride(t *testing.T) {
|
||||
tCaseConverter := NewTCaseConverter(harPath)
|
||||
tCaseConverter.SetProfile(harProfileOverridePath)
|
||||
h := NewConverterHAR(tCaseConverter)
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -192,7 +186,7 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := h.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -205,7 +199,6 @@ func TestMakeRequestHeadersWithProfile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakeRequestCookies(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -215,7 +208,7 @@ func TestMakeRequestCookies(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -228,9 +221,10 @@ func TestMakeRequestCookies(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
har.SetProfile(profilePath)
|
||||
func TestMakeRequestCookiesWithProfileOverride(t *testing.T) {
|
||||
tCaseConverter := NewTCaseConverter(harPath)
|
||||
tCaseConverter.SetProfile(harProfileOverridePath)
|
||||
h := NewConverterHAR(tCaseConverter)
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -240,7 +234,7 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := h.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -253,7 +247,6 @@ func TestMakeRequestCookiesWithProfile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakeRequestDataParams(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -266,7 +259,7 @@ func TestMakeRequestDataParams(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -277,7 +270,6 @@ func TestMakeRequestDataParams(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakeRequestDataJSON(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -287,7 +279,7 @@ func TestMakeRequestDataJSON(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -298,7 +290,6 @@ func TestMakeRequestDataJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakeRequestDataTextEmpty(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Request: Request{
|
||||
Method: "POST",
|
||||
@@ -308,7 +299,7 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
@@ -319,7 +310,6 @@ func TestMakeRequestDataTextEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakeValidate(t *testing.T) {
|
||||
har := NewHAR("")
|
||||
entry := &Entry{
|
||||
Response: Response{
|
||||
Status: 200,
|
||||
@@ -335,7 +325,7 @@ func TestMakeValidate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
step, err := har.prepareTestStep(entry)
|
||||
step, err := converterHAR.prepareTestStep(entry)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
86
hrp/internal/convert/converter_json.go
Normal file
86
hrp/internal/convert/converter_json.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
func NewConverterJSON(converter *TCaseConverter) *ConverterJSON {
|
||||
return &ConverterJSON{
|
||||
converter: converter,
|
||||
}
|
||||
}
|
||||
|
||||
type ConverterJSON struct {
|
||||
converter *TCaseConverter
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) Struct() *TCaseConverter {
|
||||
return c.converter
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) ToJSON() (string, error) {
|
||||
testCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||
err = builtin.Dump2JSON(testCase, jsonPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsonPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) ToYAML() (string, error) {
|
||||
testCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||
err = builtin.Dump2YAML(testCase, yamlPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return yamlPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) ToGoTest() (string, error) {
|
||||
//TODO implement me
|
||||
return "", errors.New("convert from json testcase to gotest scripts is not supported yet")
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) ToPyTest() (string, error) {
|
||||
return convertToPyTest(c)
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) MakePyTestScript() (string, error) {
|
||||
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
|
||||
python3, err := builtin.EnsurePython3Venv(httprunner)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
args := append([]string{"-m", "httprunner", "make"}, c.converter.InputPath)
|
||||
err = builtin.ExecCommand(python3, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.converter.genOutputPath(suffixPyTest), nil
|
||||
}
|
||||
|
||||
func (c *ConverterJSON) makeTestCase() (*hrp.TCase, error) {
|
||||
tCase, err := makeTestCaseFromJSONYAML(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tCase.MakeCompat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
488
hrp/internal/convert/converter_postman.go
Normal file
488
hrp/internal/convert/converter_postman.go
Normal file
@@ -0,0 +1,488 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
// ==================== model definition starts here ====================
|
||||
|
||||
/*
|
||||
Postman Collection format reference:
|
||||
https://schema.postman.com/json/collection/v2.0.0/collection.json
|
||||
https://schema.postman.com/json/collection/v2.1.0/collection.json
|
||||
*/
|
||||
|
||||
// CasePostman represents the postman exported file
|
||||
type CasePostman struct {
|
||||
Info TInfo `json:"info"`
|
||||
Items []TItem `json:"item"`
|
||||
}
|
||||
|
||||
// TInfo gives information about the collection
|
||||
type TInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Schema string `json:"schema"`
|
||||
}
|
||||
|
||||
// TItem contains the detail information of request and expected responses
|
||||
// item could be defined recursively
|
||||
type TItem struct {
|
||||
Items []TItem `json:"item"`
|
||||
Name string `json:"name"`
|
||||
Request TRequest `json:"request"`
|
||||
Responses []TResponse `json:"response"`
|
||||
}
|
||||
|
||||
type TRequest struct {
|
||||
Method string `json:"method"`
|
||||
Headers []TField `json:"header"`
|
||||
Body TBody `json:"body"`
|
||||
URL TUrl `json:"url"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type TResponse struct {
|
||||
Name string `json:"name"`
|
||||
OriginalRequest TRequest `json:"originalRequest"`
|
||||
Status string `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Headers []TField `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type TUrl struct {
|
||||
Raw string `json:"raw"`
|
||||
Protocol string `json:"protocol"`
|
||||
Path []string `json:"path"`
|
||||
Description string `json:"description"`
|
||||
Query []TField `json:"query"`
|
||||
Variable []TField `json:"variable"`
|
||||
}
|
||||
|
||||
type TField struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Src string `json:"src"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
|
||||
type TBody struct {
|
||||
Mode string `json:"mode"`
|
||||
FormData []TField `json:"formdata"`
|
||||
URLEncoded []TField `json:"urlencoded"`
|
||||
Raw string `json:"raw"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Options interface{} `json:"options"`
|
||||
}
|
||||
|
||||
// ==================== model definition ends here ====================
|
||||
|
||||
const (
|
||||
enumBodyRaw = "raw"
|
||||
enumBodyUrlEncoded = "urlencoded"
|
||||
enumBodyFormData = "formdata"
|
||||
enumBodyFile = "file"
|
||||
enumBodyGraphQL = "graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
enumFieldTypeText = "text"
|
||||
enumFieldTypeFile = "file"
|
||||
)
|
||||
|
||||
var contentTypeMap = map[string]string{
|
||||
"text": "text/plain",
|
||||
"javascript": "application/javascript",
|
||||
"json": "application/json",
|
||||
"html": "text/html",
|
||||
"xml": "application/xml",
|
||||
}
|
||||
|
||||
func NewConverterPostman(converter *TCaseConverter) *ConverterPostman {
|
||||
return &ConverterPostman{
|
||||
converter: converter,
|
||||
}
|
||||
}
|
||||
|
||||
type ConverterPostman struct {
|
||||
converter *TCaseConverter
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) Struct() *TCaseConverter {
|
||||
return c.converter
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) ToJSON() (string, error) {
|
||||
testCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||
err = builtin.Dump2JSON(testCase, jsonPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsonPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) ToYAML() (string, error) {
|
||||
testCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||
err = builtin.Dump2YAML(testCase, yamlPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return yamlPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) ToGoTest() (string, error) {
|
||||
//TODO implement me
|
||||
return "", errors.New("convert from postman to gotest scripts is not supported yet")
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) ToPyTest() (string, error) {
|
||||
return convertToPyTest(c)
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) makeTestCase() (*hrp.TCase, error) {
|
||||
casePostman, err := c.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
teststeps, err := c.prepareTestSteps(casePostman)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tCase := &hrp.TCase{
|
||||
Config: c.prepareConfig(casePostman),
|
||||
TestSteps: teststeps,
|
||||
}
|
||||
err = tCase.MakeCompat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) load() (*CasePostman, error) {
|
||||
casePostman := c.converter.CasePostman
|
||||
if casePostman == nil {
|
||||
return nil, errors.New("empty postman case occurs")
|
||||
}
|
||||
return casePostman, nil
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) prepareConfig(casePostman *CasePostman) *hrp.TConfig {
|
||||
return hrp.NewConfig(casePostman.Info.Name).
|
||||
SetVerifySSL(false)
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) prepareTestSteps(casePostman *CasePostman) ([]*hrp.TStep, error) {
|
||||
// recursively convert collection items into a list
|
||||
var itemList []TItem
|
||||
for _, item := range casePostman.Items {
|
||||
extractItemList(item, &itemList)
|
||||
}
|
||||
|
||||
var steps []*hrp.TStep
|
||||
for _, item := range itemList {
|
||||
step, err := c.prepareTestStep(&item, steps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func extractItemList(item TItem, itemList *[]TItem) {
|
||||
// current item contains no other items and request is not empty
|
||||
if len(item.Items) == 0 {
|
||||
if !reflect.DeepEqual(item.Request, TRequest{}) {
|
||||
*itemList = append(*itemList, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// look up all items inside
|
||||
for _, i := range item.Items {
|
||||
// append item name
|
||||
i.Name = fmt.Sprintf("%s - %s", item.Name, i.Name)
|
||||
extractItemList(i, itemList)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConverterPostman) prepareTestStep(item *TItem, steps []*hrp.TStep) (*hrp.TStep, error) {
|
||||
log.Info().
|
||||
Str("method", item.Request.Method).
|
||||
Str("url", item.Request.URL.Raw).
|
||||
Msg("convert teststep")
|
||||
|
||||
step := &stepFromPostman{
|
||||
TStep: hrp.TStep{
|
||||
Request: &hrp.Request{},
|
||||
Validators: make([]interface{}, 0),
|
||||
},
|
||||
profile: c.converter.Profile,
|
||||
}
|
||||
if err := step.makeRequestName(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestMethod(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestURL(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestParams(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestHeaders(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestCookies(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestBody(item, steps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step.TStep, nil
|
||||
}
|
||||
|
||||
type stepFromPostman struct {
|
||||
hrp.TStep
|
||||
profile *Profile
|
||||
}
|
||||
|
||||
// makeRequestName indicates the step name the same as item name
|
||||
func (s *stepFromPostman) makeRequestName(item *TItem) error {
|
||||
s.Name = item.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestMethod(item *TItem) error {
|
||||
s.Request.Method = hrp.HTTPMethod(item.Request.Method)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestURL(item *TItem) error {
|
||||
rawUrl := item.Request.URL.Raw
|
||||
// parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2
|
||||
for _, field := range item.Request.URL.Variable {
|
||||
pathVar := ":" + field.Key
|
||||
rawUrl = strings.Replace(rawUrl, pathVar, field.Value, -1)
|
||||
}
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parse URL error")
|
||||
}
|
||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestParams(item *TItem) error {
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
for _, field := range item.Request.URL.Query {
|
||||
if field.Disabled {
|
||||
continue
|
||||
}
|
||||
s.Request.Params[field.Key] = field.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestHeaders(item *TItem) error {
|
||||
// headers defined in postman collection
|
||||
s.Request.Headers = make(map[string]string)
|
||||
for _, field := range item.Request.Headers {
|
||||
if field.Disabled || strings.EqualFold(field.Key, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.Request.Headers[field.Key] = field.Value
|
||||
}
|
||||
|
||||
if s.profile == nil {
|
||||
return nil
|
||||
}
|
||||
// override all headers according to the profile
|
||||
if s.profile.Override {
|
||||
s.Request.Headers = make(map[string]string)
|
||||
}
|
||||
// create or update the headers according to the profile
|
||||
for k, v := range s.profile.Headers {
|
||||
s.Request.Headers[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestCookies(item *TItem) error {
|
||||
// cookies defined in postman collection
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
for _, field := range item.Request.Headers {
|
||||
if field.Disabled || !strings.EqualFold(field.Key, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.parseRequestCookiesMap(field.Value)
|
||||
}
|
||||
|
||||
if s.profile == nil {
|
||||
return nil
|
||||
}
|
||||
// override all cookies according to the profile
|
||||
if s.profile.Override {
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
}
|
||||
// create or update the cookies according to the profile
|
||||
for k, v := range s.profile.Cookies {
|
||||
s.Request.Cookies[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) parseRequestCookiesMap(cookies string) {
|
||||
for _, cookie := range strings.Split(cookies, ";") {
|
||||
cookie = strings.TrimSpace(cookie)
|
||||
index := strings.Index(cookie, "=")
|
||||
if index == -1 {
|
||||
log.Warn().Str("cookie", cookie).Msg("cookie format invalid")
|
||||
continue
|
||||
}
|
||||
s.Request.Cookies[cookie[:index]] = cookie[index+1:]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBody(item *TItem, steps []*hrp.TStep) error {
|
||||
mode := item.Request.Body.Mode
|
||||
if mode == "" {
|
||||
return nil
|
||||
}
|
||||
switch mode {
|
||||
case enumBodyRaw:
|
||||
return s.makeRequestBodyRaw(item)
|
||||
case enumBodyFormData:
|
||||
return s.makeRequestBodyFormData(item, steps)
|
||||
case enumBodyUrlEncoded:
|
||||
return s.makeRequestBodyUrlEncoded(item)
|
||||
case enumBodyFile, enumBodyGraphQL:
|
||||
return errors.Errorf("unsupported body type: %v", mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("make request body (raw) failed: %v", p)
|
||||
}
|
||||
}()
|
||||
|
||||
// extract language type, default languageType: text
|
||||
languageType := "text"
|
||||
iOptions := item.Request.Body.Options
|
||||
if iOptions != nil {
|
||||
iLanguage := iOptions.(map[string]interface{})["raw"]
|
||||
if iLanguage != nil {
|
||||
languageType = iLanguage.(map[string]interface{})["language"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
// make request body and indicate Content-Type
|
||||
rawBody := item.Request.Body.Raw
|
||||
if languageType == "json" {
|
||||
var iBody interface{}
|
||||
err = json.Unmarshal([]byte(rawBody), &iBody)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "make request body (raw -> json) failed")
|
||||
}
|
||||
s.Request.Body = iBody
|
||||
} else {
|
||||
s.Request.Body = rawBody
|
||||
}
|
||||
s.Request.Headers["Content-Type"] = contentTypeMap[languageType]
|
||||
return
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBodyFormData(item *TItem, steps []*hrp.TStep) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "make request body form-data failed")
|
||||
}
|
||||
}()
|
||||
payload := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(payload)
|
||||
for _, field := range item.Request.Body.FormData {
|
||||
if field.Disabled {
|
||||
continue
|
||||
}
|
||||
// form data could be text or file
|
||||
if field.Type == enumFieldTypeText {
|
||||
err = writer.WriteField(field.Key, field.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if field.Type == enumFieldTypeFile {
|
||||
err = writeFormDataFile(writer, &field)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = writer.Close()
|
||||
s.Request.Body = payload.String()
|
||||
s.Request.Headers["Content-Type"] = writer.FormDataContentType()
|
||||
return
|
||||
}
|
||||
|
||||
func writeFormDataFile(writer *multipart.Writer, field *TField) error {
|
||||
file, err := os.Open(field.Src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
formFile, err := writer.CreateFormFile(field.Key, filepath.Base(field.Src))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(formFile, file)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error {
|
||||
payloadMap := make(map[string]string)
|
||||
for _, field := range item.Request.Body.URLEncoded {
|
||||
if field.Disabled {
|
||||
continue
|
||||
}
|
||||
payloadMap[field.Key] = field.Value
|
||||
}
|
||||
s.Request.Body = payloadMap
|
||||
s.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO makeValidate from example response
|
||||
func (s *stepFromPostman) makeValidate(item *TItem) error {
|
||||
return nil
|
||||
}
|
||||
159
hrp/internal/convert/converter_postman_test.go
Normal file
159
hrp/internal/convert/converter_postman_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
collectionPath = "../../../examples/data/postman/postman_collection.json"
|
||||
collectionProfileOverridePath = "../../../examples/data/postman/profile_override.yml"
|
||||
collectionProfilePath = "../../../examples/data/postman/profile.yml"
|
||||
)
|
||||
|
||||
var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath))
|
||||
|
||||
func TestPostman2JSON(t *testing.T) {
|
||||
jsonPath, err := converterPostman.ToJSON()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.NotEmpty(t, jsonPath) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostman2YAML(t *testing.T) {
|
||||
yamlPath, err := converterPostman.ToYAML()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.NotEmpty(t, yamlPath) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCollection(t *testing.T) {
|
||||
casePostman, err := converterPostman.load()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTestCaseFromCollection(t *testing.T) {
|
||||
tCase, err := converterPostman.makeTestCase()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check name
|
||||
if !assert.Equal(t, "postman collection demo", tCase.Config.Name) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check method
|
||||
if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check url
|
||||
if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check params
|
||||
if !assert.Equal(t, "v1", tCase.TestSteps[0].Request.Params["k1"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check cookies (pass, postman collection doesn't contains cookies)
|
||||
// check headers
|
||||
if !assert.Contains(t, tCase.TestSteps[1].Request.Headers["Content-Type"], "multipart/form-data") {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "application/x-www-form-urlencoded", tCase.TestSteps[2].Request.Headers["Content-Type"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "application/json", tCase.TestSteps[3].Request.Headers["Content-Type"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "text/plain", tCase.TestSteps[4].Request.Headers["Content-Type"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "HttpRunner", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
// check body
|
||||
if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, tCase.TestSteps[2].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": "v2"}, tCase.TestSteps[3].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "have a nice day", tCase.TestSteps[4].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, nil, tCase.TestSteps[5].Request.Body) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTestCaseWithProfileOverride(t *testing.T) {
|
||||
tCaseConverter := NewTCaseConverter(collectionPath)
|
||||
tCaseConverter.SetProfile(collectionProfileOverridePath)
|
||||
c := NewConverterPostman(tCaseConverter)
|
||||
tCase, err := c.makeTestCase()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
for _, step := range tCase.TestSteps {
|
||||
if step.Request.Method == "GET" && !assert.Len(t, step.Request.Headers, 1) {
|
||||
t.Fatal()
|
||||
}
|
||||
if step.Request.Method == "POST" && !assert.Len(t, step.Request.Headers, 2) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "all original headers will be overridden", step.Request.Headers["Header1"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Len(t, step.Request.Cookies, 1) {
|
||||
t.Fatal()
|
||||
}
|
||||
if !assert.Equal(t, "all original cookies will be overridden", step.Request.Cookies["Cookie1"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTestCaseWithProfile(t *testing.T) {
|
||||
tCaseConverter := NewTCaseConverter(collectionPath)
|
||||
tCaseConverter.SetProfile(collectionProfilePath)
|
||||
c := NewConverterPostman(tCaseConverter)
|
||||
tCase, err := c.makeTestCase()
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal()
|
||||
}
|
||||
// create cookies Cookie1 indicated in profile
|
||||
if !assert.Equal(t, "this cookie will be created or updated", tCase.TestSteps[0].Request.Cookies["Cookie1"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
// update header User-Agent indicated in profile
|
||||
if !assert.Equal(t, "this header will be created or updated", tCase.TestSteps[5].Request.Headers["User-Agent"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
// pass header Connection which is not indicated in profile
|
||||
if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
19
hrp/internal/convert/converter_pytest.go
Normal file
19
hrp/internal/convert/converter_pytest.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
func convert2PyTestScripts(paths ...string) error {
|
||||
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
|
||||
python3, err := builtin.EnsurePython3Venv(httprunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := append([]string{"-m", "httprunner", "make"}, paths...)
|
||||
return builtin.ExecCommand(python3, args...)
|
||||
}
|
||||
69
hrp/internal/convert/converter_yaml.go
Normal file
69
hrp/internal/convert/converter_yaml.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
func NewConverterYAML(converter *TCaseConverter) *ConverterYAML {
|
||||
return &ConverterYAML{
|
||||
converter: converter,
|
||||
}
|
||||
}
|
||||
|
||||
type ConverterYAML struct {
|
||||
converter *TCaseConverter
|
||||
}
|
||||
|
||||
func (c *ConverterYAML) Struct() *TCaseConverter {
|
||||
return c.converter
|
||||
}
|
||||
|
||||
func (c *ConverterYAML) ToJSON() (string, error) {
|
||||
testCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsonPath := c.converter.genOutputPath(suffixJSON)
|
||||
err = builtin.Dump2JSON(testCase, jsonPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return jsonPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterYAML) ToYAML() (string, error) {
|
||||
testCase, err := c.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlPath := c.converter.genOutputPath(suffixYAML)
|
||||
err = builtin.Dump2YAML(testCase, yamlPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return yamlPath, nil
|
||||
}
|
||||
|
||||
func (c *ConverterYAML) ToGoTest() (string, error) {
|
||||
//TODO implement me
|
||||
return "", errors.New("convert from yaml testcase to gotest scripts is not supported yet")
|
||||
}
|
||||
|
||||
func (c *ConverterYAML) ToPyTest() (string, error) {
|
||||
return convertToPyTest(c)
|
||||
}
|
||||
|
||||
func (c *ConverterYAML) makeTestCase() (*hrp.TCase, error) {
|
||||
tCase, err := makeTestCaseFromJSONYAML(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tCase.MakeCompat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/version"
|
||||
)
|
||||
|
||||
func Convert2TestScripts(destType string, paths ...string) error {
|
||||
// report event
|
||||
sdk.SendEvent(sdk.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: fmt.Sprintf("hrp convert --%s", destType),
|
||||
})
|
||||
|
||||
if destType == "gotest" {
|
||||
return convert2GoTestScripts(paths...)
|
||||
} else {
|
||||
// default to pytest
|
||||
return convert2PyTestScripts(paths...)
|
||||
}
|
||||
}
|
||||
|
||||
func convert2PyTestScripts(paths ...string) error {
|
||||
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
|
||||
python3, err := builtin.EnsurePython3Venv(httprunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := append([]string{"-m", "httprunner", "make"}, paths...)
|
||||
return builtin.ExecCommand(python3, args...)
|
||||
}
|
||||
|
||||
func convert2GoTestScripts(paths ...string) error {
|
||||
log.Warn().Msg("convert to gotest scripts is not supported yet")
|
||||
os.Exit(1)
|
||||
|
||||
// TODO
|
||||
var testCasePaths []hrp.ITestCase
|
||||
for _, path := range paths {
|
||||
testCasePath := hrp.TestCasePath(path)
|
||||
testCasePaths = append(testCasePaths, &testCasePath)
|
||||
}
|
||||
|
||||
testCases, err := hrp.LoadTestCases(testCasePaths...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to load testcases")
|
||||
return err
|
||||
}
|
||||
|
||||
var pytestPaths []string
|
||||
for _, testCase := range testCases {
|
||||
tc := testCase.ToTCase()
|
||||
converter := CaseConverter{
|
||||
TCase: tc,
|
||||
}
|
||||
pytestPath, err := converter.ToPyTest()
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("originPath", tc.Config.Path).
|
||||
Msg("convert to pytest failed")
|
||||
continue
|
||||
}
|
||||
log.Info().
|
||||
Str("pytestPath", pytestPath).
|
||||
Str("originPath", tc.Config.Path).
|
||||
Msg("convert to pytest success")
|
||||
pytestPaths = append(pytestPaths, pytestPath)
|
||||
}
|
||||
|
||||
// format pytest scripts with black
|
||||
python3, err := builtin.EnsurePython3Venv("black")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := append([]string{"-m", "black"}, pytestPaths...)
|
||||
return builtin.ExecCommand(python3, args...)
|
||||
}
|
||||
|
||||
//go:embed testcase.tmpl
|
||||
var testcaseTemplate string
|
||||
|
||||
type CaseConverter struct {
|
||||
*hrp.TCase
|
||||
}
|
||||
|
||||
func (c *CaseConverter) ToPyTest() (string, error) {
|
||||
script := convertConfig(c.TCase.Config)
|
||||
println(script)
|
||||
return script, nil
|
||||
}
|
||||
|
||||
func (c *CaseConverter) ToGoTest() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func convertConfig(config *hrp.TConfig) string {
|
||||
script := fmt.Sprintf("Config('%s')", config.Name)
|
||||
|
||||
if config.Variables != nil {
|
||||
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
|
||||
}
|
||||
if config.BaseURL != "" {
|
||||
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
|
||||
}
|
||||
if config.Export != nil {
|
||||
script += fmt.Sprintf(".export(*%v)", config.Export)
|
||||
}
|
||||
script += fmt.Sprintf(".verify(%v)", config.Verify)
|
||||
|
||||
return script
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# har2case
|
||||
|
||||
Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner and HttpRunner+.
|
||||
|
||||
## Install
|
||||
|
||||
## Quick Start
|
||||
|
||||
## Examples
|
||||
@@ -1,385 +0,0 @@
|
||||
package har2case
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
suffixJSON = ".json"
|
||||
suffixYAML = ".yaml"
|
||||
)
|
||||
|
||||
func NewHAR(path string) *har {
|
||||
return &har{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
type har struct {
|
||||
path string
|
||||
filterStr string
|
||||
excludeStr string
|
||||
profile map[string]interface{}
|
||||
outputDir string
|
||||
}
|
||||
|
||||
func (h *har) SetProfile(path string) {
|
||||
log.Info().Str("path", path).Msg("set profile")
|
||||
h.profile = make(map[string]interface{})
|
||||
err := builtin.LoadFile(path, h.profile)
|
||||
if err != nil {
|
||||
log.Warn().Str("path", path).
|
||||
Msg("invalid profile format, ignore!")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *har) SetOutputDir(dir string) {
|
||||
log.Info().Str("dir", dir).Msg("set output directory")
|
||||
h.outputDir = dir
|
||||
}
|
||||
|
||||
func (h *har) GenJSON() (jsonPath string, err error) {
|
||||
event := sdk.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: "hrp har2case --to-json",
|
||||
}
|
||||
// report start event
|
||||
go sdk.SendEvent(event)
|
||||
// report running timing event
|
||||
defer sdk.SendEvent(event.StartTiming("execution"))
|
||||
|
||||
tCase, err := h.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jsonPath = h.genOutputPath(suffixJSON)
|
||||
err = builtin.Dump2JSON(tCase, jsonPath)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *har) GenYAML() (yamlPath string, err error) {
|
||||
event := sdk.EventTracking{
|
||||
Category: "ConvertTests",
|
||||
Action: "hrp har2case --to-yaml",
|
||||
}
|
||||
// report start event
|
||||
go sdk.SendEvent(event)
|
||||
// report running timing event
|
||||
defer sdk.SendEvent(event.StartTiming("execution"))
|
||||
|
||||
tCase, err := h.makeTestCase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlPath = h.genOutputPath(suffixYAML)
|
||||
err = builtin.Dump2YAML(tCase, yamlPath)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *har) makeTestCase() (*hrp.TCase, error) {
|
||||
teststeps, err := h.prepareTestSteps()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tCase := &hrp.TCase{
|
||||
Config: h.prepareConfig(),
|
||||
TestSteps: teststeps,
|
||||
}
|
||||
return tCase, nil
|
||||
}
|
||||
|
||||
func (h *har) load() (*Har, error) {
|
||||
har := &Har{}
|
||||
err := builtin.LoadFile(h.path, har)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "load har failed")
|
||||
}
|
||||
return har, nil
|
||||
}
|
||||
|
||||
func (h *har) prepareConfig() *hrp.TConfig {
|
||||
return hrp.NewConfig("testcase description").
|
||||
SetVerifySSL(false)
|
||||
}
|
||||
|
||||
func (h *har) prepareTestSteps() ([]*hrp.TStep, error) {
|
||||
har, err := h.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var steps []*hrp.TStep
|
||||
for _, entry := range har.Log.Entries {
|
||||
step, err := h.prepareTestStep(&entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
|
||||
log.Info().
|
||||
Str("method", entry.Request.Method).
|
||||
Str("url", entry.Request.URL).
|
||||
Msg("convert teststep")
|
||||
|
||||
step := &tStep{
|
||||
TStep: hrp.TStep{
|
||||
Request: &hrp.Request{},
|
||||
Validators: make([]interface{}, 0),
|
||||
},
|
||||
profile: h.profile,
|
||||
}
|
||||
if err := step.makeRequestMethod(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestURL(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestParams(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestCookies(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestHeaders(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeRequestBody(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := step.makeValidate(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step.TStep, nil
|
||||
}
|
||||
|
||||
type tStep struct {
|
||||
hrp.TStep
|
||||
profile map[string]interface{}
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestMethod(entry *Entry) error {
|
||||
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestURL(entry *Entry) error {
|
||||
u, err := url.Parse(entry.Request.URL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("make request url failed")
|
||||
return err
|
||||
}
|
||||
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestParams(entry *Entry) error {
|
||||
s.Request.Params = make(map[string]interface{})
|
||||
for _, param := range entry.Request.QueryString {
|
||||
s.Request.Params[param.Name] = param.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestCookies(entry *Entry) error {
|
||||
s.Request.Cookies = make(map[string]string)
|
||||
cookies, ok := s.profile["cookies"]
|
||||
if ok {
|
||||
// use cookies from profile
|
||||
cookies, ok := cookies.(map[string]interface{})
|
||||
if ok {
|
||||
for k, v := range cookies {
|
||||
s.Request.Cookies[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
log.Warn().Interface("cookies", cookies).
|
||||
Msg("cookies from profile is not a map, ignore!")
|
||||
}
|
||||
|
||||
// use cookies from har
|
||||
for _, cookie := range entry.Request.Cookies {
|
||||
s.Request.Cookies[cookie.Name] = cookie.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestHeaders(entry *Entry) error {
|
||||
s.Request.Headers = make(map[string]string)
|
||||
headers, ok := s.profile["headers"]
|
||||
if ok {
|
||||
// use headers from profile
|
||||
cookies, ok := headers.(map[string]interface{})
|
||||
if ok {
|
||||
for k, v := range cookies {
|
||||
s.Request.Headers[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
log.Warn().Interface("headers", headers).
|
||||
Msg("headers from profile is not a map, ignore!")
|
||||
}
|
||||
|
||||
// use headers from har
|
||||
for _, header := range entry.Request.Headers {
|
||||
if strings.EqualFold(header.Name, "cookie") {
|
||||
continue
|
||||
}
|
||||
s.Request.Headers[header.Name] = header.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tStep) makeRequestBody(entry *Entry) error {
|
||||
mimeType := entry.Request.PostData.MimeType
|
||||
if mimeType == "" {
|
||||
// GET/HEAD/DELETE without body
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST/PUT with body
|
||||
if strings.HasPrefix(mimeType, "application/json") {
|
||||
// post json
|
||||
var body interface{}
|
||||
if entry.Request.PostData.Text == "" {
|
||||
body = nil
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("make request body failed")
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.Request.Body = body
|
||||
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
|
||||
// post form
|
||||
var paramsList []string
|
||||
for _, param := range entry.Request.PostData.Params {
|
||||
paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
|
||||
}
|
||||
s.Request.Body = strings.Join(paramsList, "&")
|
||||
} else if strings.HasPrefix(mimeType, "text/plain") {
|
||||
// post raw data
|
||||
s.Request.Body = entry.Request.PostData.Text
|
||||
} else {
|
||||
// TODO
|
||||
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tStep) makeValidate(entry *Entry) error {
|
||||
// make validator for response status code
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: "status_code",
|
||||
Assert: "equals",
|
||||
Expect: entry.Response.Status,
|
||||
Message: "assert response status code",
|
||||
})
|
||||
|
||||
// make validators for response headers
|
||||
for _, header := range entry.Response.Headers {
|
||||
// assert Content-Type
|
||||
if strings.EqualFold(header.Name, "Content-Type") {
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: "headers.\"Content-Type\"",
|
||||
Assert: "equals",
|
||||
Expect: header.Value,
|
||||
Message: "assert response header Content-Type",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// make validators for response body
|
||||
respBody := entry.Response.Content
|
||||
if respBody.Text == "" {
|
||||
// response body is empty
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(respBody.MimeType, "application/json") {
|
||||
var data []byte
|
||||
var err error
|
||||
// response body is json
|
||||
if respBody.Encoding == "base64" {
|
||||
// decode base64 text
|
||||
data, err = base64.StdEncoding.DecodeString(respBody.Text)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "decode base64 error")
|
||||
}
|
||||
} else if respBody.Encoding == "" {
|
||||
// no encoding
|
||||
data = []byte(respBody.Text)
|
||||
} else {
|
||||
// other encoding type
|
||||
return nil
|
||||
}
|
||||
// convert to json
|
||||
var body interface{}
|
||||
if err = json.Unmarshal(data, &body); err != nil {
|
||||
return errors.Wrap(err, "json.Unmarshal body error")
|
||||
}
|
||||
jsonBody, ok := body.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("response body is not json, not matched with MimeType")
|
||||
}
|
||||
|
||||
// response body is json
|
||||
keys := make([]string, 0, len(jsonBody))
|
||||
for k := range jsonBody {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
// sort map keys to keep validators in stable order
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
value := jsonBody[key]
|
||||
switch v := value.(type) {
|
||||
case map[string]interface{}:
|
||||
continue
|
||||
case []interface{}:
|
||||
continue
|
||||
default:
|
||||
s.Validators = append(s.Validators, hrp.Validator{
|
||||
Check: fmt.Sprintf("body.%s", key),
|
||||
Assert: "equals",
|
||||
Expect: v,
|
||||
Message: fmt.Sprintf("assert response body %s", key),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *har) genOutputPath(suffix string) string {
|
||||
file := getFilenameWithoutExtension(h.path) + suffix
|
||||
if h.outputDir != "" {
|
||||
return filepath.Join(h.outputDir, file)
|
||||
} else {
|
||||
return filepath.Join(filepath.Dir(h.path), file)
|
||||
}
|
||||
}
|
||||
|
||||
func getFilenameWithoutExtension(path string) string {
|
||||
base := filepath.Base(path)
|
||||
ext := filepath.Ext(base)
|
||||
return base[0 : len(base)-len(ext)]
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
|
||||
return nil, errors.New("incorrect testcase file format, expected config in file")
|
||||
}
|
||||
|
||||
err = tc.makeCompat()
|
||||
err = tc.MakeCompat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -173,26 +173,27 @@ type TCase struct {
|
||||
TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
|
||||
}
|
||||
|
||||
// makeCompat converts TCase to compatible testcase
|
||||
func (tc *TCase) makeCompat() error {
|
||||
var err error
|
||||
// MakeCompat converts TCase compatible with Golang engine style
|
||||
func (tc *TCase) MakeCompat() (err error) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
err = fmt.Errorf("convert compat testcase error: %v", p)
|
||||
err = fmt.Errorf("[MakeCompat] convert compat testcase error: %v", p)
|
||||
}
|
||||
}()
|
||||
for _, step := range tc.TestSteps {
|
||||
// 1. deal with request body compatible with HttpRunner
|
||||
// 1. deal with request body compatibility
|
||||
if step.Request != nil && step.Request.Body == nil {
|
||||
if step.Request.Json != nil {
|
||||
step.Request.Headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
step.Request.Body = step.Request.Json
|
||||
step.Request.Json = nil
|
||||
} else if step.Request.Data != nil {
|
||||
step.Request.Body = step.Request.Data
|
||||
step.Request.Data = nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. deal with validators compatible with HttpRunner
|
||||
// 2. deal with validators compatibility
|
||||
err = convertCompatValidator(step.Validators)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -206,38 +207,46 @@ func (tc *TCase) makeCompat() error {
|
||||
|
||||
func convertCompatValidator(Validators []interface{}) (err error) {
|
||||
for i, iValidator := range Validators {
|
||||
if _, ok := iValidator.(Validator); ok {
|
||||
continue
|
||||
}
|
||||
validatorMap := iValidator.(map[string]interface{})
|
||||
validator := Validator{}
|
||||
_, checkExisted := validatorMap["check"]
|
||||
_, assertExisted := validatorMap["assert"]
|
||||
_, expectExisted := validatorMap["expect"]
|
||||
// check priority: HRP > HttpRunner
|
||||
iCheck, checkExisted := validatorMap["check"]
|
||||
iAssert, assertExisted := validatorMap["assert"]
|
||||
iExpect, expectExisted := validatorMap["expect"]
|
||||
// validator check priority: Golang > Python engine style
|
||||
if checkExisted && assertExisted && expectExisted {
|
||||
// HRP validator format
|
||||
validator.Check = validatorMap["check"].(string)
|
||||
validator.Assert = validatorMap["assert"].(string)
|
||||
validator.Expect = validatorMap["expect"]
|
||||
if msg, existed := validatorMap["msg"]; existed {
|
||||
validator.Message = msg.(string)
|
||||
// Golang engine style
|
||||
validator.Check = iCheck.(string)
|
||||
validator.Assert = iAssert.(string)
|
||||
validator.Expect = iExpect
|
||||
if iMsg, msgExisted := validatorMap["msg"]; msgExisted {
|
||||
validator.Message = iMsg.(string)
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else if len(validatorMap) == 1 {
|
||||
// HttpRunner validator format
|
||||
continue
|
||||
}
|
||||
if len(validatorMap) == 1 {
|
||||
// Python engine style
|
||||
for assertMethod, iValidatorContent := range validatorMap {
|
||||
checkAndExpect := iValidatorContent.([]interface{})
|
||||
if len(checkAndExpect) != 2 {
|
||||
validatorContent := iValidatorContent.([]interface{})
|
||||
if len(validatorContent) > 3 {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
validator.Check = checkAndExpect[0].(string)
|
||||
validator.Check = validatorContent[0].(string)
|
||||
validator.Assert = assertMethod
|
||||
validator.Expect = checkAndExpect[1]
|
||||
validator.Expect = validatorContent[1]
|
||||
if len(validatorContent) == 3 {
|
||||
validator.Message = validatorContent[2].(string)
|
||||
}
|
||||
}
|
||||
validator.Check = convertCheckExpr(validator.Check)
|
||||
Validators[i] = validator
|
||||
} else {
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unexpected validator format: %v", validatorMap)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
This module handles compatibility issues between testcase format v2 and v3.
|
||||
This module handles compatibility issues between testcase format v2, v3 and v4.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -14,9 +14,8 @@ from httprunner.utils import sort_dict_by_custom_order
|
||||
|
||||
|
||||
def convert_variables(
|
||||
raw_variables: Union[Dict, Text], test_path: Text
|
||||
raw_variables: Union[Dict, Text], test_path: Text
|
||||
) -> Dict[Text, Any]:
|
||||
|
||||
if isinstance(raw_variables, Dict):
|
||||
return raw_variables
|
||||
|
||||
@@ -33,6 +32,18 @@ def convert_variables(
|
||||
)
|
||||
|
||||
|
||||
def _convert_request(request: Dict) -> Dict:
|
||||
if "body" in request:
|
||||
content_type = ""
|
||||
if "headers" in request and "Content-Type" in request["headers"]:
|
||||
content_type = request["headers"]["Content-Type"]
|
||||
if content_type.startswith("application/json"):
|
||||
request["json"] = request.pop("body")
|
||||
else:
|
||||
request["data"] = request.pop("body")
|
||||
return _sort_request_by_custom_order(request)
|
||||
|
||||
|
||||
def _convert_jmespath(raw: Text) -> Text:
|
||||
if not isinstance(raw, Text):
|
||||
raise exceptions.TestCaseFormatError(f"Invalid jmespath extractor: {raw}")
|
||||
@@ -153,6 +164,9 @@ def _ensure_step_attachment(step: Dict) -> Dict:
|
||||
"name": step["name"],
|
||||
}
|
||||
|
||||
if "request" in step:
|
||||
test_dict["request"] = _convert_request(step["request"])
|
||||
|
||||
if "variables" in step:
|
||||
test_dict["variables"] = step["variables"]
|
||||
|
||||
@@ -181,11 +195,11 @@ def _ensure_step_attachment(step: Dict) -> Dict:
|
||||
return test_dict
|
||||
|
||||
|
||||
def ensure_testcase_v3_api(api_content: Dict) -> Dict:
|
||||
logger.info("convert api in v2 to testcase format v3")
|
||||
def ensure_testcase_v4_api(api_content: Dict) -> Dict:
|
||||
logger.info("convert api in v2/v3 to testcase format v4")
|
||||
|
||||
teststep = {
|
||||
"request": _sort_request_by_custom_order(api_content["request"]),
|
||||
"request": _convert_request(api_content["request"]),
|
||||
}
|
||||
teststep.update(_ensure_step_attachment(api_content))
|
||||
|
||||
@@ -202,8 +216,8 @@ def ensure_testcase_v3_api(api_content: Dict) -> Dict:
|
||||
}
|
||||
|
||||
|
||||
def ensure_testcase_v3(test_content: Dict) -> Dict:
|
||||
logger.info("ensure compatibility with testcase format v2")
|
||||
def ensure_testcase_v4(test_content: Dict) -> Dict:
|
||||
logger.info("ensure compatibility with testcase format v2/v3")
|
||||
|
||||
v3_content = {"config": test_content["config"], "teststeps": []}
|
||||
|
||||
@@ -221,7 +235,7 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
|
||||
teststep = {}
|
||||
|
||||
if "request" in step:
|
||||
teststep["request"] = _sort_request_by_custom_order(step.pop("request"))
|
||||
pass
|
||||
elif "api" in step:
|
||||
teststep["testcase"] = step.pop("api")
|
||||
elif "testcase" in step:
|
||||
|
||||
@@ -26,6 +26,53 @@ class TestCompat(unittest.TestCase):
|
||||
with self.assertRaises(exceptions.TestCaseFormatError):
|
||||
compat.convert_variables(None, "examples/data/a-b.c/1.yml")
|
||||
|
||||
def test_convert_request(self):
|
||||
request_with_json_body = {
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"k1": "v1",
|
||||
"k2": "v2"
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
compat._convert_request(request_with_json_body),
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"json": {
|
||||
"k1": "v1",
|
||||
"k2": "v2"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
request_with_text_body = {
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"headers": {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
"body": "have a nice day"
|
||||
}
|
||||
self.assertEqual(
|
||||
compat._convert_request(request_with_text_body),
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "https://postman-echo.com/post",
|
||||
"headers": {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
"data": "have a nice day"
|
||||
}
|
||||
)
|
||||
|
||||
def test_convert_jmespath(self):
|
||||
self.assertEqual(compat._convert_jmespath("content.abc"), "body.abc")
|
||||
self.assertEqual(compat._convert_jmespath("json.abc"), "body.abc")
|
||||
@@ -85,7 +132,7 @@ class TestCompat(unittest.TestCase):
|
||||
[{"eq": ["body[0].name", 201]}],
|
||||
)
|
||||
|
||||
def test_ensure_testcase_v3_api(self):
|
||||
def test_ensure_testcase_v4_api(self):
|
||||
api_content = {
|
||||
"name": "get with params",
|
||||
"request": {
|
||||
@@ -98,7 +145,7 @@ class TestCompat(unittest.TestCase):
|
||||
"validate": [{"eq": ["content.varB", 200]}, {"lt": ["json.0.varC", 0]}],
|
||||
}
|
||||
self.assertEqual(
|
||||
compat.ensure_testcase_v3_api(api_content),
|
||||
compat.ensure_testcase_v4_api(api_content),
|
||||
{
|
||||
"config": {
|
||||
"name": "get with params",
|
||||
@@ -126,7 +173,7 @@ class TestCompat(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_ensure_testcase_v3(self):
|
||||
def test_ensure_testcase_v4(self):
|
||||
testcase_content = {
|
||||
"config": {"name": "xxx", "base_url": "https://httpbin.org"},
|
||||
"teststeps": [
|
||||
@@ -150,7 +197,7 @@ class TestCompat(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
self.assertEqual(
|
||||
compat.ensure_testcase_v3(testcase_content),
|
||||
compat.ensure_testcase_v4(testcase_content),
|
||||
{
|
||||
"config": {"name": "xxx", "base_url": "https://httpbin.org"},
|
||||
"teststeps": [
|
||||
|
||||
@@ -11,8 +11,8 @@ from httprunner import __version__, exceptions
|
||||
from httprunner.compat import (
|
||||
convert_variables,
|
||||
ensure_path_sep,
|
||||
ensure_testcase_v3,
|
||||
ensure_testcase_v3_api,
|
||||
ensure_testcase_v4,
|
||||
ensure_testcase_v4_api,
|
||||
)
|
||||
from httprunner.loader import (
|
||||
convert_relative_project_root_dir,
|
||||
@@ -332,8 +332,8 @@ def make_teststep_chain_style(teststep: Dict) -> Text:
|
||||
|
||||
def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
||||
"""convert valid testcase dict to pytest file path"""
|
||||
# ensure compatibility with testcase format v2
|
||||
testcase = ensure_testcase_v3(testcase)
|
||||
# ensure compatibility with testcase format v2/v3
|
||||
testcase = ensure_testcase_v4(testcase)
|
||||
|
||||
# validate testcase format
|
||||
load_testcase(testcase)
|
||||
@@ -373,9 +373,9 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
||||
if not isinstance(test_content, Dict):
|
||||
raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
|
||||
|
||||
# api in v2 format, convert to v3 testcase
|
||||
# api in v2/v3 format, convert to v4 testcase
|
||||
if "request" in test_content and "name" in test_content:
|
||||
test_content = ensure_testcase_v3_api(test_content)
|
||||
test_content = ensure_testcase_v4_api(test_content)
|
||||
|
||||
test_content.setdefault("config", {})["path"] = ref_testcase_path
|
||||
ref_testcase_python_abs_path = make_testcase(test_content)
|
||||
@@ -473,9 +473,9 @@ def __make(tests_path: Text):
|
||||
)
|
||||
continue
|
||||
|
||||
# api in v2 format, convert to v3 testcase
|
||||
# api in v2/v3 format, convert to v4 testcase
|
||||
if "request" in test_content and "name" in test_content:
|
||||
test_content = ensure_testcase_v3_api(test_content)
|
||||
test_content = ensure_testcase_v4_api(test_content)
|
||||
|
||||
if "config" not in test_content:
|
||||
logger.warning(
|
||||
|
||||
Reference in New Issue
Block a user