feat: support importing from Postman (#179)
* feat: support importing from Postman * add ui validation * fix the nested situation
This commit is contained in:
parent
78f517c2cb
commit
98a8d5d7d7
|
@ -25,6 +25,7 @@ SOFTWARE.
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
@ -50,6 +51,7 @@ func createConvertCommand() (c *cobra.Command) {
|
|||
"The file pattern which try to execute the test cases. Brace expansion is supported, such as: test-suite-{1,2}.yaml")
|
||||
flags.StringVarP(&opt.converter, "converter", "", "",
|
||||
fmt.Sprintf("The converter format, supported: %s", util.Keys(converters)))
|
||||
flags.StringVarP(&opt.source, "source", "", "", "The source format, supported: postman")
|
||||
flags.StringVarP(&opt.target, "target", "t", "", "The target file path")
|
||||
|
||||
_ = c.MarkFlagRequired("pattern")
|
||||
|
@ -60,11 +62,21 @@ func createConvertCommand() (c *cobra.Command) {
|
|||
type convertOption struct {
|
||||
pattern string
|
||||
converter string
|
||||
source string
|
||||
target string
|
||||
}
|
||||
|
||||
func (o *convertOption) preRunE(c *cobra.Command, args []string) (err error) {
|
||||
o.target = util.EmptyThenDefault(o.target, "sample.jmx")
|
||||
switch o.source {
|
||||
case "postman":
|
||||
o.target = util.EmptyThenDefault(o.target, "sample.yaml")
|
||||
o.converter = "raw"
|
||||
case "":
|
||||
o.target = util.EmptyThenDefault(o.target, "sample.jmx")
|
||||
default:
|
||||
err = errors.New("only postman supported")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -74,23 +86,41 @@ func (o *convertOption) runE(c *cobra.Command, args []string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
var output string
|
||||
var suites []testing.TestSuite
|
||||
if suites, err = loader.ListTestSuite(); err == nil {
|
||||
if len(suites) == 0 {
|
||||
err = fmt.Errorf("no suites found")
|
||||
} else {
|
||||
converter := generator.GetTestSuiteConverter(o.converter)
|
||||
if converter == nil {
|
||||
err = fmt.Errorf("no converter found")
|
||||
} else {
|
||||
output, err = converter.Convert(&suites[0])
|
||||
}
|
||||
}
|
||||
var suite *testing.TestSuite
|
||||
if o.source == "" {
|
||||
suite, err = getSuiteFromFile(o.pattern)
|
||||
} else {
|
||||
suite, err = generator.NewPostmanImporter().ConvertFromFile(o.pattern)
|
||||
}
|
||||
|
||||
if output != "" {
|
||||
err = os.WriteFile(o.target, []byte(output), 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
converter := generator.GetTestSuiteConverter(o.converter)
|
||||
if converter == nil {
|
||||
err = fmt.Errorf("no converter found")
|
||||
} else {
|
||||
var output string
|
||||
output, err = converter.Convert(suite)
|
||||
if output != "" {
|
||||
err = os.WriteFile(o.target, []byte(output), 0644)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getSuiteFromFile(pattern string) (suite *testing.TestSuite, err error) {
|
||||
loader := testing.NewFileWriter("")
|
||||
if err = loader.Put(pattern); err == nil {
|
||||
var suites []testing.TestSuite
|
||||
if suites, err = loader.ListTestSuite(); err == nil {
|
||||
if len(suites) > 0 {
|
||||
suite = &suites[0]
|
||||
} else {
|
||||
err = errors.New("no suites found")
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ SOFTWARE.
|
|||
package cmd_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -85,4 +86,19 @@ func TestConvert(t *testing.T) {
|
|||
err := c.Execute()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("not supported source format", func(t *testing.T) {
|
||||
c.SetArgs([]string{"convert", "--source=fake"})
|
||||
err := c.Execute()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("convert from postmant", func(t *testing.T) {
|
||||
tmpFile := path.Join(os.TempDir(), time.Now().String())
|
||||
defer os.RemoveAll(tmpFile)
|
||||
|
||||
c.SetArgs([]string{"convert", "--source=postman", "--target=", tmpFile, "-p=testdata/postman.json"})
|
||||
err := c.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +1,27 @@
|
|||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 API Testing Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "0da1f6bf-fdbb-46a5-ac46-873564e2259c",
|
||||
"name": "New Collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "6795120",
|
||||
"_collection_link": "https://www.postman.com/ks-devops/workspace/kubesphere-devops/collection/6795120-0da1f6bf-fdbb-46a5-ac46-873564e2259c?action=share&creator=6795120&source=collection_link"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "New Request",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "value",
|
||||
"description": "description",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost?key=value"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -137,6 +137,7 @@ function loadStores() {
|
|||
loadStores()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const suiteCreatingLoading = ref(false)
|
||||
const suiteFormRef = ref<FormInstance>()
|
||||
const testSuiteForm = reactive({
|
||||
|
@ -144,20 +145,27 @@ const testSuiteForm = reactive({
|
|||
api: '',
|
||||
store: ''
|
||||
})
|
||||
const importSuiteFormRef = ref<FormInstance>()
|
||||
const importSuiteForm = reactive({
|
||||
url: '',
|
||||
store: ''
|
||||
})
|
||||
|
||||
function openTestSuiteCreateDialog() {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openTestSuiteImportDialog() {
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
|
||||
const rules = reactive<FormRules<Suite>>({
|
||||
name: [{ required: true, message: 'Name is required', trigger: 'blur' }],
|
||||
store: [{ required: true, message: 'Location is required', trigger: 'blur' }]
|
||||
})
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
console.log(formEl)
|
||||
await formEl.validate((valid: boolean, fields) => {
|
||||
console.log(valid, fields)
|
||||
if (valid) {
|
||||
suiteCreatingLoading.value = true
|
||||
|
||||
|
@ -184,6 +192,40 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
})
|
||||
}
|
||||
|
||||
const importSuiteFormRules = reactive<FormRules<Suite>>({
|
||||
url: [
|
||||
{ required: true, message: 'URL is required', trigger: 'blur' },
|
||||
{ type: 'url', message: 'Should be a valid URL value', trigger: 'blur' }
|
||||
],
|
||||
store: [{ required: true, message: 'Location is required', trigger: 'blur' }]
|
||||
})
|
||||
const importSuiteFormSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid: boolean, fields) => {
|
||||
if (valid) {
|
||||
suiteCreatingLoading.value = true
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Store-Name': importSuiteForm.store
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: importSuiteForm.url
|
||||
})
|
||||
}
|
||||
|
||||
fetch('/server.Runner/ImportTestSuite', requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then(() => {
|
||||
loadStores()
|
||||
importDialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const filterText = ref('')
|
||||
watch(filterText, (val) => {
|
||||
treeRef.value!.filter(val)
|
||||
|
@ -209,6 +251,9 @@ const viewName = ref('testcase')
|
|||
<el-button type="primary" @click="openTestSuiteCreateDialog"
|
||||
data-intro="Click here to create a new test suite"
|
||||
test-id="open-new-suite-dialog" :icon="Edit">New</el-button>
|
||||
<el-button type="primary" @click="openTestSuiteImportDialog"
|
||||
data-intro="Click here to import from Postman"
|
||||
test-id="open-import-suite-dialog">Import</el-button>
|
||||
<el-input v-model="filterText" placeholder="Filter keyword" test-id="search" />
|
||||
|
||||
<el-tree
|
||||
|
@ -292,6 +337,44 @@ const viewName = ref('testcase')
|
|||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="importDialogVisible" title="Import Test Suite" width="30%" draggable>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-form
|
||||
:rules="importSuiteFormRules"
|
||||
:model="importSuiteForm"
|
||||
ref="importSuiteFormRef"
|
||||
status-icon label-width="120px">
|
||||
<el-form-item label="Location" prop="store">
|
||||
<el-select v-model="importSuiteForm.store" class="m-2"
|
||||
test-id="suite-import-form-store"
|
||||
filterable=true
|
||||
default-first-option=true
|
||||
placeholder="Storage Location" size="middle">
|
||||
<el-option
|
||||
v-for="item in stores"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="URL" prop="url">
|
||||
<el-input v-model="importSuiteForm.url" test-id="suite-import-form-api" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="importSuiteFormSubmit(importSuiteFormRef)"
|
||||
test-id="suite-import-submit"
|
||||
>Import</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<TemplateFunctions/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 API Testing Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package generator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||
)
|
||||
|
||||
type PostmanCollection struct {
|
||||
Collection Postman `json:"collection"`
|
||||
}
|
||||
|
||||
type Postman struct {
|
||||
Info PostmanInfo `json:"info"`
|
||||
Item []PostmanItem `json:"item"`
|
||||
}
|
||||
|
||||
type PostmanInfo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type PostmanItem struct {
|
||||
Name string `json:"name"`
|
||||
Request PostmanRequest `json:"request"`
|
||||
Item []PostmanItem `json:"item"`
|
||||
}
|
||||
|
||||
type PostmanRequest struct {
|
||||
Method string `json:"method"`
|
||||
URL PostmanURL `json:"url"`
|
||||
Header Paris `json:"header"`
|
||||
Body PostmanBody `json:"body"`
|
||||
}
|
||||
|
||||
type PostmanBody struct {
|
||||
Mode string `json:"mode"`
|
||||
Raw string `json:"raw"`
|
||||
}
|
||||
|
||||
type PostmanURL struct {
|
||||
Raw string `json:"raw"`
|
||||
Path []string `json:"path"`
|
||||
Query Paris `json:"query"`
|
||||
}
|
||||
|
||||
type Paris []Pair
|
||||
type Pair struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (p Paris) ToMap() (result map[string]string) {
|
||||
count := len(p)
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
result = make(map[string]string, count)
|
||||
for _, item := range p {
|
||||
result[item.Key] = item.Value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Importer interface {
|
||||
Convert(data []byte) (*testing.TestSuite, error)
|
||||
ConvertFromFile(dataFile string) (*testing.TestSuite, error)
|
||||
ConvertFromURL(dataURL string) (*testing.TestSuite, error)
|
||||
}
|
||||
|
||||
type postmanImporter struct {
|
||||
}
|
||||
|
||||
// NewPostmanImporter returns a new postman importer
|
||||
func NewPostmanImporter() Importer {
|
||||
return &postmanImporter{}
|
||||
}
|
||||
|
||||
// Convert converts the postman data to test suite
|
||||
func (p *postmanImporter) Convert(data []byte) (suite *testing.TestSuite, err error) {
|
||||
postman := &Postman{}
|
||||
if err = json.Unmarshal(data, postman); err != nil {
|
||||
return
|
||||
}
|
||||
if postman.Info.Name == "" {
|
||||
postmanCollection := &PostmanCollection{}
|
||||
if err = json.Unmarshal(data, postmanCollection); err != nil {
|
||||
return
|
||||
}
|
||||
postman = &postmanCollection.Collection
|
||||
}
|
||||
|
||||
suite = &testing.TestSuite{}
|
||||
suite.Name = postman.Info.Name
|
||||
suite.Items = make([]testing.TestCase, len(postman.Item))
|
||||
|
||||
for i, item := range postman.Item {
|
||||
if len(item.Item) == 0 {
|
||||
suite.Items[i] = testing.TestCase{
|
||||
Name: item.Name,
|
||||
Request: testing.Request{
|
||||
Method: item.Request.Method,
|
||||
API: item.Request.URL.Raw,
|
||||
Body: item.Request.Body.Raw,
|
||||
Header: item.Request.Header.ToMap(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
for _, sub := range item.Item {
|
||||
suite.Items[i] = testing.TestCase{
|
||||
Name: item.Name + " " + sub.Name,
|
||||
Request: testing.Request{
|
||||
Method: sub.Request.Method,
|
||||
API: sub.Request.URL.Raw,
|
||||
Body: sub.Request.Body.Raw,
|
||||
Header: sub.Request.Header.ToMap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *postmanImporter) ConvertFromFile(dataFile string) (suite *testing.TestSuite, err error) {
|
||||
var data []byte
|
||||
if data, err = os.ReadFile(dataFile); err == nil {
|
||||
suite, err = p.Convert(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *postmanImporter) ConvertFromURL(dataURL string) (suite *testing.TestSuite, err error) {
|
||||
var resp *http.Response
|
||||
if resp, err = http.Get(dataURL); err == nil {
|
||||
var data []byte
|
||||
if data, err = io.ReadAll(resp.Body); err == nil {
|
||||
suite, err = p.Convert(data)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 API Testing Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package generator
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/h2non/gock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPostmanImport(t *testing.T) {
|
||||
importer := NewPostmanImporter()
|
||||
|
||||
converter := GetTestSuiteConverter("raw")
|
||||
if !assert.NotNil(t, converter) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
suite, err := importer.Convert([]byte(emptyJSON))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result string
|
||||
result, err = converter.Convert(suite)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, emptyJSON, strings.TrimSpace(result))
|
||||
})
|
||||
|
||||
t.Run("simple postman, from []byte", func(t *testing.T) {
|
||||
suite, err := importer.Convert([]byte(simplePostman))
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result string
|
||||
result, err = converter.Convert(suite)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedSuiteFromPostman, strings.TrimSpace(result), result)
|
||||
})
|
||||
|
||||
t.Run("simple postman, from file", func(t *testing.T) {
|
||||
suite, err := importer.ConvertFromFile("testdata/postman.json")
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result string
|
||||
result, err = converter.Convert(suite)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedSuiteFromPostman, strings.TrimSpace(result), result)
|
||||
})
|
||||
|
||||
t.Run("simple postman, from URl", func(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.New(urlFoo).Get("/").Reply(http.StatusOK).BodyString(simplePostman)
|
||||
|
||||
suite, err := importer.ConvertFromURL(urlFoo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result string
|
||||
result, err = converter.Convert(suite)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedSuiteFromPostman, strings.TrimSpace(result), result)
|
||||
})
|
||||
|
||||
t.Run("nil data", func(t *testing.T) {
|
||||
_, err := importer.Convert(nil)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("pairs toMap", func(t *testing.T) {
|
||||
pairs := Paris{}
|
||||
assert.Equal(t, 0, len(pairs.ToMap()))
|
||||
})
|
||||
}
|
||||
|
||||
const emptyJSON = "{}"
|
||||
const urlFoo = "http://foo"
|
||||
|
||||
//go:embed testdata/postman.json
|
||||
var simplePostman string
|
||||
|
||||
//go:embed testdata/expected_suite_from_postman.yaml
|
||||
var expectedSuiteFromPostman string
|
|
@ -0,0 +1,9 @@
|
|||
name: New Collection
|
||||
items:
|
||||
- name: New Request
|
||||
request:
|
||||
api: http://localhost?key=value
|
||||
method: GET
|
||||
header:
|
||||
key: value
|
||||
body: '{}'
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "0da1f6bf-fdbb-46a5-ac46-873564e2259c",
|
||||
"name": "New Collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "6795120",
|
||||
"_collection_link": "https://www.postman.com/ks-devops/workspace/kubesphere-devops/collection/6795120-0da1f6bf-fdbb-46a5-ac46-873564e2259c?action=share&creator=6795120&source=collection_link"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "New Request",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "value",
|
||||
"description": "description",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost?key=value"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,3 +1,27 @@
|
|||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 API Testing Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
// Package server provides a GRPC based server
|
||||
package server
|
||||
|
||||
|
@ -265,6 +289,45 @@ func (s *server) CreateTestSuite(ctx context.Context, in *TestSuiteIdentity) (re
|
|||
return
|
||||
}
|
||||
|
||||
func (s *server) ImportTestSuite(ctx context.Context, in *TestSuiteSource) (result *CommonResult, err error) {
|
||||
result = &CommonResult{}
|
||||
if in.Kind != "postman" && in.Kind != "" {
|
||||
result.Success = false
|
||||
result.Message = fmt.Sprintf("not support kind: %s", in.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
var suite *testing.TestSuite
|
||||
importer := generator.NewPostmanImporter()
|
||||
if in.Url != "" {
|
||||
suite, err = importer.ConvertFromURL(in.Url)
|
||||
} else if in.Data != "" {
|
||||
suite, err = importer.Convert([]byte(in.Data))
|
||||
} else {
|
||||
err = errors.New("url or data is required")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Message = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
loader := s.getLoader(ctx)
|
||||
|
||||
if err = loader.CreateSuite(suite.Name, suite.API); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range suite.Items {
|
||||
if err = loader.CreateTestCase(suite.Name, item); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
result.Success = true
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) GetTestSuite(ctx context.Context, in *TestSuiteIdentity) (result *TestSuite, err error) {
|
||||
loader := s.getLoader(ctx)
|
||||
var suite *testing.TestSuite
|
||||
|
|
|
@ -1,3 +1,27 @@
|
|||
/**
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 API Testing Authors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
|
@ -626,6 +650,42 @@ func TestCodeGenerator(t *testing.T) {
|
|||
assert.True(t, reply.Success)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ImportTestSuite, url or data is required", func(t *testing.T) {
|
||||
result, err := server.ImportTestSuite(ctx, &TestSuiteSource{})
|
||||
assert.Error(t, err)
|
||||
assert.False(t, result.Success)
|
||||
assert.Equal(t, "url or data is required", result.Message)
|
||||
})
|
||||
|
||||
t.Run("ImportTestSuite, invalid kind", func(t *testing.T) {
|
||||
result, err := server.ImportTestSuite(ctx, &TestSuiteSource{Kind: "fake"})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result.Success)
|
||||
assert.Equal(t, "not support kind: fake", result.Message)
|
||||
})
|
||||
|
||||
t.Run("ImportTestSuite, import from string", func(t *testing.T) {
|
||||
result, err := server.ImportTestSuite(ctx, &TestSuiteSource{
|
||||
Kind: "postman",
|
||||
Data: simplePostman,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Success)
|
||||
})
|
||||
|
||||
t.Run("ImportTestSuite, import from URL", func(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.New(urlFoo).Get("/").Reply(http.StatusOK).BodyString(simplePostman)
|
||||
|
||||
// already exist
|
||||
result, err := server.ImportTestSuite(ctx, &TestSuiteSource{
|
||||
Kind: "postman",
|
||||
Url: urlFoo,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.False(t, result.Success)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFunctionsQueryStream(t *testing.T) {
|
||||
|
@ -764,6 +824,9 @@ var simpleSuite string
|
|||
//go:embed testdata/simple_testcase.yaml
|
||||
var simpleTestCase string
|
||||
|
||||
//go:embed testdata/postman.json
|
||||
var simplePostman string
|
||||
|
||||
const urlFoo = "http://foo"
|
||||
|
||||
type fakeServerStream struct {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -133,6 +133,40 @@ func local_request_Runner_CreateTestSuite_0(ctx context.Context, marshaler runti
|
|||
|
||||
}
|
||||
|
||||
func request_Runner_ImportTestSuite_0(ctx context.Context, marshaler runtime.Marshaler, client RunnerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq TestSuiteSource
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.ImportTestSuite(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Runner_ImportTestSuite_0(ctx context.Context, marshaler runtime.Marshaler, server RunnerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq TestSuiteSource
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
newReader, berr := utilities.IOReaderFactory(req.Body)
|
||||
if berr != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
|
||||
}
|
||||
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.ImportTestSuite(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Runner_GetTestSuite_0(ctx context.Context, marshaler runtime.Marshaler, client RunnerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq TestSuiteIdentity
|
||||
var metadata runtime.ServerMetadata
|
||||
|
@ -1209,6 +1243,31 @@ func RegisterRunnerHandlerServer(ctx context.Context, mux *runtime.ServeMux, ser
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Runner_ImportTestSuite_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/server.Runner/ImportTestSuite", runtime.WithHTTPPathPattern("/server.Runner/ImportTestSuite"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Runner_ImportTestSuite_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Runner_ImportTestSuite_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Runner_GetTestSuite_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -2023,6 +2082,28 @@ func RegisterRunnerHandlerClient(ctx context.Context, mux *runtime.ServeMux, cli
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Runner_ImportTestSuite_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
var err error
|
||||
var annotatedContext context.Context
|
||||
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/server.Runner/ImportTestSuite", runtime.WithHTTPPathPattern("/server.Runner/ImportTestSuite"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Runner_ImportTestSuite_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Runner_ImportTestSuite_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("POST", pattern_Runner_GetTestSuite_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -2671,6 +2752,8 @@ var (
|
|||
|
||||
pattern_Runner_CreateTestSuite_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"server.Runner", "CreateTestSuite"}, ""))
|
||||
|
||||
pattern_Runner_ImportTestSuite_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"server.Runner", "ImportTestSuite"}, ""))
|
||||
|
||||
pattern_Runner_GetTestSuite_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"server.Runner", "GetTestSuite"}, ""))
|
||||
|
||||
pattern_Runner_UpdateTestSuite_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"server.Runner", "UpdateTestSuite"}, ""))
|
||||
|
@ -2737,6 +2820,8 @@ var (
|
|||
|
||||
forward_Runner_CreateTestSuite_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Runner_ImportTestSuite_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Runner_GetTestSuite_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Runner_UpdateTestSuite_0 = runtime.ForwardResponseMessage
|
||||
|
|
|
@ -10,6 +10,7 @@ service Runner {
|
|||
|
||||
rpc GetSuites(Empty) returns (Suites) {}
|
||||
rpc CreateTestSuite(TestSuiteIdentity) returns (HelloReply) {}
|
||||
rpc ImportTestSuite(TestSuiteSource) returns (CommonResult) {}
|
||||
rpc GetTestSuite(TestSuiteIdentity) returns (TestSuite) {}
|
||||
rpc UpdateTestSuite(TestSuite) returns (HelloReply) {}
|
||||
rpc DeleteTestSuite(TestSuiteIdentity) returns (HelloReply) {}
|
||||
|
@ -66,6 +67,12 @@ message TestCaseIdentity {
|
|||
string testcase = 2;
|
||||
}
|
||||
|
||||
message TestSuiteSource {
|
||||
string kind = 1;
|
||||
string url = 2;
|
||||
string data = 3;
|
||||
}
|
||||
|
||||
message TestSuite {
|
||||
string name = 1;
|
||||
string api = 2;
|
||||
|
|
|
@ -26,6 +26,7 @@ type RunnerClient interface {
|
|||
Run(ctx context.Context, in *TestTask, opts ...grpc.CallOption) (*TestResult, error)
|
||||
GetSuites(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Suites, error)
|
||||
CreateTestSuite(ctx context.Context, in *TestSuiteIdentity, opts ...grpc.CallOption) (*HelloReply, error)
|
||||
ImportTestSuite(ctx context.Context, in *TestSuiteSource, opts ...grpc.CallOption) (*CommonResult, error)
|
||||
GetTestSuite(ctx context.Context, in *TestSuiteIdentity, opts ...grpc.CallOption) (*TestSuite, error)
|
||||
UpdateTestSuite(ctx context.Context, in *TestSuite, opts ...grpc.CallOption) (*HelloReply, error)
|
||||
DeleteTestSuite(ctx context.Context, in *TestSuiteIdentity, opts ...grpc.CallOption) (*HelloReply, error)
|
||||
|
@ -98,6 +99,15 @@ func (c *runnerClient) CreateTestSuite(ctx context.Context, in *TestSuiteIdentit
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *runnerClient) ImportTestSuite(ctx context.Context, in *TestSuiteSource, opts ...grpc.CallOption) (*CommonResult, error) {
|
||||
out := new(CommonResult)
|
||||
err := c.cc.Invoke(ctx, "/server.Runner/ImportTestSuite", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *runnerClient) GetTestSuite(ctx context.Context, in *TestSuiteIdentity, opts ...grpc.CallOption) (*TestSuite, error) {
|
||||
out := new(TestSuite)
|
||||
err := c.cc.Invoke(ctx, "/server.Runner/GetTestSuite", in, out, opts...)
|
||||
|
@ -389,6 +399,7 @@ type RunnerServer interface {
|
|||
Run(context.Context, *TestTask) (*TestResult, error)
|
||||
GetSuites(context.Context, *Empty) (*Suites, error)
|
||||
CreateTestSuite(context.Context, *TestSuiteIdentity) (*HelloReply, error)
|
||||
ImportTestSuite(context.Context, *TestSuiteSource) (*CommonResult, error)
|
||||
GetTestSuite(context.Context, *TestSuiteIdentity) (*TestSuite, error)
|
||||
UpdateTestSuite(context.Context, *TestSuite) (*HelloReply, error)
|
||||
DeleteTestSuite(context.Context, *TestSuiteIdentity) (*HelloReply, error)
|
||||
|
@ -440,6 +451,9 @@ func (UnimplementedRunnerServer) GetSuites(context.Context, *Empty) (*Suites, er
|
|||
func (UnimplementedRunnerServer) CreateTestSuite(context.Context, *TestSuiteIdentity) (*HelloReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateTestSuite not implemented")
|
||||
}
|
||||
func (UnimplementedRunnerServer) ImportTestSuite(context.Context, *TestSuiteSource) (*CommonResult, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ImportTestSuite not implemented")
|
||||
}
|
||||
func (UnimplementedRunnerServer) GetTestSuite(context.Context, *TestSuiteIdentity) (*TestSuite, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetTestSuite not implemented")
|
||||
}
|
||||
|
@ -594,6 +608,24 @@ func _Runner_CreateTestSuite_Handler(srv interface{}, ctx context.Context, dec f
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Runner_ImportTestSuite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(TestSuiteSource)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RunnerServer).ImportTestSuite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/server.Runner/ImportTestSuite",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RunnerServer).ImportTestSuite(ctx, req.(*TestSuiteSource))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Runner_GetTestSuite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(TestSuiteIdentity)
|
||||
if err := dec(in); err != nil {
|
||||
|
@ -1143,6 +1175,10 @@ var Runner_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "CreateTestSuite",
|
||||
Handler: _Runner_CreateTestSuite_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ImportTestSuite",
|
||||
Handler: _Runner_ImportTestSuite_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetTestSuite",
|
||||
Handler: _Runner_GetTestSuite_Handler,
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "0da1f6bf-fdbb-46a5-ac46-873564e2259c",
|
||||
"name": "New Collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"_exporter_id": "6795120",
|
||||
"_collection_link": "https://www.postman.com/ks-devops/workspace/kubesphere-devops/collection/6795120-0da1f6bf-fdbb-46a5-ac46-873564e2259c?action=share&creator=6795120&source=collection_link"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "New Request",
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "value",
|
||||
"description": "description",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost?key=value"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue