Compare commits
199 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
4a26a8c711 | |
![]() |
6c9ab38114 | |
![]() |
9d3e7ad6d4 | |
![]() |
5f80de803c | |
![]() |
355390b68a | |
![]() |
d96e2b5485 | |
![]() |
13fe6ee1ce | |
![]() |
185fb0de7e | |
![]() |
2558d5dee3 | |
![]() |
34eeb2a253 | |
![]() |
05d9194b43 | |
![]() |
6c2cb6726f | |
![]() |
4b42dadc50 | |
![]() |
9252000e37 | |
![]() |
739e356f5d | |
![]() |
c4eca6cbc1 | |
![]() |
12a0db899e | |
![]() |
47cbd75f95 | |
![]() |
41c9b16681 | |
![]() |
9c243c29eb | |
![]() |
8a1e72f23d | |
![]() |
dd08c128ca | |
![]() |
7b8a5bbce2 | |
![]() |
66b2f1eade | |
![]() |
11ab39f11f | |
![]() |
2f409ca68d | |
![]() |
5372c556df | |
![]() |
1372e6d21d | |
![]() |
6908433f7e | |
![]() |
c8df84cbeb | |
![]() |
cc5a463b4d | |
![]() |
4a38bd2188 | |
![]() |
4b4ee3a60c | |
![]() |
430d9127c6 | |
![]() |
accdb0e4ef | |
![]() |
664451e6ee | |
![]() |
768b79f0c3 | |
![]() |
4f7f5a085a | |
![]() |
c827476bc9 | |
![]() |
7b25fa373a | |
![]() |
73d71a3321 | |
![]() |
87fe3ee61a | |
![]() |
efb2235754 | |
![]() |
b0d18fcdf8 | |
![]() |
7fe64b67cb | |
![]() |
6d754cf8bc | |
![]() |
4780d26111 | |
![]() |
ad9b4ba14c | |
![]() |
66008a7b1e | |
![]() |
0720cef5ea | |
![]() |
8faaf2b275 | |
![]() |
66f724ae78 | |
![]() |
072cd42c16 | |
![]() |
0f0298455e | |
![]() |
33703da27b | |
![]() |
9e4259f09f | |
![]() |
19871a5d89 | |
![]() |
6f4aef81e0 | |
![]() |
28a32a03ca | |
![]() |
2db16248ef | |
![]() |
bbaf91f73c | |
![]() |
7d9a97f46f | |
![]() |
908fbfeb05 | |
![]() |
72c53c21df | |
![]() |
dcdac9e032 | |
![]() |
7de67ca381 | |
![]() |
fe9d168713 | |
![]() |
af36027460 | |
![]() |
3a9bde8c06 | |
![]() |
eff529225b | |
![]() |
3620c5475a | |
![]() |
834a28fc67 | |
![]() |
344ed1bb62 | |
![]() |
296fd4084f | |
![]() |
71e1aafb1b | |
![]() |
95adf09eaf | |
![]() |
4aadd0611c | |
![]() |
96d3b4452c | |
![]() |
e68c9961b1 | |
![]() |
d00314334b | |
![]() |
b6188a0e68 | |
![]() |
0998b2f4b6 | |
![]() |
f295eb58b1 | |
![]() |
f63520e765 | |
![]() |
ad1521bbd1 | |
![]() |
b880911da6 | |
![]() |
d108b0fd85 | |
![]() |
6e0c70e07d | |
![]() |
62383ba78b | |
![]() |
b7f05034d1 | |
![]() |
1d5f54f0b9 | |
![]() |
6ce9363d5a | |
![]() |
33c89be15c | |
![]() |
a8c52fa891 | |
![]() |
2b98b95017 | |
![]() |
491ebdfe01 | |
![]() |
ea4c0d4dbf | |
![]() |
03a614ad23 | |
![]() |
3aceced901 | |
![]() |
2e50d1c4ea | |
![]() |
d9724e8d2d | |
![]() |
b7926da2c0 | |
![]() |
896197110f | |
![]() |
157253e9c3 | |
![]() |
6495a50f51 | |
![]() |
dabb95542d | |
![]() |
f10f441a53 | |
![]() |
ee312cd07f | |
![]() |
7af3cb2007 | |
![]() |
605cf9addc | |
![]() |
7df7d75b58 | |
![]() |
2ba9c11413 | |
![]() |
666d4bbb8a | |
![]() |
c8eece849d | |
![]() |
a5d8eabd4d | |
![]() |
2b332eb25a | |
![]() |
f38d299bd9 | |
![]() |
d6162c7f2c | |
![]() |
4a7f92c824 | |
![]() |
e866727d53 | |
![]() |
2e0779dfdc | |
![]() |
dd90d1e90e | |
![]() |
c415efadbe | |
![]() |
485dffc8ea | |
![]() |
0cc0bb8be1 | |
![]() |
1735b8f326 | |
![]() |
c0d9a8af62 | |
![]() |
a11b7ff550 | |
![]() |
b9533ee974 | |
![]() |
1a720c03f8 | |
![]() |
92d517f7b9 | |
![]() |
1765d139c0 | |
![]() |
775aab1d30 | |
![]() |
00e53452fc | |
![]() |
e7620a4a0f | |
![]() |
ce5ad55216 | |
![]() |
98b4dae698 | |
![]() |
e10bc5988c | |
![]() |
811047ada1 | |
![]() |
ba485ce922 | |
![]() |
38a309f9f2 | |
![]() |
b40eea5afa | |
![]() |
1dd9dd449a | |
![]() |
966a635768 | |
![]() |
50798ffcb5 | |
![]() |
8b2c8f289c | |
![]() |
0d481b31ab | |
![]() |
a503d757a6 | |
![]() |
3f7749ae66 | |
![]() |
344694031b | |
![]() |
ce31b7145a | |
![]() |
95205fe4e5 | |
![]() |
d31a4c5dc9 | |
![]() |
f7fdbace6d | |
![]() |
ea033d94b6 | |
![]() |
76c60b3e06 | |
![]() |
7896523e66 | |
![]() |
595b82620b | |
![]() |
c2fcf50f2d | |
![]() |
9f400bbcb9 | |
![]() |
369a8d28a4 | |
![]() |
1f0b7ca5e9 | |
![]() |
f41f5a9e14 | |
![]() |
fc085fe337 | |
![]() |
d6485c8603 | |
![]() |
6bf626d72d | |
![]() |
d344a14502 | |
![]() |
02026b2199 | |
![]() |
ffac34fd9d | |
![]() |
72fc008ea1 | |
![]() |
42c4eb259b | |
![]() |
d95d2ffd73 | |
![]() |
2d3c1737b7 | |
![]() |
c8378aee85 | |
![]() |
6f2fed9519 | |
![]() |
eb1973a486 | |
![]() |
e9974374b1 | |
![]() |
02dd80fee0 | |
![]() |
9156fb85a4 | |
![]() |
f4f1d4c312 | |
![]() |
1943e7d82e | |
![]() |
ea911ae8c2 | |
![]() |
ab38bef403 | |
![]() |
de36cc490d | |
![]() |
ac992a8d5a | |
![]() |
062f96f965 | |
![]() |
c17b3e7c89 | |
![]() |
7ef28e3127 | |
![]() |
5ce297db68 | |
![]() |
2bd1433b70 | |
![]() |
6159ae3739 | |
![]() |
26599052af | |
![]() |
887c35ff81 | |
![]() |
e53a8c5892 | |
![]() |
9ce40e3370 | |
![]() |
e05ff4b485 | |
![]() |
77526e2170 | |
![]() |
cc662bdd17 | |
![]() |
4322123586 |
|
@ -1,6 +1,8 @@
|
|||
console/atest-ui/node_modules
|
||||
console/atest-ui/dist
|
||||
.git/
|
||||
bin/
|
||||
extensions/
|
||||
dist/
|
||||
.vscode/
|
||||
console/atest-desktop/
|
||||
console/atest-ui/node_modules/
|
||||
docs/site/node_modules/
|
||||
docs/site/public/
|
||||
docs/site/resources/
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
# 4 space indentation
|
||||
[*.{py,proto,go,js,ts,json,vue}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
|
@ -0,0 +1,9 @@
|
|||
*.go text eol=lf
|
||||
*.vue text eol=lf
|
||||
*.js text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.json text eol=lf
|
||||
*.yml text eol=lf
|
|
@ -0,0 +1,7 @@
|
|||
# see also https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
# pkg/runner/grpc.go @Ink-33
|
||||
# pkg/runner/grpc_test.go @Ink-33
|
||||
# pkg/runner/verify.go @Ink-33
|
||||
|
||||
tools/ @yuluo-yx
|
||||
docs/site @yuluo-yx
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "Crash bug"
|
||||
url: https://github.com/LinuxSuRen/api-testing/SECURITY.md
|
||||
about: "Please file any crash bug with api-testing-security@googlegroups.com."
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement,triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
*Description*:
|
||||
>Describe the desired behavior, what scenario it enables and how it
|
||||
would be used.
|
||||
|
||||
[optional *Relevant Links*:]
|
||||
>Any extra documentation required to understand the issue.
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Bug
|
||||
about: Bugs
|
||||
title: ''
|
||||
labels: bug,triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
*Description*:
|
||||
>What issue is being seen? Describe what should be happening instead of
|
||||
the bug, for example: api-testing should not crash, the expected value isn't
|
||||
returned, etc.
|
||||
|
||||
*Repro steps*:
|
||||
> Include sample requests, environment, etc. All data and inputs
|
||||
required to reproduce the bug.
|
||||
|
||||
>**Note**: If there are privacy concerns, sanitize the data prior to
|
||||
sharing.
|
||||
|
||||
*Environment*:
|
||||
>Include the environment like api-testing version, os version and so on.
|
||||
|
||||
*Logs*:
|
||||
>Include the access logs and the api-testing logs.
|
||||
>
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: Other
|
||||
about: Questions, design proposals, tech debt, etc.
|
||||
title: ''
|
||||
labels: triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
*Description*:
|
||||
>Describe the issue.
|
||||
|
||||
[optional *Relevant Links*:]
|
||||
>Any extra documentation required to understand the issue.
|
|
@ -0,0 +1,29 @@
|
|||
<!--
|
||||
Make sure you run the `make test` command before committing the pr.
|
||||
Making sure that your local build is OK before committing will help you reduce debugging time
|
||||
and make it easier for maintainers to review.
|
||||
-->
|
||||
|
||||
> We highly recommend you read [the contributor's documentation](https://github.com/LinuxSuRen/api-testing/blob/master/CONTRIBUTING.md) before starting the review process especially since this is your first contribution to this project.
|
||||
>
|
||||
> It was updated on 2024/5/27
|
||||
|
||||
**What type of PR is this?**
|
||||
<!--
|
||||
Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary
|
||||
and summary followed by a colon. format `chore/docs/feat/fix/refactor/style/test: summary`.
|
||||
Examples:
|
||||
* "docs: fix grammar error"
|
||||
* "feat(translator): add new feature"
|
||||
* "fix: fix xx bug"
|
||||
* "chore: change ci & build tools etc"
|
||||
-->
|
||||
|
||||
**What this PR does / why we need it**:
|
||||
|
||||
**Which issue(s) this PR fixes**:
|
||||
<!--
|
||||
*Automatically closes linked issue when PR is merged.
|
||||
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
|
||||
-->
|
||||
Fixes #
|
|
@ -0,0 +1,15 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/console/atest-ui" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"MD001": true,
|
||||
"MD002": false,
|
||||
"MD003": false,
|
||||
"MD004": false,
|
||||
"MD005": false,
|
||||
"MD006": false,
|
||||
"MD007": false,
|
||||
"MD008": false,
|
||||
"MD009": false,
|
||||
"MD010": false,
|
||||
"MD011": false,
|
||||
"MD012": false,
|
||||
"MD013": false,
|
||||
"MD014": false,
|
||||
"MD015": false,
|
||||
"MD016": false,
|
||||
"MD017": false,
|
||||
"MD018": false,
|
||||
"MD019": false,
|
||||
"MD020": false,
|
||||
"MD021": false,
|
||||
"MD022": false,
|
||||
"MD023": false,
|
||||
"MD024": false,
|
||||
"MD025": false,
|
||||
"MD026": false,
|
||||
"MD027": false,
|
||||
"MD028": false,
|
||||
"MD029": false,
|
||||
"MD030": false,
|
||||
"MD031": true,
|
||||
"MD032": false,
|
||||
"MD033": false,
|
||||
"MD034": false,
|
||||
"MD035": false,
|
||||
"MD036": false,
|
||||
"MD037": true,
|
||||
"MD038": true,
|
||||
"MD039": false,
|
||||
"MD040": false,
|
||||
"MD041": false,
|
||||
"MD042": false,
|
||||
"MD043": false,
|
||||
"MD044": false,
|
||||
"MD045": false,
|
||||
"MD046": false,
|
||||
"MD047": false,
|
||||
"MD048": false,
|
||||
"MD049": false,
|
||||
"MD050": false,
|
||||
"MD051": false
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
make fmt test-all
|
||||
make fmt test
|
||||
|
|
|
@ -52,7 +52,7 @@ change-template: '* $TITLE (#$NUMBER) @$AUTHOR'
|
|||
template: |
|
||||
## What’s Changed
|
||||
|
||||
[中文 ChangeLog](https://linuxsuren.github.io/api-testing/release-note-v$NEXT_PATCH_VERSION)
|
||||
[中文 ChangeLog](https://linuxsuren.github.io/api-testing/releases/release-note-v$NEXT_PATCH_VERSION)
|
||||
|
||||
$CHANGES
|
||||
|
||||
|
|
|
@ -1,160 +1,14 @@
|
|||
#!api-testing
|
||||
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json
|
||||
# https://docs.gitlab.com/ee/api/api_resources.html
|
||||
name: atest
|
||||
api: |
|
||||
{{default "http://localhost:8080/server.Runner" (env "SERVER")}}
|
||||
{{default "http://localhost:8080/api/v1" (env "SERVER")}}
|
||||
param:
|
||||
name: "{{randAlpha 6}}"
|
||||
items:
|
||||
- name: createSuite
|
||||
request:
|
||||
api: /CreateTestSuite
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{.param.name}}"}
|
||||
- name: suites
|
||||
request:
|
||||
api: /GetSuites
|
||||
method: POST
|
||||
- name: suite
|
||||
request:
|
||||
api: /GetTestSuite
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{.param.name}}"}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
name: "{{.param.name}}"
|
||||
api: ""
|
||||
- name: UpdateTestSuite
|
||||
request:
|
||||
api: /UpdateTestSuite
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"name": "{{.param.name}}",
|
||||
"api": "{{randAlpha 6}}"}
|
||||
}
|
||||
- name: DeleteTestSuiteNotFound
|
||||
request:
|
||||
api: /DeleteTestSuite
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{randAlpha 6}}"}
|
||||
expect:
|
||||
statusCode: 500
|
||||
|
||||
- name: ListTestCase
|
||||
request:
|
||||
api: /ListTestCase
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{.param.name}}"}
|
||||
- name: list-testcases-not-found
|
||||
request:
|
||||
api: /ListTestCase
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{randAlpha 6}}"}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
name: ""
|
||||
- name: GetSuggestedAPIs-no-testsuite-found
|
||||
request:
|
||||
api: /GetSuggestedAPIs
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{randAlpha 6}}"}
|
||||
expect:
|
||||
verify:
|
||||
- len(data.data) == 0
|
||||
- name: get-testcase-not-found
|
||||
request:
|
||||
api: /GetTestCase
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "test"}
|
||||
expect:
|
||||
statusCode: 500
|
||||
bodyFieldsExpect:
|
||||
code: 2
|
||||
- name: get-popular-headers
|
||||
request:
|
||||
api: /PopularHeaders
|
||||
method: POST
|
||||
|
||||
- name: list-code-generators
|
||||
request:
|
||||
api: /ListCodeGenerator
|
||||
method: POST
|
||||
expect:
|
||||
verify:
|
||||
- len(data) == 1
|
||||
- name: GenerateCode
|
||||
request:
|
||||
api: /GenerateCode
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"TestSuite": "{{.param.name}}",
|
||||
"TestCase": "{{randAlpha 6}}",
|
||||
"Generator": "golang"
|
||||
}
|
||||
expect:
|
||||
statusCode: 500 # no testcase found
|
||||
verify:
|
||||
- indexOf(data.message, "not found") != -1
|
||||
|
||||
- name: listConverters
|
||||
request:
|
||||
api: /ListConverter
|
||||
method: POST
|
||||
expect:
|
||||
verify:
|
||||
- len(data) == 1
|
||||
- name: ConvertTestSuite
|
||||
request:
|
||||
api: /ConvertTestSuite
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"TestSuite": "{{.param.name}}",
|
||||
"Generator": "jmeter"
|
||||
}
|
||||
expect:
|
||||
verify:
|
||||
- data.message != ""
|
||||
- indexOf(data.message, "jmeterTestPlan") != -1
|
||||
|
||||
- name: list-stores
|
||||
request:
|
||||
api: /GetStores
|
||||
method: POST
|
||||
expect:
|
||||
verify:
|
||||
- len(data) >= 1
|
||||
- name: query-funcs
|
||||
request:
|
||||
api: /FunctionsQuery
|
||||
method: POST
|
||||
expect:
|
||||
verify:
|
||||
- len(data) == 1
|
||||
- name: version
|
||||
request:
|
||||
api: /GetVersion
|
||||
method: POST
|
||||
- name: GetSecrets
|
||||
request:
|
||||
api: /GetSecrets
|
||||
method: POST
|
||||
expect:
|
||||
statusCode: 500
|
||||
|
||||
- name: DeleteTestSuite
|
||||
request:
|
||||
api: /DeleteTestSuite
|
||||
api: /suites
|
||||
method: POST
|
||||
body: |
|
||||
{"name": "{{.param.name}}"}
|
||||
|
|
|
@ -8,6 +8,7 @@ spec:
|
|||
rpc:
|
||||
import:
|
||||
- ./pkg/server
|
||||
- ./pkg/apispec/data/proto
|
||||
protofile: server.proto
|
||||
items:
|
||||
- name: GetVersion
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
#!api-testing
|
||||
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json
|
||||
# see also https://github.com/LinuxSuRen/api-testing
|
||||
name: grpc-sample-set
|
||||
api: 127.0.0.1:7070
|
||||
spec:
|
||||
kind: grpc
|
||||
rpc:
|
||||
protoset: .github/testing/server.pb
|
||||
items:
|
||||
- name: GetVersion
|
||||
request:
|
||||
api: /server.Runner/GetVersion
|
||||
- name: FunctionsQuery
|
||||
request:
|
||||
api: /server.Runner/FunctionsQuery
|
||||
body: |
|
||||
{
|
||||
"name": "hello"
|
||||
}
|
||||
expect:
|
||||
body: |
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"key": "hello",
|
||||
"value": "func() string"
|
||||
}
|
||||
]
|
||||
}
|
||||
- name: FunctionsQueryStream
|
||||
request:
|
||||
api: /server.Runner/FunctionsQueryStream
|
||||
body: |
|
||||
[
|
||||
{
|
||||
"name": "hello"
|
||||
},
|
||||
{
|
||||
"name": "title"
|
||||
}
|
||||
]
|
||||
expect:
|
||||
verify:
|
||||
- "len(data) == 2"
|
|
@ -6,6 +6,8 @@ api: 127.0.0.1:7070
|
|||
spec:
|
||||
kind: grpc
|
||||
rpc:
|
||||
import:
|
||||
- ./pkg/apispec/data/proto
|
||||
serverReflection: true
|
||||
items:
|
||||
- name: GetVersion
|
||||
|
|
Binary file not shown.
|
@ -5,54 +5,53 @@ on:
|
|||
env:
|
||||
IMG_TOOL: docker
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Test:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Unit Test
|
||||
run: |
|
||||
make test-all-backend
|
||||
make test build-ui test-ui
|
||||
- name: Long Test
|
||||
run: |
|
||||
make testlong
|
||||
- name: Lint Helm
|
||||
run: |
|
||||
make helm-package helm-lint
|
||||
make helm-pkg helm-lint
|
||||
- name: Report
|
||||
if: github.actor == 'linuxsuren'
|
||||
env:
|
||||
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
run: |
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) report --partial --force-coverage-parser go -r coverage.out
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) report --partial console/atest-ui/coverage/clover.xml
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) final
|
||||
|
||||
APITest:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: API Test
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TOOLEXEC= make build copy
|
||||
make build copy
|
||||
sudo atest service install
|
||||
sudo atest service restart
|
||||
sudo atest service status
|
||||
|
||||
# make test-ui-e2e
|
||||
|
||||
atest run -p '.github/testing/*.yaml' --request-ignore-error --report github --report-file bin/report.json --report-github-repo linuxsuren/api-testing --report-github-pr ${{ github.event.number }}
|
||||
atest run -p .github/testing/grpc.yaml --request-ignore-error --report github --report-file bin/report.json --report-github-repo linuxsuren/api-testing --report-github-pr ${{ github.event.number }}
|
||||
sudo atest service status
|
||||
sudo atest service stop
|
||||
sudo atest service uninstall
|
||||
|
@ -64,34 +63,23 @@ jobs:
|
|||
testFilePath: sample.jmx
|
||||
|
||||
Build:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
with:
|
||||
version: latest
|
||||
args: release --skip-publish --rm-dist --snapshot
|
||||
# - name: Operator
|
||||
# run: cd operator && make build
|
||||
version: '~> v2'
|
||||
args: release --clean --snapshot
|
||||
|
||||
BuildImage:
|
||||
runs-on: ubuntu-20.04
|
||||
E2E:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Set output
|
||||
id: vars
|
||||
run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT
|
||||
|
@ -104,9 +92,9 @@ jobs:
|
|||
unset APP_VERSION
|
||||
unset HELM_VERSION
|
||||
fi
|
||||
make helm-package
|
||||
make helm-pkg
|
||||
- name: Core Image
|
||||
run: GOPROXY=direct IMG_TOOL=docker make build-image
|
||||
run: GOPROXY=direct IMG_TOOL=docker TAG=master REGISTRY=ghcr.io make image
|
||||
- name: Run e2e
|
||||
env:
|
||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||
|
@ -116,17 +104,14 @@ jobs:
|
|||
sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
|
||||
sudo chmod u+x /usr/local/bin/docker-compose
|
||||
make test-e2e
|
||||
# - name: Operator Image
|
||||
# run: cd operator && make docker-build
|
||||
- name: Code Generator Test
|
||||
run: cd e2e/code-generator && ./start.sh
|
||||
|
||||
BuildEmbedUI:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -135,8 +120,39 @@ jobs:
|
|||
cache-dependency-path: console/atest-ui/package-lock.json
|
||||
- name: Build
|
||||
run: |
|
||||
TOOLEXEC= make build-embed-ui copy
|
||||
make build-embed-ui copy
|
||||
sudo atest service install
|
||||
sudo atest service restart
|
||||
- name: Test
|
||||
run: make test-ui
|
||||
|
||||
BuildDesktop:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
# for fixing Error: Cannot find module 'appdmg'
|
||||
- name: Install Python 3.11.4
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11.4'
|
||||
- name: Build Desktop on Windows
|
||||
if: runner.os == 'Windows'
|
||||
env:
|
||||
BINARY: atest.exe
|
||||
run: |
|
||||
make desktop-package desktop-make
|
||||
- name: Build Desktop
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
make desktop-package desktop-make desktop-test
|
||||
- name: Test extension cmd
|
||||
run: |
|
||||
./console/atest-desktop/atest extension git
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
name: Hugo Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "docs/site/**"
|
||||
- "tools/make/docs.mk"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docs-lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
|
||||
- name: Run markdown linter
|
||||
uses: nosborn/github-action-markdown-cli@9b5e871c11cc0649c5ac2526af22e23525fa344d # v3.3.0
|
||||
with:
|
||||
files: docs/site/content/*
|
||||
config_file: ".github/markdown_lint_config.json"
|
||||
|
||||
- name: Install linkinator
|
||||
run: npm install -g linkinator@6.0.4
|
||||
|
||||
- name: Check links
|
||||
run: make docs # docs-check-links
|
||||
|
||||
docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: docs-lint
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Git checkout
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3.0.0
|
||||
with:
|
||||
hugo-version: "latest"
|
||||
extended: true
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.1.0
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Install Site Dependencies and Build Site
|
||||
run: |
|
||||
cp docs/api-testing-schema.json docs/site/static/api-testing-schema.json
|
||||
cp docs/api-testing-mock-schema.json docs/site/static/api-testing-mock-schema.json
|
||||
make docs # docs-check-links
|
||||
|
||||
# Upload docs for GitHub Pages
|
||||
- name: Upload GitHub Pages artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
|
||||
with:
|
||||
# Path of the directory containing the static assets.
|
||||
path: docs/site/public
|
||||
# Duration after which artifact will expire in days.
|
||||
# retention-days: # optional, default is 1
|
||||
|
||||
# This workflow contains a single job called "build"
|
||||
docs-publish:
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
needs: docs-build
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
deployments: write
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
|
|
@ -0,0 +1,33 @@
|
|||
name: Issue and PR comment commands
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
execute:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: jpmcb/prow-github-actions@f4d01dd4b13f289014c23fe5a19878a2479cb35b # v1.1.3
|
||||
with:
|
||||
prow-commands: '/assign
|
||||
/unassign
|
||||
/area
|
||||
/kind
|
||||
/priority
|
||||
/remove
|
||||
/close
|
||||
/reopen
|
||||
/lock
|
||||
/milestone
|
||||
/hold
|
||||
/cc
|
||||
/uncc'
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
@ -1,44 +0,0 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Check broken links
|
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||
# events but only for the master branch
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
# Run everyday at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07)
|
||||
- cron: "0 5 * * *"
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
check:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: markdown-link-check
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: "yes"
|
||||
config-file: "checklink_config.json"
|
||||
max-depth: 3
|
||||
|
||||
- name: Archive Broken Links List
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: broken-links.json
|
||||
path: /brokenLinks.json
|
||||
retention-days: 5
|
|
@ -7,7 +7,7 @@ on:
|
|||
|
||||
jobs:
|
||||
UpdateReleaseDraft:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
|
|
|
@ -11,20 +11,18 @@ env:
|
|||
REGISTRY: ghcr.io
|
||||
REGISTRY_DOCKERHUB: docker.io
|
||||
REGISTRY_ALIYUN: registry.aliyuncs.com
|
||||
REGISTRY_TENCENT: ccr.ccs.tencentyun.com
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.ref != 'refs/heads/master'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Unshallow
|
||||
run: git fetch --prune --unshallow
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -32,34 +30,32 @@ jobs:
|
|||
cache: "npm"
|
||||
cache-dependency-path: console/atest-ui/package-lock.json
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2.9.1
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
|
||||
Test:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Unit Test
|
||||
run: |
|
||||
make test-all-backend
|
||||
make test build-ui test-ui
|
||||
- name: Report
|
||||
if: github.actor == 'linuxsuren'
|
||||
env:
|
||||
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
run: |
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) report --partial --force-coverage-parser go -r coverage.out
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) report --partial console/atest-ui/coverage/clover.xml
|
||||
bash <(curl -Ls https://coverage.codacy.com/get.sh) final
|
||||
|
||||
image:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
@ -78,68 +74,6 @@ jobs:
|
|||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/linuxsuren/api-testing
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: VERSION=${{ steps.vars.outputs.tag }}
|
||||
|
||||
# image-operator:
|
||||
# runs-on: ubuntu-20.04
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# - name: Setup Docker buildx
|
||||
# uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
|
||||
# - name: Log into registry ${{ env.REGISTRY }}
|
||||
# if: github.event_name != 'pull_request'
|
||||
# uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
# with:
|
||||
# registry: ${{ env.REGISTRY }}
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
# - name: Extract Docker metadata
|
||||
# id: meta
|
||||
# uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
# with:
|
||||
# images: ${{ env.REGISTRY }}/linuxsuren/api-testing-operator
|
||||
# - name: Build and push Docker image
|
||||
# id: build-and-push
|
||||
# uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
# with:
|
||||
# context: operator
|
||||
# push: ${{ github.event_name != 'pull_request' }}
|
||||
# tags: ${{ steps.meta.outputs.tags }}
|
||||
# labels: ${{ steps.meta.outputs.labels }}
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
|
||||
image-dockerhub:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
- name: Set output
|
||||
id: vars
|
||||
run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
|
||||
- name: Log into registry ${{ env.REGISTRY_DOCKERHUB }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
|
@ -147,11 +81,29 @@ jobs:
|
|||
registry: ${{ env.REGISTRY_DOCKERHUB }}
|
||||
username: linuxsuren
|
||||
password: ${{ secrets.DOCKER_HUB_PUBLISH_SECRETS }}
|
||||
- name: Log into registry ${{ env.REGISTRY_ALIYUN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_ALIYUN }}
|
||||
username: ${{ secrets.REGISTRY_ALIYUN_USER }}
|
||||
password: ${{ secrets.REGISTRY_ALIYUN_PUBLISH_SECRETS }}
|
||||
- name: Log into registry ${{ env.REGISTRY_TENCENT }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_TENCENT }}
|
||||
username: 100002400732
|
||||
password: ${{ secrets.REGISTRY_TENCENT_PUBLISH_SECRETS }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY_DOCKERHUB }}/${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
${{ env.REGISTRY }}/linuxsuren/api-testing
|
||||
${{ env.REGISTRY_DOCKERHUB }}/${{ env.IMAGE_NAME }}
|
||||
${{ env.REGISTRY_ALIYUN }}/${{ env.IMAGE_NAME }}
|
||||
${{ env.REGISTRY_TENCENT }}/${{ env.IMAGE_NAME }}
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
|
@ -174,42 +126,88 @@ jobs:
|
|||
unset APP_VERSION
|
||||
unset HELM_VERSION
|
||||
fi
|
||||
make helm-package helm-push
|
||||
make helm-pkg helm-push
|
||||
|
||||
image-aliyuncs:
|
||||
runs-on: ubuntu-20.04
|
||||
BuildDesktop:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# see https://github.com/actions/runner-images
|
||||
os: [ubuntu-latest, windows-latest, macos-latest, macos-13]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: ./tools/github-actions/setup-deps
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
fetch-tags: true
|
||||
fetch-depth: 0
|
||||
- name: Set output
|
||||
id: vars
|
||||
run: echo "tag=$(git describe --tags)" >> $GITHUB_OUTPUT
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
|
||||
- name: Log into registry ${{ env.REGISTRY_ALIYUN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
||||
node-version: 18.x
|
||||
- name: Build Server on Windows
|
||||
if: runner.os == 'Windows'
|
||||
env:
|
||||
BINARY: atest.exe
|
||||
run: |
|
||||
make desktop-package
|
||||
- name: Build Server
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
make desktop-package
|
||||
# for fixing Error: Cannot find module 'appdmg'
|
||||
- name: Install Python 3.11.4
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_ALIYUN }}
|
||||
username: ${{ secrets.REGISTRY_ALIYUN_USER }}
|
||||
password: ${{ secrets.REGISTRY_ALIYUN_PUBLISH_SECRETS }}
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: ${{ env.REGISTRY_ALIYUN }}/${{ env.IMAGE_NAME }}
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: VERSION=${{ steps.vars.outputs.tag }}
|
||||
python-version: '3.11.4'
|
||||
- name: Upload to Draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
if: github.ref == 'refs/heads/master' && runner.os != 'Windows'
|
||||
run: |
|
||||
cd console/atest-desktop
|
||||
export TAG=$(gh release list -L 1 | awk '{print $4}')
|
||||
export TAG=${TAG#"v"}
|
||||
jq '.version = env.TAG' package.json > package.json.new && mv package.json.new package.json
|
||||
npm i
|
||||
npm run publish
|
||||
- name: Upload to Draft on Windows
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
if: github.ref == 'refs/heads/master' && runner.os == 'Windows'
|
||||
run: |
|
||||
cd console/atest-desktop
|
||||
$TAG = (gh release list -L 1).Split(' ')[0]
|
||||
if ($TAG -like "v*") {
|
||||
$TAG = $TAG -replace "^v", ""
|
||||
}
|
||||
Set-Content -Path "env:TAG" -Value "$TAG"
|
||||
jq '.version = env.TAG' package.json > package.json.new
|
||||
rm package.json
|
||||
Rename-Item -Path package.json.new -NewName package.json
|
||||
npm i
|
||||
npm run publish
|
||||
- name: Upload
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
if: github.ref != 'refs/heads/master' && runner.os != 'Windows'
|
||||
run: |
|
||||
cd console/atest-desktop
|
||||
export TAG=$(git describe --tags --abbrev=0)
|
||||
export TAG=${TAG#"v"}
|
||||
jq '.version = env.TAG' package.json > package.json.new && mv package.json.new package.json
|
||||
npm i
|
||||
npm run publish
|
||||
- name: Upload on Windows
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PUBLISH_SECRETS }}
|
||||
if: github.ref != 'refs/heads/master' && runner.os == 'Windows'
|
||||
run: |
|
||||
cd console/atest-desktop
|
||||
$TAG = git describe --tags --abbrev=0
|
||||
if ($TAG -like "v*") {
|
||||
$TAG = $TAG -replace "^v", ""
|
||||
}
|
||||
Set-Content -Path "env:TAG" -Value "$TAG"
|
||||
jq '.version = env.TAG' package.json > package.json.new
|
||||
rm package.json
|
||||
Rename-Item -Path package.json.new -NewName package.json
|
||||
npm i
|
||||
npm run publish
|
||||
|
|
|
@ -10,8 +10,14 @@ console/atest-ui/node_modules
|
|||
cmd/data/index.html
|
||||
cmd/data/index.js
|
||||
cmd/data/index.css
|
||||
operator/bundle
|
||||
helm/*.tgz
|
||||
helm/api-testing/*.tgz
|
||||
oryxBuildBinary
|
||||
/helm/api-testing/charts/
|
||||
console/atest-desktop/out
|
||||
console/atest-desktop/node_modules
|
||||
console/atest-desktop/atest
|
||||
console/atest-desktop/atest.exe
|
||||
console/atest-desktop/coverage
|
||||
atest-store-git
|
||||
.db
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
## Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery
|
||||
|
||||
- Personal attacks
|
||||
|
||||
- Trolling or insulting/derogatory comments
|
||||
|
||||
- Public or private harassment
|
||||
|
||||
- Publishing other’s private information, such as physical or electronic addresses, without explicit permission
|
||||
|
||||
- Other unethical or unprofessional conduct
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org/), version 1.3.0, available at [contributor-covenant.org/version/1/3/0/](http://contributor-covenant.org/version/1/3/0/)
|
|
@ -0,0 +1,95 @@
|
|||
> 中文 | [English](CONTRIBUTING.md)
|
||||
|
||||
请加入我们,共同完善这个项目。
|
||||
|
||||
后端由 [Golang](https://go.dev/) 编写,前端由 [Vue](https://vuejs.org/) 编写。
|
||||
|
||||
### 对于初学者
|
||||
|
||||
在开始之前,您可能需要了解以下技术:
|
||||
|
||||
| Name | Domain |
|
||||
|-----------------------------------------------------------------------------|------------------------------------------------------------------------|
|
||||
| [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview) Protocol | Core |
|
||||
| [RESTful](https://en.wikipedia.org/wiki/REST) | Core |
|
||||
| [gRPC](https://grpc.io/) | `gRPC` runner extension |
|
||||
| [Prometheus](https://prometheus.io/) | Application monitor |
|
||||
| [Cobra](https://github.com/spf13/cobra) | The Go CLI framework |
|
||||
| [Element Plus](https://element-plus.org/) | The front-end framework |
|
||||
| [Docker](https://www.docker.com/get-started/) | The container image build |
|
||||
| [Helm chart](https://helm.sh/) | The [Kubernetes](https://kubernetes.io/docs/home/) application package |
|
||||
| [GitHub Actions](https://docs.github.com/en/actions) | The continuous integration |
|
||||
| [make](https://www.gnu.org/software/make/) | The automated Build Tools |
|
||||
| [Docs Guide](https://github.com/LinuxSuRen/api-testing.git) | 文档编写指南 |
|
||||
|
||||
## 设置开发环境
|
||||
|
||||
> 本项目使用 `make` 作为构建工具,并设计了非常强大的 make 指令系统。您可以通过运行 `make help` 查看所有可用的命令。
|
||||
|
||||
强烈建议您配置 `git pre-commit` 钩子。它会强制在提交前运行单元测试。
|
||||
运行以下命令:
|
||||
|
||||
```shell
|
||||
make install-precheck
|
||||
```
|
||||
|
||||
## 打印各行代码:
|
||||
|
||||
```shell
|
||||
git ls-files | xargs cloc
|
||||
```
|
||||
|
||||
## pprof
|
||||
|
||||
```shell
|
||||
go tool pprof -http=:9999 http://localhost:8080/debug/pprof/heap
|
||||
```
|
||||
|
||||
其他用法:
|
||||
|
||||
* `/debug/pprof/heap?gc=1`
|
||||
* `/debug/pprof/heap?seconds=10`
|
||||
* `/debug/pprof/goroutine/?debug=0`
|
||||
|
||||
## SkyWalking
|
||||
|
||||
```shell
|
||||
docker run -p 12800:12800 -p 9412:9412 docker.io/apache/skywalking-oap-server:9.0.0
|
||||
docker run -p 8080:8080 -e SW_OAP_ADDRESS=http://172.11.0.6:12800 -e SW_ZIPKIN_ADDRESS=http://172.11.0.6:9412 docker.io/apache/skywalking-ui:9.0.0
|
||||
|
||||
make build
|
||||
|
||||
export SW_AGENT_NAME=atest
|
||||
export SW_AGENT_REPORTER_GRPC_BACKEND_SERVICE=172.11.0.6:30689
|
||||
export SW_AGENT_PLUGIN_CONFIG_HTTP_SERVER_COLLECT_PARAMETERS=true
|
||||
export SW_AGENT_METER_COLLECT_INTERVAL=3
|
||||
export SW_AGENT_LOG_TYPE=std
|
||||
export SW_AGENT_REPORTER_DISCARD=true
|
||||
./bin/atest server --local-storage 'bin/*.yaml' --http-port 8082 --port 7072 --console-path console/atest-ui/dist/
|
||||
```
|
||||
|
||||
通过 BanYanDB 运行 SkyWalking:
|
||||
|
||||
```shell
|
||||
docker run -p 17912:17912 -p 17913:17913 apache/skywalking-banyandb:latest standalone
|
||||
|
||||
docker run -p 12800:12800 -p 9412:9412 \
|
||||
-e SW_STORAGE=banyandb \
|
||||
-e SW_STORAGE_BANYANDB_HOST=192.168.1.98 \
|
||||
docker.io/apache/skywalking-oap-server
|
||||
```
|
||||
|
||||
## 第一次贡献
|
||||
|
||||
对于第一次对此项目贡献代码的开发者,您应该在本地开发环境运行如下命令:
|
||||
|
||||
```shell
|
||||
make test
|
||||
```
|
||||
|
||||
以确保通过项目测试,这会有助于您检查并解决在提交时遇到的错误,同时减少 review 的复杂度。
|
||||
|
||||
## FAQ
|
||||
|
||||
* Got sum missing match error of go.
|
||||
* 运行命令: `go clean -modcache && go mod tidy`.
|
|
@ -0,0 +1,95 @@
|
|||
> English | [中文](CONTRIBUTING-ZH.md)
|
||||
|
||||
Please join us to improve this project.
|
||||
|
||||
The backend is written by [Golang](https://go.dev/), and the front-end is written by [Vue](https://vuejs.org/).
|
||||
|
||||
## For beginner
|
||||
|
||||
You might need to know the following tech before get started.
|
||||
|
||||
| Name | Domain |
|
||||
|-----------------------------------------------------------------------------|------------------------------------------------------------------------|
|
||||
| [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview) Protocol | Core |
|
||||
| [RESTful](https://en.wikipedia.org/wiki/REST) | Core |
|
||||
| [gRPC](https://grpc.io/) | `gRPC` runner extension |
|
||||
| [Prometheus](https://prometheus.io/) | Application monitor |
|
||||
| [Cobra](https://github.com/spf13/cobra) | The Go CLI framework |
|
||||
| [Element Plus](https://element-plus.org/) | The front-end framework |
|
||||
| [Docker](https://www.docker.com/get-started/) | The container image build |
|
||||
| [Helm chart](https://helm.sh/) | The [Kubernetes](https://kubernetes.io/docs/home/) application package |
|
||||
| [GitHub Actions](https://docs.github.com/en/actions) | The continuous integration |
|
||||
| [make](https://www.gnu.org/software/make/) | The automated Build Tools |
|
||||
| [Docs Guide](https://github.com/LinuxSuRen/api-testing.git) | Documentation Guidelines |
|
||||
|
||||
## Setup development environment
|
||||
|
||||
> This project uses `make` as a build tool and has a very powerful make command system.
|
||||
> You can see all the available commands by running `make help`.
|
||||
|
||||
It's highly recommended you to configure the git pre-commit hook. It will force to run unit tests before commit.
|
||||
Run the following command:
|
||||
|
||||
```shell
|
||||
make install-precheck
|
||||
```
|
||||
|
||||
## Print the code of lines:
|
||||
|
||||
```shell
|
||||
git ls-files | xargs cloc
|
||||
```
|
||||
|
||||
## pprof
|
||||
|
||||
```
|
||||
go tool pprof -http=:9999 http://localhost:8080/debug/pprof/heap
|
||||
```
|
||||
|
||||
Other usage of this:
|
||||
* `/debug/pprof/heap?gc=1`
|
||||
* `/debug/pprof/heap?seconds=10`
|
||||
* `/debug/pprof/goroutine/?debug=0`
|
||||
|
||||
## SkyWalking
|
||||
|
||||
```shell
|
||||
docker run -p 12800:12800 -p 9412:9412 docker.io/apache/skywalking-oap-server:9.0.0
|
||||
docker run -p 8080:8080 -e SW_OAP_ADDRESS=http://172.11.0.6:12800 -e SW_ZIPKIN_ADDRESS=http://172.11.0.6:9412 docker.io/apache/skywalking-ui:9.0.0
|
||||
|
||||
make build
|
||||
|
||||
export SW_AGENT_NAME=atest
|
||||
export SW_AGENT_REPORTER_GRPC_BACKEND_SERVICE=172.11.0.6:30689
|
||||
export SW_AGENT_PLUGIN_CONFIG_HTTP_SERVER_COLLECT_PARAMETERS=true
|
||||
export SW_AGENT_METER_COLLECT_INTERVAL=3
|
||||
export SW_AGENT_LOG_TYPE=std
|
||||
export SW_AGENT_REPORTER_DISCARD=true
|
||||
./bin/atest server --local-storage 'bin/*.yaml' --http-port 8082 --port 7072 --console-path console/atest-ui/dist/
|
||||
```
|
||||
|
||||
Run SkyWalking with BanYanDB:
|
||||
|
||||
```shell
|
||||
docker run -p 17912:17912 -p 17913:17913 apache/skywalking-banyandb:latest standalone
|
||||
|
||||
docker run -p 12800:12800 -p 9412:9412 \
|
||||
-e SW_STORAGE=banyandb \
|
||||
-e SW_STORAGE_BANYANDB_HOST=192.168.1.98 \
|
||||
docker.io/apache/skywalking-oap-server
|
||||
```
|
||||
|
||||
## First contribution
|
||||
|
||||
For developers contributing code to this project for the first time, you should run the following command in your local development environment:
|
||||
|
||||
```shell
|
||||
make test
|
||||
```
|
||||
|
||||
This will help you to check for and fix any bugs that you encounter at commit time, as well as reduce the complexity of the review.
|
||||
|
||||
## FAQ
|
||||
|
||||
* Got sum missing match error of go.
|
||||
* Run command: `go clean -modcache && go mod tidy`
|
|
@ -1,58 +0,0 @@
|
|||
Please join us to improve this project.
|
||||
|
||||
## Setup development environment
|
||||
It's highly recommended you to configure the git pre-commit hook. It will force to run unit tests before commit.
|
||||
Run the following command:
|
||||
|
||||
```shell
|
||||
make install-precheck
|
||||
```
|
||||
|
||||
## Print the code of lines:
|
||||
|
||||
```shell
|
||||
git ls-files | xargs cloc
|
||||
```
|
||||
|
||||
## pprof
|
||||
|
||||
```
|
||||
go tool pprof -http=:9999 http://localhost:8080/debug/pprof/heap
|
||||
```
|
||||
|
||||
Other usage of this:
|
||||
* `/debug/pprof/heap?gc=1`
|
||||
* `/debug/pprof/heap?seconds=10`
|
||||
* `/debug/pprof/goroutine/?debug=0`
|
||||
|
||||
## Skywalking
|
||||
|
||||
```shell
|
||||
docker run -p 12800:12800 -p 9412:9412 docker.io/apache/skywalking-oap-server:9.0.0
|
||||
docker run -p 8080:8080 -e SW_OAP_ADDRESS=http://172.11.0.6:12800 -e SW_ZIPKIN_ADDRESS=http://172.11.0.6:9412 docker.io/apache/skywalking-ui:9.0.0
|
||||
|
||||
make build
|
||||
|
||||
export SW_AGENT_NAME=atest
|
||||
export SW_AGENT_REPORTER_GRPC_BACKEND_SERVICE=172.11.0.6:30689
|
||||
export SW_AGENT_PLUGIN_CONFIG_HTTP_SERVER_COLLECT_PARAMETERS=true
|
||||
export SW_AGENT_METER_COLLECT_INTERVAL=3
|
||||
export SW_AGENT_LOG_TYPE=std
|
||||
export SW_AGENT_REPORTER_DISCARD=true
|
||||
./bin/atest server --local-storage 'bin/*.yaml' --http-port 8082 --port 7072 --console-path console/atest-ui/dist/
|
||||
```
|
||||
|
||||
Run SkyWalking with BanYanDB
|
||||
```shell
|
||||
docker run -p 17912:17912 -p 17913:17913 apache/skywalking-banyandb:latest standalone
|
||||
|
||||
docker run -p 12800:12800 -p 9412:9412 \
|
||||
-e SW_STORAGE=banyandb \
|
||||
-e SW_STORAGE_BANYANDB_HOST=192.168.1.98 \
|
||||
docker.io/apache/skywalking-oap-server
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
* Got sum missing match error of go.
|
||||
* Run command: `go clean -modcache && go mod tidy`
|
37
Dockerfile
37
Dockerfile
|
@ -5,22 +5,23 @@ COPY console/atest-ui .
|
|||
RUN npm install --ignore-scripts --registry=https://registry.npmmirror.com
|
||||
RUN npm run build-only
|
||||
|
||||
FROM docker.io/golang:1.20 AS builder
|
||||
FROM docker.io/golang:1.22.4 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG GOPROXY
|
||||
WORKDIR /workspace
|
||||
RUN mkdir -p console/atest-ui
|
||||
|
||||
COPY cmd/ cmd/
|
||||
COPY pkg/ pkg/
|
||||
COPY operator/ operator/
|
||||
COPY .github/ .github/
|
||||
COPY sample/ sample/
|
||||
COPY docs/ docs/
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
COPY go.work go.work
|
||||
COPY go.work.sum go.work.sum
|
||||
COPY main.go main.go
|
||||
COPY console/atest-ui/ui.go console/atest-ui/ui.go
|
||||
COPY console/atest-ui/package.json console/atest-ui/package.json
|
||||
COPY README.md README.md
|
||||
COPY LICENSE LICENSE
|
||||
|
||||
|
@ -32,21 +33,13 @@ COPY --from=ui /workspace/dist/assets/*.css cmd/data/index.css
|
|||
RUN CGO_ENABLED=0 go build -v -a -ldflags "-w -s -X github.com/linuxsuren/api-testing/pkg/version.version=${VERSION}\
|
||||
-X github.com/linuxsuren/api-testing/pkg/version.date=$(date +%Y-%m-%d)" -o atest .
|
||||
|
||||
FROM ghcr.io/linuxsuren/atest-ext-store-mongodb:master as mango
|
||||
FROM ghcr.io/linuxsuren/atest-ext-store-git:master as git
|
||||
FROM ghcr.io/linuxsuren/atest-ext-store-s3:master as s3
|
||||
FROM ghcr.io/linuxsuren/atest-ext-store-etcd:master as etcd
|
||||
FROM ghcr.io/linuxsuren/atest-ext-store-orm:master as orm
|
||||
FROM ghcr.io/linuxsuren/atest-ext-monitor-docker:master as docker
|
||||
FROM ghcr.io/linuxsuren/atest-ext-collector:master as collector
|
||||
FROM ghcr.io/linuxsuren/api-testing-vault-extension:v0.0.1 as vault
|
||||
|
||||
FROM docker.io/library/ubuntu:23.10
|
||||
FROM docker.io/library/alpine:3.20.3
|
||||
|
||||
LABEL "com.github.actions.name"="API testing"
|
||||
LABEL "com.github.actions.description"="API testing"
|
||||
LABEL "com.github.actions.icon"="home"
|
||||
LABEL "com.github.actions.color"="red"
|
||||
LABEL org.opencontainers.image.description "This is an API testing tool that supports HTTP, gRPC, and GraphQL."
|
||||
|
||||
LABEL "repository"="https://github.com/linuxsuren/api-testing"
|
||||
LABEL "homepage"="https://github.com/linuxsuren/api-testing"
|
||||
|
@ -55,21 +48,11 @@ LABEL "maintainer"="Rick <linuxsuren@gmail.com>"
|
|||
LABEL "Name"="API testing"
|
||||
|
||||
COPY --from=builder /workspace/atest /usr/local/bin/atest
|
||||
COPY --from=collector /usr/local/bin/atest-collector /usr/local/bin/atest-collector
|
||||
COPY --from=orm /usr/local/bin/atest-store-orm /usr/local/bin/atest-store-orm
|
||||
COPY --from=s3 /usr/local/bin/atest-store-s3 /usr/local/bin/atest-store-s3
|
||||
COPY --from=etcd /usr/local/bin/atest-store-etcd /usr/local/bin/atest-store-etcd
|
||||
COPY --from=git /usr/local/bin/atest-store-git /usr/local/bin/atest-store-git
|
||||
COPY --from=mango /usr/local/bin/atest-store-mongodb /usr/local/bin/atest-store-mongodb
|
||||
COPY --from=docker /usr/local/bin/atest-monitor-docker /usr/local/bin/atest-monitor-docker
|
||||
COPY --from=vault /usr/local/bin/atest-vault-ext /usr/local/bin
|
||||
COPY --from=builder /workspace/LICENSE /LICENSE
|
||||
COPY --from=builder /workspace/README.md /README.md
|
||||
|
||||
RUN apt update -y && \
|
||||
# required for atest-store-git
|
||||
apt install -y --no-install-recommends ssh-client ca-certificates && \
|
||||
apt install -y curl
|
||||
|
||||
# required for atest-store-git
|
||||
RUN apk add curl openssh-client bash openssl
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["atest", "server", "--local-storage=/var/data/api-testing/*.yaml"]
|
||||
|
|
165
Makefile
165
Makefile
|
@ -1,151 +1,22 @@
|
|||
IMG_TOOL?=docker
|
||||
BINARY?=atest
|
||||
TOOLEXEC?= #-toolexec="skywalking-go-agent"
|
||||
BUILD_FLAG?=-ldflags "-w -s -X github.com/linuxsuren/api-testing/pkg/version.version=$(shell git describe --tags) \
|
||||
-X github.com/linuxsuren/api-testing/pkg/version.date=$(shell date +%Y-%m-%d)"
|
||||
GOPROXY?=direct
|
||||
HELM_VERSION?=v0.0.3
|
||||
APP_VERSION?=v0.0.13
|
||||
HELM_REPO?=docker.io/linuxsuren
|
||||
# All make targets should be implemented in tools/make/*.mk
|
||||
# ====================================================================================================
|
||||
# Supported Targets: (Run `make help` to see more API Testing information)
|
||||
# ====================================================================================================
|
||||
|
||||
fmt:
|
||||
go mod tidy
|
||||
go fmt ./...
|
||||
build:
|
||||
mkdir -p bin
|
||||
rm -rf bin/atest
|
||||
CGO_ENABLED=0 go build ${TOOLEXEC} -a ${BUILD_FLAG} -o bin/${BINARY} main.go
|
||||
build-ui:
|
||||
cd console/atest-ui && npm i && npm run build-only
|
||||
embed-ui:
|
||||
cd console/atest-ui && npm i && npm run build-only
|
||||
cp console/atest-ui/dist/index.html cmd/data/index.html
|
||||
cp console/atest-ui/dist/assets/*.js cmd/data/index.js
|
||||
cp console/atest-ui/dist/assets/*.css cmd/data/index.css
|
||||
clean-embed-ui:
|
||||
git checkout cmd/data/index.html
|
||||
git checkout cmd/data/index.js
|
||||
git checkout cmd/data/index.css
|
||||
build-embed-ui: embed-ui
|
||||
GOOS=${OS} go build ${TOOLEXEC} -a -ldflags "-w -s -X github.com/linuxsuren/api-testing/pkg/version.version=$(shell git rev-parse --short HEAD)" -o bin/${BINARY} main.go
|
||||
make clean-embed-ui
|
||||
build-darwin:
|
||||
BINARY=atest_darwin GOOS=darwin make build
|
||||
build-win:
|
||||
BINARY=atest.exe GOOS=windows make build
|
||||
build-win-embed-ui:
|
||||
BINARY=atest.exe GOOS=windows make build-embed-ui
|
||||
goreleaser:
|
||||
goreleaser build --rm-dist --snapshot
|
||||
make clean-embed-ui
|
||||
build-image:
|
||||
${IMG_TOOL} build -t ghcr.io/linuxsuren/api-testing:master . \
|
||||
--build-arg GOPROXY=${GOPROXY} \
|
||||
--build-arg VERSION=$(shell git describe --abbrev=0 --tags)-$(shell git rev-parse --short HEAD)
|
||||
run-image:
|
||||
docker run -p 7070:7070 -p 8080:8080 ghcr.io/linuxsuren/api-testing:master
|
||||
run-server: build-ui
|
||||
go run . server --local-storage 'bin/*.yaml' --console-path console/atest-ui/dist
|
||||
run-console:
|
||||
cd console/atest-ui && npm run dev
|
||||
copy:
|
||||
sudo cp bin/atest /usr/local/bin/
|
||||
copy-restart: build-embed-ui
|
||||
atest service stop
|
||||
make copy
|
||||
atest service restart
|
||||
# An wrapper around `make` so that we can force on the,
|
||||
# --warn-undefined-variables flag. Sure, you can set
|
||||
# `MAKEFLAGS += --warn-undefined-variables` from inside of a Makefile,
|
||||
# but then it won't turn on until the second phase (recipe execution),
|
||||
# and won't actually be on during the initial phase (parsing).
|
||||
# See: https://www.gnu.org/software/make/manual/make.html#Reading-Makefiles
|
||||
|
||||
# helm
|
||||
helm-dev-update:
|
||||
helm dep update helm/api-testing
|
||||
helm-package: helm-dev-update
|
||||
helm package helm/api-testing --version ${HELM_VERSION}-helm --app-version ${APP_VERSION} -d bin
|
||||
helm-push:
|
||||
helm push bin/api-testing-${HELM_VERSION}-helm.tgz oci://${HELM_REPO}
|
||||
helm-lint: helm-dev-update
|
||||
helm lint helm/api-testing
|
||||
# Have everything-else ("%") depend on _run (which uses
|
||||
# $(MAKECMDGOALS) to decide what to run), rather than having
|
||||
# everything else run $(MAKE) directly, since that'd end up running
|
||||
# multiple sub-Makes if you give multiple targets on the CLI.
|
||||
|
||||
test:
|
||||
go test ./... -cover -v -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
testlong:
|
||||
go test pkg/limit/limiter_long_test.go -v
|
||||
test-ui:
|
||||
cd console/atest-ui && npm run test:unit
|
||||
test-ui-e2e:
|
||||
cd console/atest-ui && npm i && npm run test:e2e
|
||||
test-operator:
|
||||
cd operator && make test # converage file path: operator/cover.out
|
||||
test-all-backend: test
|
||||
test-all: test-all-backend test-ui
|
||||
test-e2e:
|
||||
cd e2e && ./start.sh && ./start.sh compose-k8s.yaml && ./start.sh compose-external.yaml
|
||||
fuzz:
|
||||
cd pkg/util && go test -fuzz FuzzZeroThenDefault -fuzztime 6s
|
||||
install-precheck:
|
||||
cp .github/pre-commit .git/hooks/pre-commit
|
||||
_run:
|
||||
@$(MAKE) --warn-undefined-variables -f tools/make/common.mk $(MAKECMDGOALS)
|
||||
|
||||
grpc:
|
||||
protoc --proto_path=. \
|
||||
--go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
pkg/server/server.proto \
|
||||
pkg/testing/remote/loader.proto \
|
||||
pkg/runner/monitor/monitor.proto
|
||||
grpc-gw:
|
||||
protoc -I . --grpc-gateway_out . \
|
||||
--grpc-gateway_opt logtostderr=true \
|
||||
--grpc-gateway_opt paths=source_relative \
|
||||
--grpc-gateway_opt generate_unbound_methods=true \
|
||||
--openapiv2_out . \
|
||||
--openapiv2_opt logtostderr=true \
|
||||
--openapiv2_opt generate_unbound_methods=true \
|
||||
pkg/server/server.proto
|
||||
grpc-java:
|
||||
protoc --plugin=protoc-gen-grpc-java \
|
||||
--grpc-java_out=bin --proto_path=. \
|
||||
pkg/server/server.proto \
|
||||
pkg/testing/remote/loader.proto
|
||||
grpc-js:
|
||||
protoc -I=pkg/server server.proto \
|
||||
--js_out=import_style=commonjs:bin \
|
||||
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:bin
|
||||
# https://github.com/grpc/grpc-web
|
||||
grpc-ts:
|
||||
protoc -I=pkg/server server.proto \
|
||||
--js_out=import_style=commonjs,binary:console/atest-ui/src \
|
||||
--grpc-web_out=import_style=typescript,mode=grpcwebtext:console/atest-ui/src
|
||||
# grpc-java:
|
||||
# protoc --plugin=protoc-gen-grpc-java=/usr/local/bin/protoc-gen-grpc-java \
|
||||
# --grpc-java_out=bin --proto_path=pkg/server server.proto
|
||||
grpc-decs:
|
||||
protoc --proto_path=. \
|
||||
--descriptor_set_out=.github/testing/server.pb \
|
||||
pkg/server/server.proto
|
||||
|
||||
grpc-testproto:
|
||||
protoc -I . \
|
||||
--descriptor_set_out=pkg/runner/grpc_test/test.pb \
|
||||
pkg/runner/grpc_test/test.proto
|
||||
|
||||
protoc -I . \
|
||||
--go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
pkg/runner/grpc_test/test.proto
|
||||
|
||||
hd:
|
||||
curl https://linuxsuren.github.io/tools/install.sh|bash
|
||||
install-tool: hd
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
|
||||
hd i protoc-gen-grpc-web
|
||||
hd i protoc-gen-grpc-gateway
|
||||
hd get protocolbuffers/protobuf@v25.1 -o protobuf.zip
|
||||
unzip protobuf.zip bin/protoc
|
||||
rm -rf protobuf.zip
|
||||
sudo install bin/protoc /usr/local/bin/
|
||||
sudo hd get https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v2.18.1/protoc-gen-openapiv2-v2.18.1-linux-x86_64 -o /usr/local/bin/protoc-gen-openapiv2
|
||||
sudo chmod +x /usr/local/bin/protoc-gen-openapiv2
|
||||
init-env: hd
|
||||
hd i cli/cli
|
||||
gh extension install linuxsuren/gh-dev
|
||||
.PHONY: _run
|
||||
$(if $(MAKECMDGOALS),$(MAKECMDGOALS): %: _run)
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
[](https://cla-assistant.io/LinuxSuRen/api-testing)
|
||||
[](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://tooomm.github.io/github-release-stats/?username=linuxsuren&repository=api-testing)
|
||||
[](https://hub.docker.com/r/linuxsuren/api-testing)
|
||||
[](https://github.com/LinuxSuRen/open-source-best-practice)
|
||||

|
||||
|
||||
> 中文 | [English](README.md)
|
||||
|
||||
一个开源的 API 测试工具。🚀
|
||||
|
||||
## 功能特性
|
||||
|
||||
* 支持的协议: HTTP, gRPC, tRPC
|
||||
* 支持多种格式的测试结果导出: Markdown, HTML, PDF, Stdout
|
||||
* 简单易用的 Mock 服务,支持 OpenAPI
|
||||
* 支持转换为 [JMeter](https://jmeter.apache.org/) 文件格式
|
||||
* 支持响应体字段检查或 [eval](https://expr.medv.io/)
|
||||
* 使用 [JSON schema] 校验响应参数(https://json-schema.org/)
|
||||
* 支持预处理和后处理 API 请求
|
||||
* 支持以服务器模式运行并支持 [gRPC](pkg/server/server.proto) 和 HTTP endpoint
|
||||
* [VS Code 扩展支持](https://github.com/LinuxSuRen/vscode-api-testing)
|
||||
* [Github 扩展支持](https://github.com/marketplace/actions/api-testing-with-kubernetes)
|
||||
* 支持多种存储方式 (Local, ORM Database, S3, Git, Etcd, etc.)
|
||||
* [HTTP API record](https://github.com/LinuxSuRen/atest-ext-collector)
|
||||
* 支持多种安装方式(CLI, Container, Native-Service, [Operator](https://github.com/LinuxSuRen/atest-operator), Helm, etc.)
|
||||
* 整合 Prometheus, SkyWalking 监控
|
||||
|
||||
## 快速开始
|
||||
|
||||
[](http://play-with-docker.com?stack=https://raw.githubusercontent.com/LinuxSuRen/api-testing/master/docs/manifests/docker-compose.yml)
|
||||
|
||||
通过 [hd](https://github.com/LinuxSuRen/http-downloader) 安装,或从 [releases](https://github.com/LinuxSuRen/api-testing/releases) 下载安装:
|
||||
|
||||
```shell
|
||||
hd install atest
|
||||
```
|
||||
|
||||
您也可以通过 kubernetes 安装,更多细节请参考: [manifests](docs/manifests/kubernetes/default/manifest.yaml).
|
||||
|
||||
用法如下:
|
||||
|
||||
```shell
|
||||
API testing tool
|
||||
|
||||
Usage:
|
||||
atest [command]
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
func Print all the supported functions
|
||||
help Help about any command
|
||||
json Print the JSON schema of the test suites struct
|
||||
run Run the test suite
|
||||
sample Generate a sample test case YAML file
|
||||
server Run as a server mode
|
||||
service Install atest as a Linux service
|
||||
|
||||
Flags:
|
||||
-h, --help help for atest
|
||||
-v, --version version for atest
|
||||
|
||||
Use "atest [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
API Testing 使用示例,在此示例中,您将通过 md 格式阅览生成的接口测试报告:
|
||||
|
||||
`atest run -p sample/testsuite-gitlab.yaml --duration 1m --thread 3 --report md`
|
||||
|
||||
| API | Average | Max | Min | Count | Error |
|
||||
|---|---|---|---|---|---|
|
||||
| GET https://gitlab.com/api/v4/projects | 1.152777167s | 2.108680194s | 814.928496ms | 99 | 0 |
|
||||
| GET https://gitlab.com/api/v4/projects/45088772 | 840.761064ms | 1.487285371s | 492.583066ms | 10 | 0 |
|
||||
consume: 1m2.153686448s
|
||||
|
||||
## 在 Docker 中使用
|
||||
|
||||
在 Docker 中以服务器模式运行 `atest`,您可以通过 `8080` 访问 `atest` 的 UI 控制台:
|
||||
|
||||
```bash
|
||||
docker run --pull always -p 8080:8080 ghcr.io/linuxsuren/api-testing:master
|
||||
```
|
||||
|
||||
在 Docker 中使用 `atest-collector`:
|
||||
|
||||
```shell
|
||||
docker run -p 1234:8080 -v /var/tmp:/var/tmp \
|
||||
ghcr.io/linuxsuren/api-testing atest-collector \
|
||||
--filter-path /api \
|
||||
-o /var/tmp/sample.yaml
|
||||
# you could find the test cases file from /var/tmp/sample
|
||||
# cat /var/tmp/sample
|
||||
```
|
||||
|
||||
## 模板
|
||||
|
||||
以下字段的模板配置参考:[sprig](http://masterminds.github.io/sprig/):
|
||||
|
||||
* API
|
||||
* Request Body
|
||||
* Request Header
|
||||
|
||||
### Functions
|
||||
|
||||
您可以使用 [sprig](http://masterminds.github.io/sprig/) 中的所有常用函数。此外,还有一些特殊函数可以在 `atest` 中使用:
|
||||
|
||||
| Name | Usage |
|
||||
|---|---|
|
||||
| `randomKubernetesName` | `{{randomKubernetesName}}` to generate Kubernetes resource name randomly, the name will have 8 chars |
|
||||
| `sleep` | `{{sleep(1)}}` in the pre and post request handle |
|
||||
|
||||
## 验证 Kuberntes 资源
|
||||
|
||||
`atest` 可以验证任何类型的 Kubernetes 资源。使用前请先设置 Kubernetes 相关的环境变量:
|
||||
|
||||
* `KUBERNETES_SERVER`
|
||||
* `KUBERNETES_TOKEN`
|
||||
|
||||
另请参考 [example](sample/kubernetes.yaml)。
|
||||
|
||||
## 待办事项
|
||||
|
||||
* 减少上下文的大小
|
||||
* 支持自定义上下文
|
||||
|
||||
## 功能限制
|
||||
|
||||
* 仅支持解析 map 或 array 类型的响应体。
|
||||
|
||||
## 社区交流
|
||||
|
||||
欢迎使用以下联系方式,探讨有关 API Testing 的任何问题!
|
||||
|
||||
### 邮件列表
|
||||
|
||||
`api-testing-tech@googlegroups.com`, 欢迎通过此邮件列表讨论与 API Testing 相关的任何问题。
|
||||
|
||||
### GitHub Discussion
|
||||
|
||||
[GitHub Discussion](https://github.com/LinuxSuRen/api-testing/discussions/new/choose)
|
263
README.md
263
README.md
|
@ -1,122 +1,141 @@
|
|||
[](https://cla-assistant.io/LinuxSuRen/api-testing)
|
||||
[](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://tooomm.github.io/github-release-stats/?username=linuxsuren&repository=api-testing)
|
||||
[](https://hub.docker.com/r/linuxsuren/api-testing)
|
||||
[](https://github.com/LinuxSuRen/open-source-best-practice)
|
||||
|
||||
This is a API testing tool.
|
||||
|
||||
## Features
|
||||
|
||||
* Supportted protocols: HTTP, gRPC, tRPC
|
||||
* Multiple test report formats: Markdown, HTML, PDF, Stdout
|
||||
* Support converting to [JMeter](https://jmeter.apache.org/) files
|
||||
* Response Body fields equation check or [eval](https://expr.medv.io/)
|
||||
* Validate the response body with [JSON schema](https://json-schema.org/)
|
||||
* Pre and post handle with the API request
|
||||
* Run in server mode, and provide the [gRPC](pkg/server/server.proto) and HTTP endpoint
|
||||
* [VS Code extension](https://github.com/LinuxSuRen/vscode-api-testing) support
|
||||
* Multiple storage backends supported(Local, ORM Database, S3, Git, Etcd, etc)
|
||||
* [HTTP API record](https://github.com/LinuxSuRen/atest-ext-collector)
|
||||
* Install in mutiple use cases(CLI, Container, Native-Service, Operator, Helm, etc)
|
||||
* Monitoring integration with Prometheus, Skywalking
|
||||
|
||||
## Get started
|
||||
|
||||
[](https://zeabur.com?referralCode=LinuxSuRen&utm_source=LinuxSuRen&utm_campaign=oss) [](http://play-with-docker.com?stack=https://raw.githubusercontent.com/LinuxSuRen/api-testing/master/docs/manifests/docker-compose.yml)
|
||||
|
||||
Install it via [hd](https://github.com/LinuxSuRen/http-downloader) or download from [releases](https://github.com/LinuxSuRen/api-testing/releases):
|
||||
|
||||
```shell
|
||||
hd install atest
|
||||
```
|
||||
|
||||
or, you can install it in Kubernetes. See also the [manifests](docs/manifests/kubernetes/default/manifest.yaml).
|
||||
|
||||
see the following usage:
|
||||
|
||||
```shell
|
||||
API testing tool
|
||||
|
||||
Usage:
|
||||
atest [command]
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
func Print all the supported functions
|
||||
help Help about any command
|
||||
json Print the JSON schema of the test suites struct
|
||||
run Run the test suite
|
||||
sample Generate a sample test case YAML file
|
||||
server Run as a server mode
|
||||
service Install atest as a Linux service
|
||||
|
||||
Flags:
|
||||
-h, --help help for atest
|
||||
-v, --version version for atest
|
||||
|
||||
Use "atest [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
below is an example of the usage, and you could see the report as well:
|
||||
|
||||
`atest run -p sample/testsuite-gitlab.yaml --duration 1m --thread 3 --report md`
|
||||
|
||||
| API | Average | Max | Min | Count | Error |
|
||||
|---|---|---|---|---|---|
|
||||
| GET https://gitlab.com/api/v4/projects | 1.152777167s | 2.108680194s | 814.928496ms | 99 | 0 |
|
||||
| GET https://gitlab.com/api/v4/projects/45088772 | 840.761064ms | 1.487285371s | 492.583066ms | 10 | 0 |
|
||||
consume: 1m2.153686448s
|
||||
|
||||
## Use in Docker
|
||||
|
||||
Use `atest` as server mode in Docker, then you could visit the UI from `8080`:
|
||||
```
|
||||
docker run --pull always -p 8080:8080 ghcr.io/linuxsuren/api-testing:master
|
||||
```
|
||||
|
||||
Use `atest-collector` in Docker:
|
||||
```shell
|
||||
docker run -p 1234:8080 -v /var/tmp:/var/tmp \
|
||||
ghcr.io/linuxsuren/api-testing atest-collector \
|
||||
--filter-path /api \
|
||||
-o /var/tmp/sample.yaml
|
||||
# you could find the test cases file from /var/tmp/sample
|
||||
# cat /var/tmp/sample
|
||||
```
|
||||
|
||||
## Template
|
||||
|
||||
The following fields are templated with [sprig](http://masterminds.github.io/sprig/):
|
||||
|
||||
* API
|
||||
* Request Body
|
||||
* Request Header
|
||||
|
||||
### Functions
|
||||
|
||||
You could use all the common functions which comes from [sprig](http://masterminds.github.io/sprig/). Besides some specific functions are available:
|
||||
|
||||
| Name | Usage |
|
||||
|---|---|
|
||||
| `randomKubernetesName` | `{{randomKubernetesName}}` to generate Kubernetes resource name randomly, the name will have 8 chars |
|
||||
| `sleep` | `{{sleep(1)}}` in the pre and post request handle |
|
||||
|
||||
## Verify against Kubernetes
|
||||
|
||||
It could verify any kinds of Kubernetes resources. Please set the environment variables before using it:
|
||||
|
||||
* `KUBERNETES_SERVER`
|
||||
* `KUBERNETES_TOKEN`
|
||||
|
||||
See also the [example](sample/kubernetes.yaml).
|
||||
|
||||
## TODO
|
||||
|
||||
* Reduce the size of context
|
||||
* Support customized context
|
||||
|
||||
## Limit
|
||||
|
||||
* Only support to parse the response body when it's a map or array
|
||||
[](https://cla-assistant.io/LinuxSuRen/api-testing)
|
||||
[](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
[](https://tooomm.github.io/github-release-stats/?username=linuxsuren&repository=api-testing)
|
||||
[](https://hub.docker.com/r/linuxsuren/api-testing)
|
||||
[](https://github.com/LinuxSuRen/open-source-best-practice)
|
||||

|
||||
|
||||
> English | [中文](README-ZH.md)
|
||||
|
||||
This is an awesome API testing tool. 🚀
|
||||
|
||||
## Features
|
||||
|
||||
* Supported protocols: HTTP, gRPC, tRPC
|
||||
* Multiple test report formats: Markdown, HTML, PDF, Stdout
|
||||
* Mock Server in simple configuration, and Open API support
|
||||
* Support converting to [JMeter](https://jmeter.apache.org/) files
|
||||
* Response Body fields equation check or [eval](https://expr.medv.io/)
|
||||
* Validate the response body with [JSON schema](https://json-schema.org/)
|
||||
* Pre and post handle with the API request
|
||||
* Run in server mode, and provide the [gRPC](pkg/server/server.proto) and HTTP endpoint
|
||||
* [VS Code extension](https://github.com/LinuxSuRen/vscode-api-testing) support
|
||||
* Simple Database query support
|
||||
* Multiple storage backends supported(Local, ORM Database, S3, Git, Etcd, etc.)
|
||||
* [HTTP API record](https://github.com/LinuxSuRen/atest-ext-collector)
|
||||
* Install in multiple use cases(CLI, Container, Native-Service, [Operator](https://github.com/LinuxSuRen/atest-operator), Helm, etc.)
|
||||
* Monitoring integration with Prometheus, SkyWalking
|
||||
|
||||
## Get started
|
||||
|
||||
[](http://play-with-docker.com?stack=https://raw.githubusercontent.com/LinuxSuRen/api-testing/master/docs/manifests/docker-compose.yml)
|
||||
|
||||
Install it via [hd](https://github.com/LinuxSuRen/http-downloader) or download from [releases](https://github.com/LinuxSuRen/api-testing/releases):
|
||||
|
||||
```shell
|
||||
hd install atest
|
||||
```
|
||||
|
||||
or, you can install it in Kubernetes. See also the [manifests](docs/manifests/kubernetes/default/manifest.yaml).
|
||||
|
||||
see the following usage:
|
||||
|
||||
```shell
|
||||
API testing tool
|
||||
|
||||
Usage:
|
||||
atest [command]
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
func Print all the supported functions
|
||||
help Help about any command
|
||||
json Print the JSON schema of the test suites struct
|
||||
run Run the test suite
|
||||
sample Generate a sample test case YAML file
|
||||
server Run as a server mode
|
||||
service Install atest as a Linux service
|
||||
|
||||
Flags:
|
||||
-h, --help help for atest
|
||||
-v, --version version for atest
|
||||
|
||||
Use "atest [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
below is an example of the usage, and you could see the report as well:
|
||||
|
||||
`atest run -p sample/testsuite-gitlab.yaml --duration 1m --thread 3 --report md`
|
||||
|
||||
| API | Average | Max | Min | Count | Error |
|
||||
|---|---|---|---|---|---|
|
||||
| GET https://gitlab.com/api/v4/projects | 1.152777167s | 2.108680194s | 814.928496ms | 99 | 0 |
|
||||
| GET https://gitlab.com/api/v4/projects/45088772 | 840.761064ms | 1.487285371s | 492.583066ms | 10 | 0 |
|
||||
consume: 1m2.153686448s
|
||||
|
||||
## Use in Docker
|
||||
|
||||
Use `atest` as server mode in Docker, then you could visit the UI from `8080`:
|
||||
|
||||
```bash
|
||||
docker run --pull always -p 8080:8080 ghcr.io/linuxsuren/api-testing:master
|
||||
```
|
||||
|
||||
Use `atest-collector` in Docker:
|
||||
|
||||
```bash
|
||||
docker run -p 1234:8080 -v /var/tmp:/var/tmp \
|
||||
ghcr.io/linuxsuren/api-testing atest-collector \
|
||||
--filter-path /api \
|
||||
-o /var/tmp/sample.yaml
|
||||
# you could find the test cases file from /var/tmp/sample
|
||||
# cat /var/tmp/sample
|
||||
```
|
||||
|
||||
## Template
|
||||
|
||||
The following fields are templated with [sprig](https://masterminds.github.io/sprig/):
|
||||
|
||||
* API
|
||||
* Request Body
|
||||
* Request Header
|
||||
|
||||
### Functions
|
||||
|
||||
You could use all the common functions which comes from [sprig](https://masterminds.github.io/sprig/). Besides some specific functions are available:
|
||||
|
||||
| Name | Usage |
|
||||
|---|---|
|
||||
| `randomKubernetesName` | `{{randomKubernetesName}}` to generate Kubernetes resource name randomly, the name will have 8 chars |
|
||||
| `sleep` | `{{sleep(1)}}` in the pre and post request handle |
|
||||
|
||||
## Verify against Kubernetes
|
||||
|
||||
It could verify any kinds of Kubernetes resources. Please set the environment variables before using it:
|
||||
|
||||
* `KUBERNETES_SERVER`
|
||||
* `KUBERNETES_TOKEN`
|
||||
|
||||
See also the [example](sample/kubernetes.yaml).
|
||||
|
||||
## TODO
|
||||
|
||||
* Reduce the size of context.
|
||||
* Support customized context.
|
||||
|
||||
## Limit
|
||||
|
||||
* Only support to parse the response body when it's a map or array.
|
||||
|
||||
## Community Exchange
|
||||
|
||||
Feel free to talk to us about any questions you may have about API Testing in the following ways.
|
||||
|
||||
### Mailing List
|
||||
|
||||
`api-testing-tech@googlegroups.com`, Feel free to discuss everything related to API Testing via this mailing list.
|
||||
|
||||
### `GitHub` discussion
|
||||
|
||||
[GitHub Discussion](https://github.com/LinuxSuRen/api-testing/discussions/new/choose)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
## Reporting Security Issues
|
||||
|
||||
The API Testing commnity takes a rigorous standpoint in annihilating the security issues in its software projects. API Testing is highly sensitive and forthcoming to issues pertaining to its features and functionality.
|
||||
|
||||
## REPORTING VULNERABILITY
|
||||
|
||||
If you have apprehensions regarding API Testing's security or you discover vulnerability or potential threat, don’t hesitate to get in touch with the api-testing Security Team by dropping a mail at [api-testing-security@googlegroups.com](mailto:api-testing-security@googlegroups.com). In the mail, specify the description of the issue or potential threat. You are also urged to recommend the way to reproduce and replicate the issue. The API Testing community will get back to you after assessing and analysing the findings.
|
||||
|
||||
PLEASE PAY ATTENTION to report the security issue on the security email before disclosing it on public domain.
|
||||
|
||||
## VULNERABILITY HANDLING
|
||||
|
||||
An overview of the vulnerability handling process is:
|
||||
|
||||
The reporter reports the vulnerability privately to API Testing community.
|
||||
The appropriate project's security team works privately with the reporter to resolve the vulnerability.
|
||||
A new release of the API Testing product concerned is made that includes the fix.
|
||||
The vulnerability is publically announced.
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023 API Testing Authors.
|
||||
Copyright 2023-2025 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,6 +21,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -36,7 +37,8 @@ func TestConvert(t *testing.T) {
|
|||
c.SetOut(io.Discard)
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
tmpFile := path.Join(os.TempDir(), time.Now().String())
|
||||
now := strconv.Itoa(int(time.Now().Unix()))
|
||||
tmpFile := path.Join(os.TempDir(), now)
|
||||
defer os.RemoveAll(tmpFile)
|
||||
|
||||
c.SetArgs([]string{"convert", "-p=testdata/simple-suite.yaml", "--converter=jmeter", "--target", tmpFile})
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/downloader"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type extensionOption struct {
|
||||
ociDownloader downloader.PlatformAwareOCIDownloader
|
||||
output string
|
||||
registry string
|
||||
kind string
|
||||
tag string
|
||||
os string
|
||||
arch string
|
||||
timeout time.Duration
|
||||
imagePrefix string
|
||||
}
|
||||
|
||||
func createExtensionCommand(ociDownloader downloader.PlatformAwareOCIDownloader) (c *cobra.Command) {
|
||||
opt := &extensionOption{
|
||||
ociDownloader: ociDownloader,
|
||||
}
|
||||
c = &cobra.Command{
|
||||
Use: "extension",
|
||||
Short: "Download extension binary files",
|
||||
Long: "Download the store extension files",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: opt.runE,
|
||||
}
|
||||
flags := c.Flags()
|
||||
flags.StringVarP(&opt.output, "output", "", ".", "The target directory")
|
||||
flags.StringVarP(&opt.tag, "tag", "", "", "The extension image tag, try to find the latest one if this is empty")
|
||||
flags.StringVarP(&opt.registry, "registry", "", "", "The target extension image registry, supported: docker.io, ghcr.io")
|
||||
flags.StringVarP(&opt.kind, "kind", "", "store", "The extension kind")
|
||||
flags.StringVarP(&opt.os, "os", "", runtime.GOOS, "The OS")
|
||||
flags.StringVarP(&opt.arch, "arch", "", runtime.GOARCH, "The architecture")
|
||||
flags.DurationVarP(&opt.timeout, "timeout", "", time.Minute, "The timeout of downloading")
|
||||
flags.StringVarP(&opt.imagePrefix, "image-prefix", "", "linuxsuren", "The prefix for the image address")
|
||||
return
|
||||
}
|
||||
|
||||
func (o *extensionOption) runE(cmd *cobra.Command, args []string) (err error) {
|
||||
o.ociDownloader.WithOS(o.os)
|
||||
o.ociDownloader.WithArch(o.arch)
|
||||
o.ociDownloader.WithRegistry(o.registry)
|
||||
o.ociDownloader.WithImagePrefix(o.imagePrefix)
|
||||
o.ociDownloader.WithTimeout(o.timeout)
|
||||
o.ociDownloader.WithKind(o.kind)
|
||||
o.ociDownloader.WithContext(cmd.Context())
|
||||
|
||||
for _, arg := range args {
|
||||
var reader io.Reader
|
||||
if reader, err = o.ociDownloader.Download(arg, o.tag, ""); err != nil {
|
||||
return
|
||||
} else if reader == nil {
|
||||
err = fmt.Errorf("cannot find %s", arg)
|
||||
return
|
||||
}
|
||||
extFile := o.ociDownloader.GetTargetFile()
|
||||
cmd.Println("found target file", extFile)
|
||||
|
||||
targetFile := filepath.Base(extFile)
|
||||
if err = downloader.WriteTo(reader, o.output, targetFile); err == nil {
|
||||
cmd.Println("downloaded", targetFile)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/downloader"
|
||||
"github.com/linuxsuren/api-testing/pkg/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExtensionCmd(t *testing.T) {
|
||||
t.Run("minimum one arg", func(t *testing.T) {
|
||||
command := createExtensionCommand(nil)
|
||||
command.SetOut(io.Discard)
|
||||
err := command.Execute()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
d := downloader.NewStoreDownloader()
|
||||
server := mock.NewInMemoryServer(context.Background(), 0)
|
||||
|
||||
err := server.Start(mock.NewLocalFileReader("../pkg/downloader/testdata/registry.yaml"), "/v2")
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
server.Stop()
|
||||
}()
|
||||
|
||||
registry := fmt.Sprintf("127.0.0.1:%s", server.GetPort())
|
||||
d.WithRegistry(registry)
|
||||
d.WithInsecure(true)
|
||||
d.WithBasicAuth("", "")
|
||||
d.WithOS("linux")
|
||||
d.WithArch("amd64")
|
||||
d.WithRoundTripper(nil)
|
||||
|
||||
var tmpDownloadDir string
|
||||
tmpDownloadDir, err = os.MkdirTemp(os.TempDir(), "download")
|
||||
defer os.RemoveAll(tmpDownloadDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
command := createExtensionCommand(d)
|
||||
command.SetOut(io.Discard)
|
||||
command.SetArgs([]string{"git", "--output", tmpDownloadDir, "--registry", registry})
|
||||
err = command.Execute()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// not found
|
||||
command.SetArgs([]string{"orm", "--output", tmpDownloadDir, "--registry", registry})
|
||||
err = command.Execute()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright 2024-2025 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/mock"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type mockOption struct {
|
||||
port int
|
||||
prefix string
|
||||
metrics bool
|
||||
}
|
||||
|
||||
func createMockCmd() (c *cobra.Command) {
|
||||
opt := &mockOption{}
|
||||
|
||||
c = &cobra.Command{
|
||||
Use: "mock",
|
||||
Short: "Start a mock server",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: opt.runE,
|
||||
}
|
||||
|
||||
flags := c.Flags()
|
||||
flags.IntVarP(&opt.port, "port", "", 6060, "The mock server port")
|
||||
flags.StringVarP(&opt.prefix, "prefix", "", "/mock", "The mock server API prefix")
|
||||
flags.BoolVarP(&opt.metrics, "metrics", "m", true, "Enable request metrics collection")
|
||||
return
|
||||
}
|
||||
|
||||
func (o *mockOption) runE(c *cobra.Command, args []string) (err error) {
|
||||
reader := mock.NewLocalFileReader(args[0])
|
||||
server := mock.NewInMemoryServer(c.Context(), o.port)
|
||||
if o.metrics {
|
||||
server.EnableMetrics()
|
||||
}
|
||||
if err = server.Start(reader, o.prefix); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
clean := make(chan os.Signal, 1)
|
||||
signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||
printLocalIPs(c, o.port)
|
||||
if o.metrics {
|
||||
c.Printf("Metrics available at http://localhost:%d%s/metrics\n", o.port, o.prefix)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.Context().Done():
|
||||
case <-clean:
|
||||
}
|
||||
err = server.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
func printLocalIPs(c *cobra.Command, port int) {
|
||||
if ips, err := getLocalIPs(); err == nil {
|
||||
for _, ip := range ips {
|
||||
c.Printf("server is available at http://%s:%d\n", ip, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIPs() ([]string, error) {
|
||||
var ips []string
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get interface addresses: %v", err)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok {
|
||||
if ipNet.IP.To4() != nil && !ipNet.IP.IsLoopback() {
|
||||
ips = append(ips, ipNet.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips, nil
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2025 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/linuxsuren/api-testing/pkg/server"
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMockCommand(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
args []string
|
||||
verify func(t *testing.T, err error)
|
||||
}{
|
||||
{
|
||||
name: "mock",
|
||||
args: []string{"mock"},
|
||||
verify: func(t *testing.T, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mock with file",
|
||||
args: []string{"mock", "testdata/stores.yaml", "--port=0"},
|
||||
verify: func(t *testing.T, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, server.NewFakeHTTPServer())
|
||||
root.SetOut(io.Discard)
|
||||
root.SetArgs(tc.args)
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
root.SetContext(ctx)
|
||||
go func() {
|
||||
time.Sleep(time.Second * 2)
|
||||
cancel()
|
||||
}()
|
||||
err := root.Execute()
|
||||
tc.verify(t, err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ package cmd
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/downloader"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/server"
|
||||
"github.com/linuxsuren/api-testing/pkg/version"
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
|
@ -20,7 +22,8 @@ func NewRootCmd(execer fakeruntime.Execer, httpServer server.HTTPServer) (c *cob
|
|||
c.AddCommand(createInitCommand(execer),
|
||||
createRunCommand(), createSampleCmd(),
|
||||
createServerCmd(execer, httpServer), createJSONSchemaCmd(),
|
||||
createServiceCommand(execer), createFunctionCmd(), createConvertCommand())
|
||||
createServiceCommand(execer), createFunctionCmd(), createConvertCommand(),
|
||||
createMockCmd(), createExtensionCommand(downloader.NewStoreDownloader()))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
77
cmd/run.go
77
cmd/run.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023 API Testing Authors.
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -21,7 +21,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -29,12 +29,16 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/util/home"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/apispec"
|
||||
"github.com/linuxsuren/api-testing/pkg/limit"
|
||||
"github.com/linuxsuren/api-testing/pkg/logging"
|
||||
"github.com/linuxsuren/api-testing/pkg/runner"
|
||||
"github.com/linuxsuren/api-testing/pkg/runner/monitor"
|
||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||
"github.com/linuxsuren/api-testing/pkg/util"
|
||||
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
@ -47,6 +51,7 @@ type runOption struct {
|
|||
duration time.Duration
|
||||
requestTimeout time.Duration
|
||||
requestIgnoreError bool
|
||||
caseFilter []string
|
||||
thread int64
|
||||
context context.Context
|
||||
qps int32
|
||||
|
@ -58,6 +63,8 @@ type runOption struct {
|
|||
reportWriter runner.ReportResultWriter
|
||||
report string
|
||||
reportIgnore bool
|
||||
reportTemplate string
|
||||
reportDest string
|
||||
swaggerURL string
|
||||
level string
|
||||
caseItems []string
|
||||
|
@ -68,6 +75,10 @@ type runOption struct {
|
|||
loader testing.Loader
|
||||
}
|
||||
|
||||
var (
|
||||
runLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("run")
|
||||
)
|
||||
|
||||
func newDefaultRunOption() *runOption {
|
||||
return &runOption{
|
||||
reporter: runner.NewMemoryTestReporter(nil, ""),
|
||||
|
@ -106,9 +117,12 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
|
|||
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
|
||||
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
|
||||
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
|
||||
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus")
|
||||
flags.StringArrayVarP(&opt.caseFilter, "case-filter", "", nil, "The filter of the test case")
|
||||
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http, grpc")
|
||||
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
|
||||
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
|
||||
flags.StringVarP(&opt.reportTemplate, "report-template", "", "", "The template used to render the report")
|
||||
flags.StringVarP(&opt.reportDest, "report-dest", "", "", "The server url where you want to send the report")
|
||||
flags.StringVarP(&opt.swaggerURL, "swagger-url", "", "", "The URL of swagger")
|
||||
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
|
||||
flags.Int32VarP(&opt.qps, "qps", "", 5, "QPS")
|
||||
|
@ -118,8 +132,14 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
|
|||
return
|
||||
}
|
||||
|
||||
const caseFilter = "case-filter"
|
||||
|
||||
func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
o.context = cmd.Context()
|
||||
ctx := cmd.Context()
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
o.context = context.WithValue(ctx, caseFilter, o.caseFilter)
|
||||
writer := cmd.OutOrStdout()
|
||||
|
||||
if o.reportFile != "" && !strings.HasPrefix(o.reportFile, "http://") && !strings.HasPrefix(o.reportFile, "https://") {
|
||||
|
@ -143,6 +163,10 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
case "", "std":
|
||||
o.reportWriter = runner.NewResultWriter(writer)
|
||||
case "pdf":
|
||||
if o.reportFile == "" {
|
||||
err = fmt.Errorf("report file is required for pdf report")
|
||||
return
|
||||
}
|
||||
o.reportWriter = runner.NewPDFResultWriter(writer)
|
||||
case "prometheus":
|
||||
if o.reportFile == "" {
|
||||
|
@ -153,15 +177,23 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
case "github":
|
||||
o.githubReportOption.ReportFile = o.reportFile
|
||||
o.reportWriter, err = runner.NewGithubPRCommentWriter(o.githubReportOption)
|
||||
case "http":
|
||||
templateOption := runner.NewTemplateOption(o.reportTemplate, "json")
|
||||
o.reportWriter = runner.NewHTTPResultWriter(http.MethodPost, o.reportDest, nil, templateOption)
|
||||
case "grpc":
|
||||
if o.reportDest == "" {
|
||||
err = fmt.Errorf("report gRPC server url is required for prometheus report")
|
||||
}
|
||||
o.reportWriter = runner.NewGRPCResultWriter(o.context, o.reportDest)
|
||||
default:
|
||||
err = fmt.Errorf("not supported report type: '%s'", o.report)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var swaggerAPI apispec.APIConverage
|
||||
var swaggerAPI apispec.SwaggerAPI
|
||||
if o.swaggerURL != "" {
|
||||
if swaggerAPI, err = apispec.ParseURLToSwagger(o.swaggerURL); err == nil {
|
||||
o.reportWriter.WithAPIConverage(swaggerAPI)
|
||||
if swaggerAPI.Swagger, err = apispec.ParseURLToSwagger(o.swaggerURL); err == nil {
|
||||
o.reportWriter.WithAPICoverage(&swaggerAPI)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -174,23 +206,25 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
const extensionMonitor = "atest-monitor-docker"
|
||||
|
||||
func (o *runOption) startMonitor() (err error) {
|
||||
if o.monitorDocker == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var monitorBin string
|
||||
if monitorBin, err = exec.LookPath("atest-monitor-docker"); err != nil {
|
||||
if monitorBin, err = exec.LookPath(extensionMonitor); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sockFile := os.ExpandEnv(fmt.Sprintf("$HOME/.config/atest/%s.sock", "atest-monitor-docker"))
|
||||
sockFile := home.GetExtensionSocketPath(extensionMonitor)
|
||||
os.MkdirAll(filepath.Dir(sockFile), 0755)
|
||||
|
||||
execer := fakeruntime.NewDefaultExecerWithContext(o.context)
|
||||
go func(socketURL, plugin string) {
|
||||
if err = execer.RunCommandWithIO(plugin, "", os.Stdout, os.Stderr, nil, "server", "--socket", socketURL); err != nil {
|
||||
log.Printf("failed to start %s, error: %v", socketURL, err)
|
||||
runLogger.Info("failed to start", "socketURL", socketURL, " error", err.Error())
|
||||
}
|
||||
}(sockFile, monitorBin)
|
||||
|
||||
|
@ -279,7 +313,7 @@ func (o *runOption) runSuiteWithDuration(loader testing.Loader) (err error) {
|
|||
defer sem.Release(1)
|
||||
defer wait.Done()
|
||||
defer func() {
|
||||
log.Println("routing end with", time.Since(now))
|
||||
runLogger.Info("routing end with", "time", time.Since(now))
|
||||
}()
|
||||
|
||||
dataContext := getDefaultContext()
|
||||
|
@ -311,6 +345,7 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter
|
|||
return
|
||||
}
|
||||
|
||||
runLogger.Info("render test suite", "name", testSuite.Name)
|
||||
if err = testSuite.Render(dataContext); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -322,7 +357,27 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter
|
|||
suiteRunner.WithOutputWriter(os.Stdout)
|
||||
suiteRunner.WithWriteLevel(o.level)
|
||||
suiteRunner.WithSuite(testSuite)
|
||||
var caseFilterObj interface{}
|
||||
if o.context != nil {
|
||||
caseFilterObj = o.context.Value(caseFilter)
|
||||
}
|
||||
runLogger.Info("run test suite", "name", testSuite.Name, "filter", caseFilter)
|
||||
for _, testCase := range testSuite.Items {
|
||||
if caseFilterObj != nil {
|
||||
if filter, ok := caseFilterObj.([]string); ok && len(filter) > 0 {
|
||||
match := false
|
||||
for _, ff := range filter {
|
||||
if strings.Contains(testCase.Name, ff) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if !testCase.InScope(o.caseItems) {
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -270,7 +270,7 @@ func TestPreRunE(t *testing.T) {
|
|||
func TestPrinter(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
c := &cobra.Command{}
|
||||
c.SetOutput(buf)
|
||||
c.SetOut(buf)
|
||||
|
||||
println(c, nil, "foo")
|
||||
assert.Empty(t, buf.String())
|
||||
|
|
206
cmd/server.go
206
cmd/server.go
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023 API Testing Authors.
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,7 +22,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"github.com/linuxsuren/api-testing/pkg/apispec"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -33,26 +33,42 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/runner"
|
||||
"github.com/linuxsuren/api-testing/pkg/util/home"
|
||||
|
||||
_ "embed"
|
||||
pprof "net/http/pprof"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/linuxsuren/api-testing/pkg/oauth"
|
||||
"github.com/linuxsuren/api-testing/pkg/downloader"
|
||||
"github.com/linuxsuren/api-testing/pkg/logging"
|
||||
"github.com/linuxsuren/api-testing/pkg/mock"
|
||||
atestoauth "github.com/linuxsuren/api-testing/pkg/oauth"
|
||||
template "github.com/linuxsuren/api-testing/pkg/render"
|
||||
"github.com/linuxsuren/api-testing/pkg/server"
|
||||
"github.com/linuxsuren/api-testing/pkg/service"
|
||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||
"github.com/linuxsuren/api-testing/pkg/testing/local"
|
||||
"github.com/linuxsuren/api-testing/pkg/testing/remote"
|
||||
"github.com/linuxsuren/api-testing/pkg/util"
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
"github.com/linuxsuren/oauth-hub"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
var (
|
||||
serverLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("server")
|
||||
)
|
||||
|
||||
func createServerCmd(execer fakeruntime.Execer, httpServer server.HTTPServer) (c *cobra.Command) {
|
||||
|
@ -71,8 +87,9 @@ func createServerCmd(execer fakeruntime.Execer, httpServer server.HTTPServer) (c
|
|||
flags.IntVarP(&opt.httpPort, "http-port", "", 8080, "The HTTP server port")
|
||||
flags.BoolVarP(&opt.printProto, "print-proto", "", false, "Print the proto content and exit")
|
||||
flags.StringArrayVarP(&opt.localStorage, "local-storage", "", []string{"*.yaml"}, "The local storage path")
|
||||
flags.IntVarP(&opt.grpcMaxRecvMsgSize, "grpc-max-recv-msg-size", "", 4*1024*1024, "The maximum received message size for gRPC clients")
|
||||
flags.StringVarP(&opt.consolePath, "console-path", "", "", "The path of the console")
|
||||
flags.StringVarP(&opt.configDir, "config-dir", "", os.ExpandEnv("$HOME/.config/atest"), "The config directory")
|
||||
flags.StringVarP(&opt.configDir, "config-dir", "", home.GetUserConfigDir(), "The config directory")
|
||||
flags.StringVarP(&opt.secretServer, "secret-server", "", "", "The secret server URL")
|
||||
flags.StringVarP(&opt.skyWalking, "skywalking", "", "", "Push the browser tracing data to the Apache SkyWalking HTTP URL")
|
||||
flags.StringVarP(&opt.auth, "auth", "", os.Getenv("AUTH_MODE"), "The auth mode, supported: oauth. Keep it empty to disable auth")
|
||||
|
@ -83,9 +100,17 @@ func createServerCmd(execer fakeruntime.Execer, httpServer server.HTTPServer) (c
|
|||
flags.StringVarP(&opt.clientID, "client-id", "", os.Getenv("OAUTH_CLIENT_ID"), "ClientID is the application's ID")
|
||||
flags.StringVarP(&opt.clientSecret, "client-secret", "", os.Getenv("OAUTH_CLIENT_SECRET"), "ClientSecret is the application's secret")
|
||||
flags.BoolVarP(&opt.dryRun, "dry-run", "", false, "Do not really start a gRPC server")
|
||||
flags.StringArrayVarP(&opt.mockConfig, "mock-config", "", nil, "The mock config files")
|
||||
flags.StringVarP(&opt.mockPrefix, "mock-prefix", "", "/mock", "The mock server API prefix")
|
||||
flags.StringVarP(&opt.extensionRegistry, "extension-registry", "", "docker.io", "The extension registry URL")
|
||||
flags.DurationVarP(&opt.downloadTimeout, "download-timeout", "", time.Minute, "The timeout of extension download")
|
||||
|
||||
// gc releated flags
|
||||
// gc related flags
|
||||
flags.IntVarP(&opt.gcPercent, "gc-percent", "", 100, "The GC percent of Go")
|
||||
//grpc_tls
|
||||
flags.BoolVarP(&opt.tls, "tls-grpc", "", false, "Enable TLS mode. Set to true to enable TLS. Alow SAN certificates")
|
||||
flags.StringVarP(&opt.tlsCert, "cert-file", "", "", "The path to the certificate file, Alow SAN certificates")
|
||||
flags.StringVarP(&opt.tlsKey, "key-file", "", "", "The path to the key file, Alow SAN certificates")
|
||||
|
||||
c.Flags().MarkHidden("dry-run")
|
||||
c.Flags().MarkHidden("gc-percent")
|
||||
|
@ -97,14 +122,16 @@ type serverOption struct {
|
|||
httpServer server.HTTPServer
|
||||
execer fakeruntime.Execer
|
||||
|
||||
port int
|
||||
httpPort int
|
||||
printProto bool
|
||||
localStorage []string
|
||||
consolePath string
|
||||
secretServer string
|
||||
configDir string
|
||||
skyWalking string
|
||||
port int
|
||||
httpPort int
|
||||
printProto bool
|
||||
localStorage []string
|
||||
consolePath string
|
||||
secretServer string
|
||||
configDir string
|
||||
skyWalking string
|
||||
extensionRegistry string
|
||||
downloadTimeout time.Duration
|
||||
|
||||
auth string
|
||||
oauthProvider string
|
||||
|
@ -116,12 +143,20 @@ type serverOption struct {
|
|||
oauthSkipTls bool
|
||||
oauthGroup []string
|
||||
|
||||
mockConfig []string
|
||||
mockPrefix string
|
||||
|
||||
gcPercent int
|
||||
|
||||
dryRun bool
|
||||
|
||||
grpcMaxRecvMsgSize int
|
||||
|
||||
// inner fields, not as command flags
|
||||
provider oauth.OAuthProvider
|
||||
tls bool
|
||||
tlsCert string
|
||||
tlsKey string
|
||||
}
|
||||
|
||||
func (o *serverOption) preRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
|
@ -151,9 +186,17 @@ func (o *serverOption) preRunE(cmd *cobra.Command, args []string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
grpcOpts = append(grpcOpts, oauth.NewAuthInterceptor(o.oauthGroup))
|
||||
grpcOpts = append(grpcOpts, atestoauth.NewAuthInterceptor(o.oauthGroup))
|
||||
}
|
||||
if o.tls {
|
||||
if o.tlsCert != "" && o.tlsKey != "" {
|
||||
creds, err := credentials.NewServerTLSFromFile(o.tlsCert, o.tlsKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load credentials: %v", err)
|
||||
}
|
||||
grpcOpts = append(grpcOpts, grpc.Creds(creds))
|
||||
}
|
||||
}
|
||||
|
||||
if o.dryRun {
|
||||
o.gRPCServer = &fakeGRPCServer{}
|
||||
} else {
|
||||
|
@ -204,23 +247,38 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
|
|||
if secretServer, err = remote.NewGRPCSecretFrom(o.secretServer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
template.SetSecretGetter(remote.NewGRPCSecretGetter(secretServer))
|
||||
} else {
|
||||
secretServer = local.NewLocalSecretService(o.configDir)
|
||||
}
|
||||
template.SetSecretGetter(remote.NewGRPCSecretGetter(secretServer))
|
||||
|
||||
extDownloader := downloader.NewStoreDownloader()
|
||||
extDownloader.WithRegistry(o.extensionRegistry)
|
||||
extDownloader.WithTimeout(o.downloadTimeout)
|
||||
storeExtMgr := server.NewStoreExtManager(o.execer)
|
||||
defer storeExtMgr.StopAll()
|
||||
|
||||
remoteServer := server.NewRemoteServer(loader, remote.NewGRPCloaderFromStore(), secretServer, storeExtMgr, o.configDir)
|
||||
storeExtMgr.WithDownloader(extDownloader)
|
||||
remoteServer := server.NewRemoteServer(loader, remote.NewGRPCloaderFromStore(), secretServer, storeExtMgr, o.configDir, o.grpcMaxRecvMsgSize)
|
||||
kinds, storeKindsErr := remoteServer.GetStoreKinds(ctx, nil)
|
||||
if storeKindsErr != nil {
|
||||
cmd.PrintErrf("failed to get store kinds, error: %p\n", storeKindsErr)
|
||||
cmd.PrintErrf("failed to get store kinds, error: %v\n", storeKindsErr)
|
||||
} else {
|
||||
if err = startPlugins(storeExtMgr, kinds); err != nil {
|
||||
return
|
||||
if runPluginErr := startPlugins(storeExtMgr, kinds); runPluginErr != nil {
|
||||
cmd.PrintErrf("error occurred during starting plugins, error: %v\n", runPluginErr)
|
||||
}
|
||||
}
|
||||
|
||||
// create mock server controller
|
||||
var mockWriter mock.ReaderAndWriter
|
||||
if len(o.mockConfig) > 0 {
|
||||
cmd.Println("currently only one mock config is supported, will take the first one")
|
||||
mockWriter = mock.NewLocalFileReader(o.mockConfig[0])
|
||||
} else {
|
||||
mockWriter = mock.NewInMemoryReader("")
|
||||
}
|
||||
|
||||
dynamicMockServer := mock.NewInMemoryServer(cmd.Context(), 0)
|
||||
mockServerController := server.NewMockServerController(mockWriter, dynamicMockServer, o.httpPort)
|
||||
|
||||
clean := make(chan os.Signal, 1)
|
||||
signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||
|
||||
|
@ -230,25 +288,71 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
|
|||
reflection.Register(gRPCServer)
|
||||
}
|
||||
server.RegisterRunnerServer(s, remoteServer)
|
||||
log.Printf("gRPC server listening at %v", lis.Addr())
|
||||
server.RegisterMockServer(s, mockServerController)
|
||||
server.RegisterDataServerServer(s, remoteServer.(server.DataServerServer))
|
||||
serverLogger.Info("gRPC server listening at", "addr", lis.Addr())
|
||||
s.Serve(lis)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-clean
|
||||
log.Println("stopping the server")
|
||||
serverLogger.Info("stopping the extensions")
|
||||
storeExtMgr.StopAll()
|
||||
serverLogger.Info("stopping the server")
|
||||
_ = lis.Close()
|
||||
_ = o.httpServer.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc))
|
||||
err = server.RegisterRunnerHandlerFromEndpoint(ctx, mux, "127.0.0.1:7070", []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())})
|
||||
go func() {
|
||||
err := apispec.DownloadSwaggerData("", extDownloader)
|
||||
if err != nil {
|
||||
fmt.Println("failed to download swagger data", err)
|
||||
} else {
|
||||
fmt.Println("success to download swagger data")
|
||||
}
|
||||
}()
|
||||
|
||||
mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc),
|
||||
runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
|
||||
MarshalOptions: protojson.MarshalOptions{
|
||||
Indent: " ",
|
||||
Multiline: true, // Optional, implied by presence of "Indent".
|
||||
},
|
||||
UnmarshalOptions: protojson.UnmarshalOptions{
|
||||
DiscardUnknown: true,
|
||||
},
|
||||
}))
|
||||
|
||||
gRPCServerPort := util.GetPort(lis)
|
||||
gRPCServerAddr := fmt.Sprintf("127.0.0.1:%s", gRPCServerPort)
|
||||
|
||||
if o.tls {
|
||||
creds, err := credentials.NewClientTLSFromFile(o.tlsCert, "localhost")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load credentials: %v", err)
|
||||
}
|
||||
err = errors.Join(
|
||||
server.RegisterRunnerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, []grpc.DialOption{grpc.WithTransportCredentials(creds)}),
|
||||
server.RegisterMockHandlerFromEndpoint(ctx, mux, gRPCServerAddr, []grpc.DialOption{grpc.WithTransportCredentials(creds)}),
|
||||
server.RegisterDataServerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, []grpc.DialOption{grpc.WithTransportCredentials(creds)}))
|
||||
} else {
|
||||
dialOption := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
|
||||
err = errors.Join(
|
||||
server.RegisterRunnerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
|
||||
server.RegisterMockHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
|
||||
server.RegisterDataServerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption))
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
mux.HandlePath(http.MethodGet, "/", frontEndHandlerWithLocation(o.consolePath))
|
||||
mux.HandlePath(http.MethodGet, "/assets/{asset}", frontEndHandlerWithLocation(o.consolePath))
|
||||
mux.HandlePath(http.MethodGet, "/healthz", frontEndHandlerWithLocation(o.consolePath))
|
||||
mux.HandlePath(http.MethodGet, "/favicon.ico", frontEndHandlerWithLocation(o.consolePath))
|
||||
mux.HandlePath(http.MethodGet, "/swagger.json", frontEndHandlerWithLocation(o.consolePath))
|
||||
mux.HandlePath(http.MethodGet, "/get", o.getAtestBinary)
|
||||
mux.HandlePath(http.MethodPost, "/runner/{suite}/{case}", service.WebRunnerHandler)
|
||||
mux.HandlePath(http.MethodGet, "/api/v1/sbom", service.SBomHandler)
|
||||
mux.HandlePath(http.MethodGet, "/api/v1/swaggers", apispec.SwaggersHandler)
|
||||
|
||||
postRequestProxyFunc := postRequestProxy(o.skyWalking)
|
||||
mux.HandlePath(http.MethodPost, "/browser/{app}", postRequestProxyFunc)
|
||||
|
@ -273,15 +377,32 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
|
|||
reg.MustRegister(
|
||||
collectors.NewGoCollector(),
|
||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||
collectors.NewBuildInfoCollector(),
|
||||
server.ExecutionCountNum, server.ExecutionSuccessNum, server.ExecutionFailNum,
|
||||
server.RequestCounter,
|
||||
runner.RunnersNum,
|
||||
)
|
||||
mux.HandlePath(http.MethodGet, "/metrics", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
|
||||
promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}).ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
combineHandlers := server.NewDefaultCombineHandler()
|
||||
combineHandlers.PutHandler("", mux)
|
||||
|
||||
if handler, hErr := dynamicMockServer.SetupHandler(mockWriter, o.mockPrefix+"/server"); hErr != nil {
|
||||
err = hErr
|
||||
return
|
||||
} else {
|
||||
combineHandlers.PutHandler(o.mockPrefix+"/server", handler)
|
||||
}
|
||||
|
||||
debugHandler(mux, remoteServer)
|
||||
o.httpServer.WithHandler(mux)
|
||||
log.Printf("HTTP server listening at %v", httplis.Addr())
|
||||
log.Printf("Server is running.")
|
||||
o.httpServer.WithHandler(combineHandlers.GetHandler())
|
||||
serverLogger.Info("HTTP server started", "addr", httplis.Addr())
|
||||
serverLogger.Info("gRPC server started", "addr", lis.Addr())
|
||||
serverLogger.Info("Server is running.")
|
||||
printLocalIPs(cmd, o.httpPort)
|
||||
|
||||
err = o.httpServer.Serve(httplis)
|
||||
err = util.IgnoreErrServerClosed(err)
|
||||
}
|
||||
|
@ -304,9 +425,7 @@ func startPlugins(storeExtMgr server.ExtManager, kinds *server.StoreKinds) (err
|
|||
|
||||
for _, kind := range kinds.Data {
|
||||
if kind.Enabled && strings.HasPrefix(kind.Url, socketPrefix) {
|
||||
if err = storeExtMgr.Start(kind.Name, kind.Url); err != nil {
|
||||
break
|
||||
}
|
||||
err = errors.Join(err, storeExtMgr.Start(kind.Name, kind.Url))
|
||||
}
|
||||
}
|
||||
return
|
||||
|
@ -320,9 +439,12 @@ func frontEndHandlerWithLocation(consolePath string) func(w http.ResponseWriter,
|
|||
} else if target == "/healthz" {
|
||||
w.Write([]byte("ok"))
|
||||
return
|
||||
} else if target == "/swagger.json" {
|
||||
w.Write(server.SwaggerJSON)
|
||||
return
|
||||
}
|
||||
|
||||
var content string
|
||||
var content []byte
|
||||
customHeader := map[string]string{}
|
||||
switch {
|
||||
case strings.HasSuffix(target, ".html"):
|
||||
|
@ -338,11 +460,11 @@ func frontEndHandlerWithLocation(consolePath string) func(w http.ResponseWriter,
|
|||
customHeader[util.ContentType] = "image/x-icon"
|
||||
}
|
||||
|
||||
if content != "" {
|
||||
if len(content) > 0 {
|
||||
for k, v := range customHeader {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader([]byte(content)))
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader(content))
|
||||
} else {
|
||||
http.ServeFile(w, r, path.Join(consolePath, target))
|
||||
}
|
||||
|
@ -354,7 +476,7 @@ func debugHandler(mux *runtime.ServeMux, remoteServer server.RunnerServer) {
|
|||
sub := pathParams["sub"]
|
||||
extName := r.URL.Query().Get("name")
|
||||
if extName != "" && remoteServer != nil {
|
||||
log.Println("get pprof of extension:", extName)
|
||||
serverLogger.Info("get pprof of extension", "name", extName)
|
||||
|
||||
ctx := metadata.NewIncomingContext(r.Context(), metadata.New(map[string]string{
|
||||
server.HeaderKeyStoreName: extName,
|
||||
|
@ -364,7 +486,7 @@ func debugHandler(mux *runtime.ServeMux, remoteServer server.RunnerServer) {
|
|||
Name: sub,
|
||||
})
|
||||
if err == nil {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set(util.ContentType, "application/octet-stream")
|
||||
w.Write(data.Data)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
@ -436,13 +558,13 @@ func (s *fakeGRPCServer) RegisterService(desc *grpc.ServiceDesc, impl interface{
|
|||
}
|
||||
|
||||
//go:embed data/index.js
|
||||
var uiResourceJS string
|
||||
var uiResourceJS []byte
|
||||
|
||||
//go:embed data/index.css
|
||||
var uiResourceCSS string
|
||||
var uiResourceCSS []byte
|
||||
|
||||
//go:embed data/index.html
|
||||
var uiResourceIndex string
|
||||
var uiResourceIndex []byte
|
||||
|
||||
//go:embed data/favicon.ico
|
||||
var uiResourceIcon string
|
||||
var uiResourceIcon []byte
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023 API Testing Authors.
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,18 +17,25 @@ package cmd
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/h2non/gock"
|
||||
"github.com/linuxsuren/api-testing/pkg/server"
|
||||
"github.com/linuxsuren/api-testing/pkg/util"
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -57,6 +64,18 @@ func TestPrintProto(t *testing.T) {
|
|||
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
|
||||
assert.Nil(t, err)
|
||||
},
|
||||
}, {
|
||||
name: "mock server, not found config",
|
||||
args: []string{"server", "--mock-config=fake", "-p=0", "--http-port=0"},
|
||||
verify: func(t *testing.T, buffer *bytes.Buffer, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
}, {
|
||||
name: "mock server, normal",
|
||||
args: []string{"server", "--mock-config=testdata/invalid-api.yaml", "-p=0", "--http-port=0"},
|
||||
verify: func(t *testing.T, buffer *bytes.Buffer, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -87,14 +106,14 @@ func TestFrontEndHandlerWithLocation(t *testing.T) {
|
|||
req, err := http.NewRequest(http.MethodGet, "/assets/index.js", nil)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
uiResourceJS = ""
|
||||
uiResourceJS = []byte("")
|
||||
}()
|
||||
|
||||
resp := newFakeResponseWriter()
|
||||
|
||||
uiResourceJS = "js"
|
||||
uiResourceJS = []byte("js")
|
||||
handler(resp, req, map[string]string{})
|
||||
assert.Equal(t, uiResourceJS, resp.GetBody().String())
|
||||
assert.Equal(t, uiResourceJS, resp.GetBody().Bytes())
|
||||
|
||||
assert.Equal(t, "text/javascript; charset=utf-8", resp.Header().Get(util.ContentType))
|
||||
})
|
||||
|
@ -155,8 +174,6 @@ func TestFrontEndHandlerWithLocation(t *testing.T) {
|
|||
defer listen.Close()
|
||||
|
||||
for _, name := range apis {
|
||||
// gock.Off()
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%s/debug/pprof/%s?seconds=1", port, name))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
@ -193,13 +210,13 @@ func TestFrontEndHandlerWithLocation(t *testing.T) {
|
|||
resp := newFakeResponseWriter()
|
||||
|
||||
opt.getAtestBinary(resp, req, map[string]string{})
|
||||
assert.Equal(t, `failed to read "atest": open : no such file or directory`, resp.GetBody().String())
|
||||
assert.Contains(t, resp.GetBody().String(), `failed to read "atest"`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
gock.Off()
|
||||
defer gock.Off()
|
||||
|
||||
gock.New("http://localhost:8080").Post("/api/v1/echo").Reply(http.StatusOK)
|
||||
gock.New("http://localhost:9090").Post("/api/v1/echo").Reply(http.StatusOK)
|
||||
|
@ -213,7 +230,7 @@ func TestProxy(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("no proxy", func(t *testing.T) {
|
||||
gock.Off()
|
||||
defer gock.Off()
|
||||
|
||||
gock.New("http://localhost:8080").Post("/api/v1/echo").Reply(http.StatusOK)
|
||||
|
||||
|
@ -246,7 +263,7 @@ func TestOAuth(t *testing.T) {
|
|||
hasErr: true,
|
||||
}, {
|
||||
name: "oauth is ok",
|
||||
args: []string{"server", "--auth=oauth", "--client-id=fake", "--client-secret=fake"},
|
||||
args: []string{"server", "--auth=oauth", "--client-id=fake", "--client-secret=fake", "--http-port=0"},
|
||||
hasErr: false,
|
||||
}}
|
||||
for i, tt := range tests {
|
||||
|
@ -263,6 +280,65 @@ func TestOAuth(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStartPlugins(t *testing.T) {
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "atest")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "stores.yaml"), []byte(sampleStores), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("dry-run", func(t *testing.T) {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "atest",
|
||||
}
|
||||
rootCmd.SetOut(io.Discard)
|
||||
rootCmd.AddCommand(createServerCmd(
|
||||
fakeruntime.FakeExecer{ExpectOS: "linux", ExpectLookPathError: errors.New("not-found")},
|
||||
server.NewFakeHTTPServer(),
|
||||
))
|
||||
|
||||
rootCmd.SetArgs([]string{"server", "--config-dir", dir, "--dry-run", "--port=0", "--http-port=0"})
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
httpServer := server.NewDefaultHTTPServer()
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "atest",
|
||||
}
|
||||
rootCmd.SetOut(io.Discard)
|
||||
rootCmd.AddCommand(createServerCmd(
|
||||
fakeruntime.FakeExecer{ExpectOS: "linux", ExpectLookPathError: errors.New("not-found")},
|
||||
httpServer,
|
||||
))
|
||||
|
||||
rootCmd.SetArgs([]string{"server", "--config-dir", dir, "--port=0", "--http-port=0"})
|
||||
go func() {
|
||||
err = rootCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
for httpServer.GetPort() == "" {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
httpServer.Shutdown(context.Background())
|
||||
}()
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%s/api/v1/suites", httpServer.GetPort()))
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, err = http.Get(fmt.Sprintf("http://localhost:%s/metrics", httpServer.GetPort()))
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type fakeResponseWriter struct {
|
||||
buf *bytes.Buffer
|
||||
header http.Header
|
||||
|
@ -287,3 +363,6 @@ func (w *fakeResponseWriter) WriteHeader(int) {
|
|||
func (w *fakeResponseWriter) GetBody() *bytes.Buffer {
|
||||
return w.buf
|
||||
}
|
||||
|
||||
//go:embed testdata/stores.yaml
|
||||
var sampleStores string
|
||||
|
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/linuxsuren/api-testing/pkg/util/home"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
@ -91,7 +92,7 @@ func (o *serviceOption) preRunE(c *cobra.Command, args []string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
local := os.ExpandEnv("$HOME/.config/atest")
|
||||
local := home.GetUserConfigDir()
|
||||
if err = o.Execer.MkdirAll(local, os.ModePerm); err == nil {
|
||||
err = o.Execer.MkdirAll(o.LocalStorage, os.ModePerm)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
stores:
|
||||
- name: git
|
||||
kind:
|
||||
name: atest-store-git
|
||||
enabled: true
|
||||
url: xxx
|
||||
readonly: false
|
||||
disabled: false
|
||||
plugins:
|
||||
- name: atest-store-git
|
||||
url: unix:///tmp/atest-store-git.sock
|
||||
enabled: true
|
|
@ -0,0 +1,37 @@
|
|||
```shell
|
||||
npx electron-forge import
|
||||
```
|
||||
|
||||
```shell
|
||||
npm config set registry https://registry.npmmirror.com
|
||||
```
|
||||
|
||||
## Package
|
||||
|
||||
```shell
|
||||
npm run package -- --platform=darwin
|
||||
npm run package -- --platform=win32
|
||||
npm run package -- --platform=linux
|
||||
```
|
||||
|
||||
## For Linux
|
||||
|
||||
You need to install tools if you want to package Windows on Linux:
|
||||
```shell
|
||||
apt install wine64 zip -y
|
||||
```
|
||||
|
||||
## For Windows
|
||||
|
||||
```powershell
|
||||
dotnet tool install --global wix
|
||||
```
|
||||
|
||||
## Publish
|
||||
|
||||
export GITHUB_TOKEN=your-token
|
||||
|
||||
```shell
|
||||
npm run publish -- --platform=darwin
|
||||
npm run publish -- --platform=linux
|
||||
```
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('node:path')
|
||||
|
||||
exports.control = function(okCallback, errorCallback) {
|
||||
fetch(getHealthzUrl()).
|
||||
then(okCallback).catch(errorCallback)
|
||||
}
|
||||
|
||||
function getPort() {
|
||||
// TODO support set this value
|
||||
return 7788
|
||||
}
|
||||
|
||||
function getHomePage() {
|
||||
return 'http://localhost:' + getPort()
|
||||
}
|
||||
|
||||
function getHealthzUrl() {
|
||||
return 'http://localhost:' + getPort() + '/healthz'
|
||||
}
|
||||
|
||||
function getHomeDir() {
|
||||
const homedir = require('os').homedir();
|
||||
return path.join(homedir, ".config", 'atest')
|
||||
}
|
||||
|
||||
function getLogfile() {
|
||||
return path.join(getHomeDir(), 'log.log')
|
||||
}
|
||||
|
||||
exports.getPort = getPort
|
||||
exports.getHomePage = getHomePage
|
||||
exports.getHomeDir = getHomeDir
|
||||
exports.getLogfile = getLogfile
|
||||
exports.getHealthzUrl = getHealthzUrl
|
|
@ -0,0 +1,22 @@
|
|||
const { getPort, getHealthzUrl, getHomePage } = require('./api');
|
||||
|
||||
describe('getPort function', () => {
|
||||
test('should return the default port number 7788', () => {
|
||||
const port = getPort();
|
||||
expect(port).toBe(7788);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHealthzUrl function', () => {
|
||||
test('should return the default healthz url', () => {
|
||||
const url = getHealthzUrl();
|
||||
expect(url).toBe('http://localhost:7788/healthz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHomePage function', () => {
|
||||
test('should return the default home page url', () => {
|
||||
const url = getHomePage();
|
||||
expect(url).toBe('http://localhost:7788');
|
||||
});
|
||||
})
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
const path = require('node:path');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
icon: path.join(__dirname, 'api-testing.ico'),
|
||||
asar: true
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {
|
||||
icon: 'api-testing.ico'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
config: {
|
||||
options: {
|
||||
icon: 'api-testing.ico'
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-rpm',
|
||||
config: {
|
||||
icon: 'api-testing.ico'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
icon: path.join(__dirname, 'api-testing.icns')
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-wix',
|
||||
config: {
|
||||
language: 1033,
|
||||
manufacturer: 'API Testing Authors',
|
||||
icon: 'api-testing.ico',
|
||||
ui: {
|
||||
"enabled": true,
|
||||
"chooseDirectory": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
||||
config: {},
|
||||
},
|
||||
// Fuses are used to enable/disable various Electron functionality
|
||||
// at package time, before code signing the application
|
||||
new FusesPlugin({
|
||||
version: FuseVersion.V1,
|
||||
[FuseV1Options.RunAsNode]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
}),
|
||||
],
|
||||
publishers: [
|
||||
{
|
||||
name: '@electron-forge/publisher-github',
|
||||
config: {
|
||||
repository: {
|
||||
owner: 'linuxsuren',
|
||||
name: 'api-testing'
|
||||
},
|
||||
prerelease: true,
|
||||
force: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
<!--index.html-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * self blob: data: gap:; style-src * self 'unsafe-inline' blob: data: gap:; script-src * 'self' 'unsafe-eval' 'unsafe-inline' blob: data: gap:; object-src * 'self' blob: data: gap:; img-src * self 'unsafe-inline' blob: data: gap:; connect-src self * 'unsafe-inline' blob: data: gap:; frame-src * self blob: data: gap:;">
|
||||
<title>API Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div style="margin: 5px">
|
||||
<div>
|
||||
<div>Server Status</div>
|
||||
<button type="button" id="action">Start</button>
|
||||
<button type="button" id="open-server-page">Open Server Page</button>
|
||||
<div>
|
||||
<span>Port:</span><input name="port" id="port" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>Log</div>
|
||||
<button type="button" id="open-log-file">Open Log File</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const actionBut = document.getElementById('action');
|
||||
actionBut.addEventListener('click', (e) => {
|
||||
const action = actionBut.innerHTML;
|
||||
switch (action) {
|
||||
case 'Stop':
|
||||
window.electronAPI.stopServer()
|
||||
break;
|
||||
case 'Start':
|
||||
window.electronAPI.startServer()
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
const openServerBut = document.getElementById('open-server-page');
|
||||
openServerBut.addEventListener('click', async (e) => {
|
||||
window.location = await window.electronAPI.getHomePage()
|
||||
})
|
||||
|
||||
const openLogfileBut = document.getElementById('open-log-file')
|
||||
openLogfileBut.addEventListener('click', () => {
|
||||
window.electronAPI.openLogDir()
|
||||
})
|
||||
|
||||
const loadServerStatus = async () => {
|
||||
const healthzUrl = await window.electronAPI.getHealthzUrl()
|
||||
|
||||
fetch(healthzUrl).then(res => {
|
||||
actionBut.innerHTML = 'Stop';
|
||||
}).catch(err => {
|
||||
actionBut.innerHTML = 'Start';
|
||||
})
|
||||
}
|
||||
|
||||
loadServerStatus()
|
||||
window.setInterval(loadServerStatus, 2000)
|
||||
|
||||
const portInput = document.getElementById('port');
|
||||
(async function() {
|
||||
portInput.value = await window.electronAPI.getPort()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Modules to control application life and create native browser window
|
||||
const { app, shell, BrowserWindow, Menu, MenuItem, ipcMain, contextBridge } = require('electron')
|
||||
const log = require('electron-log/main');
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const server = require('./api')
|
||||
const spawn = require("child_process").spawn;
|
||||
const atestHome = server.getHomeDir()
|
||||
const storage = require('electron-json-storage')
|
||||
|
||||
// setup log output
|
||||
log.initialize();
|
||||
|
||||
log.transports.file.level = getLogLevel()
|
||||
log.transports.file.resolvePathFn = () => server.getLogfile()
|
||||
if (process.platform === 'darwin'){
|
||||
app.dock.setIcon(path.join(__dirname, "api-testing.png"))
|
||||
}
|
||||
|
||||
const windowOptions = {
|
||||
width: 1024,
|
||||
height: 600,
|
||||
frame: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
enableRemoteModule: true
|
||||
},
|
||||
icon: path.join(__dirname, '/api-testing.ico'),
|
||||
}
|
||||
|
||||
const createWindow = () => {
|
||||
var width = storage.getSync('window.width')
|
||||
if (!isNaN(width)) {
|
||||
windowOptions.width = width
|
||||
}
|
||||
var height = storage.getSync('window.height')
|
||||
if (!isNaN(height)) {
|
||||
windowOptions.height = height
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow(windowOptions)
|
||||
|
||||
if (!isNaN(serverProcess.pid)) {
|
||||
// server process started by app
|
||||
mainWindow.loadURL(server.getHomePage())
|
||||
} else {
|
||||
server.control(() => {
|
||||
mainWindow.loadURL(server.getHomePage())
|
||||
}, () => {
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile('index.html')
|
||||
})
|
||||
}
|
||||
|
||||
mainWindow.on('resize', () => {
|
||||
const size = mainWindow.getSize();
|
||||
storage.set('window.width', size[0])
|
||||
storage.set('window.height', size[1])
|
||||
})
|
||||
}
|
||||
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({
|
||||
label: 'Window',
|
||||
submenu: [{
|
||||
label: 'Console',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Cmd+C' : 'Alt+Shift+C',
|
||||
click: () => {
|
||||
BrowserWindow.getFocusedWindow().loadFile('index.html');
|
||||
}
|
||||
}, {
|
||||
label: 'Server',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Cmd+S' : 'Alt+Shift+S',
|
||||
click: () => {
|
||||
BrowserWindow.getFocusedWindow().loadURL(server.getHomePage());
|
||||
}
|
||||
}, {
|
||||
label: 'Reload',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+R' : 'F5',
|
||||
click: () => {
|
||||
BrowserWindow.getFocusedWindow().reload()
|
||||
}
|
||||
}, {
|
||||
label: 'Developer Mode',
|
||||
accelerator: process.platform === 'darwin' ? 'Alt+Cmd+D' : 'F12',
|
||||
click: () => {
|
||||
BrowserWindow.getFocusedWindow().webContents.openDevTools();
|
||||
}
|
||||
}, {
|
||||
label: 'Quit',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+Shift+Q',
|
||||
click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}]
|
||||
}))
|
||||
|
||||
Menu.setApplicationMenu(menu)
|
||||
|
||||
let serverProcess;
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
ipcMain.on('openLogDir', () => {
|
||||
shell.openExternal('file://' + server.getLogfile())
|
||||
})
|
||||
ipcMain.on('startServer', startServer)
|
||||
ipcMain.on('stopServer', stopServer)
|
||||
ipcMain.on('control', (e, okCallback, errCallback) => {
|
||||
server.control(okCallback, errCallback)
|
||||
})
|
||||
ipcMain.handle('getHomePage', server.getHomePage)
|
||||
ipcMain.handle('getPort', () => {
|
||||
return server.getPort()
|
||||
})
|
||||
ipcMain.handle('getHealthzUrl', server.getHealthzUrl)
|
||||
|
||||
startServer()
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
const startServer = () => {
|
||||
const homeData = path.join(atestHome, 'data')
|
||||
const homeBin = path.join(atestHome, 'bin')
|
||||
|
||||
fs.mkdirSync(homeData, {
|
||||
recursive: true
|
||||
})
|
||||
fs.mkdirSync(homeBin, {
|
||||
recursive: true
|
||||
})
|
||||
|
||||
// try to find the atest file first
|
||||
const serverFile = process.platform === "win32" ? "atest.exe" : "atest"
|
||||
const atestFromHome = path.join(homeBin, serverFile)
|
||||
const atestFromPkg = path.join(__dirname, serverFile)
|
||||
|
||||
const data = fs.readFileSync(atestFromPkg)
|
||||
log.info('start to write file with length', data.length)
|
||||
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
const file = fs.openSync(atestFromHome, 'w');
|
||||
fs.writeSync(file, data, 0, data.length, 0);
|
||||
fs.closeSync(file);
|
||||
}else{
|
||||
fs.writeFileSync(atestFromHome, data);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Error Code:', e.code);
|
||||
}
|
||||
fs.chmodSync(atestFromHome, 0o755);
|
||||
|
||||
serverProcess = spawn(atestFromHome, [
|
||||
"server",
|
||||
"--http-port", server.getPort(),
|
||||
"--port=0",
|
||||
"--local-storage", path.join(homeData, "*.yaml")
|
||||
])
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
log.info(data.toString())
|
||||
if (data.toString().indexOf('Server is running') != -1) {
|
||||
BrowserWindow.getFocusedWindow().loadURL(server.getHomePage())
|
||||
}
|
||||
})
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
log.error(data.toString())
|
||||
})
|
||||
serverProcess.on('close', (code) => {
|
||||
log.log(`child process exited with code ${code}`);
|
||||
})
|
||||
log.info('start atest server as pid:', serverProcess.pid)
|
||||
log.info(serverProcess.spawnargs)
|
||||
}
|
||||
|
||||
const stopServer = () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill()
|
||||
}
|
||||
}
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
|
||||
stopServer()
|
||||
}
|
||||
})
|
||||
app.on('before-quit', stopServer)
|
||||
|
||||
function getLogLevel() {
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "atest-desktop",
|
||||
"version": "0.0.1",
|
||||
"description": "API Testing Desktop Application",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"test": "jest --coverage",
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish"
|
||||
},
|
||||
"author": "linuxsuren",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.4.0",
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-dmg": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.0",
|
||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||
"@electron-forge/maker-wix": "^7.4.0",
|
||||
"@electron-forge/maker-zip": "^7.4.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
||||
"@electron-forge/publisher-github": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"electron": "^30.0.4",
|
||||
"electron-wix-msi": "^5.1.3",
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"child_process": "^1.0.2",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
"electron-log": "^5.1.4",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^9.0.0"
|
||||
},
|
||||
"build": {
|
||||
"extraResources": [
|
||||
"./assets/*"
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"appdmg": "^0.6.6"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
// All the Node.js APIs are available in the preload process.
|
||||
// It has the same sandbox as a Chrome extension.
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const replaceText = (selector, text) => {
|
||||
const element = document.getElementById(selector)
|
||||
if (element) element.innerText = text
|
||||
}
|
||||
|
||||
for (const dependency of ['chrome', 'node', 'electron']) {
|
||||
replaceText(`${dependency}-version`, process.versions[dependency])
|
||||
}
|
||||
})
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openLogDir: () => ipcRenderer.send('openLogDir'),
|
||||
startServer: () => ipcRenderer.send('startServer'),
|
||||
stopServer: () => ipcRenderer.send('stopServer'),
|
||||
control: (okCallback, errCallback) => ipcRenderer.send('control', okCallback, errCallback),
|
||||
getHomePage: () => ipcRenderer.invoke('getHomePage'),
|
||||
getPort: () => ipcRenderer.invoke('getPort'),
|
||||
getHealthzUrl: () => ipcRenderer.invoke('getHealthzUrl'),
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
"version": "v0.0.14",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "jest --collect-coverage",
|
||||
|
@ -16,12 +16,16 @@
|
|||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-plus": "^2.3.7",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"codemirror": "^5.65.17",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"element-plus": "^2.9.6",
|
||||
"intro.js": "^7.0.1",
|
||||
"jsonpath-plus": "^7.2.0",
|
||||
"jsonlint-mod": "^1.7.6",
|
||||
"jsonpath-plus": "^10.0.7",
|
||||
"skywalking-client-js": "^0.10.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-codemirror": "^6.1.1",
|
||||
"vue-codemirror": "^5.1.0",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-json-viewer": "^3.0.4",
|
||||
"vue-router": "^4.2.2"
|
||||
|
@ -37,25 +41,24 @@
|
|||
"@types/node-fetch": "^2.6.4",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^13.1.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"eslint-plugin-vue": "^9.11.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"grpc_tools_node_protoc_ts": "^5.3.3",
|
||||
"grpc-tools": "^1.12.4",
|
||||
"jest": "^29.6.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier": "^3.4.2",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "~5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vite": "^4.5.5",
|
||||
"vitest": "^0.32.4",
|
||||
"vue-tsc": "^1.6.5"
|
||||
},
|
||||
|
|
|
@ -2,25 +2,32 @@
|
|||
import {
|
||||
Document,
|
||||
Menu as IconMenu,
|
||||
Histogram,
|
||||
Location,
|
||||
Share,
|
||||
ArrowDown,
|
||||
Guide,
|
||||
DataAnalysis
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { API } from './views/net'
|
||||
import { Cache } from './views/cache'
|
||||
import TestingPanel from './views/TestingPanel.vue'
|
||||
import TestingHistoryPanel from './views/TestingHistoryPanel.vue'
|
||||
import MockManager from './views/MockManager.vue'
|
||||
import StoreManager from './views/StoreManager.vue'
|
||||
import SecretManager from './views/SecretManager.vue'
|
||||
import WelcomePage from './views/WelcomePage.vue'
|
||||
import DataManager from './views/DataManager.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale: i18nLocale } = useI18n()
|
||||
|
||||
import setAsDarkTheme from './theme'
|
||||
|
||||
const asDarkMode = ref(Cache.GetPreference().darkTheme)
|
||||
setAsDarkTheme(asDarkMode.value)
|
||||
watch(asDarkMode, Cache.WatchDarkTheme)
|
||||
watch(asDarkMode, Cache.WithDarkTheme)
|
||||
watch(asDarkMode, () => {
|
||||
setAsDarkTheme(asDarkMode.value)
|
||||
})
|
||||
|
@ -28,9 +35,9 @@ watch(asDarkMode, () => {
|
|||
const appVersion = ref('')
|
||||
const appVersionLink = ref('https://github.com/LinuxSuRen/api-testing')
|
||||
API.GetVersion((d) => {
|
||||
appVersion.value = d.message
|
||||
const version = d.message.match('^v\\d*.\\d*.\\d*')
|
||||
const dirtyVersion = d.message.match('^v\\d*.\\d*.\\d*-\\d*-g')
|
||||
appVersion.value = d.version
|
||||
const version = d.version.match('^v\\d*.\\d*.\\d*')
|
||||
const dirtyVersion = d.version.match('^v\\d*.\\d*.\\d*-\\d*-g')
|
||||
|
||||
if (!version && !dirtyVersion) {
|
||||
return
|
||||
|
@ -43,31 +50,53 @@ API.GetVersion((d) => {
|
|||
}
|
||||
})
|
||||
|
||||
const panelName = ref('')
|
||||
const sideWidth = ref("width: 200px; display: flex;flex-direction: column;")
|
||||
const isCollapse = ref(false)
|
||||
watch(isCollapse, (e) => {
|
||||
if (e) {
|
||||
sideWidth.value = "width: 80px; display: flex;flex-direction: column;"
|
||||
} else {
|
||||
sideWidth.value = "width: 200px; display: flex;flex-direction: column;"
|
||||
}
|
||||
})
|
||||
const isCollapse = ref(true)
|
||||
const lastActiveMenu = window.localStorage.getItem('activeMenu')
|
||||
const activeMenu = ref(lastActiveMenu === '' ? 'welcome' : lastActiveMenu)
|
||||
const panelName = ref(activeMenu)
|
||||
const handleSelect = (key: string) => {
|
||||
panelName.value = key
|
||||
window.localStorage.setItem('activeMenu', key)
|
||||
}
|
||||
|
||||
const locale = ref(Cache.GetPreference().language)
|
||||
i18nLocale.value = locale.value
|
||||
|
||||
watch(locale, (e: string) =>{
|
||||
Cache.WithLocale(e)
|
||||
i18nLocale.value = locale
|
||||
})
|
||||
|
||||
const handleChangeLan = (command: string) => {
|
||||
switch (command) {
|
||||
case "chinese":
|
||||
locale.value = "zh-CN"
|
||||
break;
|
||||
case "english":
|
||||
locale.value = "en-US"
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const ID = ref(null);
|
||||
const toHistoryPanel = ({ ID: selectID, panelName: historyPanelName }) => {
|
||||
ID.value = selectID;
|
||||
panelName.value = historyPanelName;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container style="height: 100%">
|
||||
<el-aside :style="sideWidth">
|
||||
<el-container style="height: 100%;">
|
||||
<el-aside width="auto" style="display: flex; flex-direction: column;">
|
||||
<el-radio-group v-model="isCollapse">
|
||||
<el-radio-button :label="false">+</el-radio-button>
|
||||
<el-radio-button :label="true">-</el-radio-button>
|
||||
<el-radio-button :value="false">+</el-radio-button>
|
||||
<el-radio-button :value="true">-</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-menu
|
||||
style="flex-grow: 1;"
|
||||
default-active="welcome"
|
||||
class="el-menu-vertical"
|
||||
style="height: 100%;"
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
@select="handleSelect"
|
||||
>
|
||||
|
@ -79,6 +108,18 @@ const handleSelect = (key: string) => {
|
|||
<el-icon><icon-menu /></el-icon>
|
||||
<template #title>{{ t('title.testing' )}}</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="history" test-id="history-menu">
|
||||
<el-icon><histogram /></el-icon>
|
||||
<template #title>{{ t('title.history' )}}</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="mock" test-id="mock-menu">
|
||||
<el-icon><Guide /></el-icon>
|
||||
<template #title>{{ t('title.mock' )}}</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="data" test-id="data-menu">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<template #title>{{ t('title.data' )}}</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="secret">
|
||||
<el-icon><document /></el-icon>
|
||||
<template #title>{{ t('title.secrets') }}</template>
|
||||
|
@ -91,7 +132,24 @@ const handleSelect = (key: string) => {
|
|||
</el-aside>
|
||||
|
||||
<el-main style="padding-top: 5px; padding-bottom: 5px;">
|
||||
<TestingPanel v-if="panelName === 'testing'" />
|
||||
<div style="position: absolute; top: 10px; right: 20px;">
|
||||
<el-col style="display: flex; align-items: center;">
|
||||
<el-tag style="font-size: 18px;">{{ t('language') }}</el-tag>
|
||||
<el-dropdown trigger="click" @command="(command: string) => handleChangeLan(command)">
|
||||
<el-icon><arrow-down /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="chinese">中文</el-dropdown-item>
|
||||
<el-dropdown-item command="english">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</div>
|
||||
<TestingPanel v-if="panelName === 'testing'" @toHistoryPanel="toHistoryPanel"/>
|
||||
<TestingHistoryPanel v-else-if="panelName === 'history'" :ID="ID"/>
|
||||
<DataManager v-else-if="panelName === 'data'" />
|
||||
<MockManager v-else-if="panelName === 'mock'" />
|
||||
<StoreManager v-else-if="panelName === 'store'" />
|
||||
<SecretManager v-else-if="panelName === 'secret'" />
|
||||
<WelcomePage v-else />
|
||||
|
@ -102,3 +160,9 @@ const handleSelect = (key: string) => {
|
|||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.el-menu-vertical:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import type { InputInstance } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
value: String || Number,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['changed'])
|
||||
const inputVisible = ref(false)
|
||||
const inputValue = ref('')
|
||||
const InputRef = ref<InputInstance>()
|
||||
|
||||
const showInput = () => {
|
||||
inputVisible.value = true
|
||||
inputValue.value = props.value ?? ''
|
||||
nextTick(() => {
|
||||
InputRef.value!.input!.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue.value && props.value !== inputValue.value) {
|
||||
emit('changed', inputValue.value)
|
||||
}
|
||||
inputVisible.value = false
|
||||
inputValue.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="flex gap-2">
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="InputRef"
|
||||
v-model="inputValue"
|
||||
class="w-20"
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleInputConfirm"
|
||||
@blur="handleInputConfirm"
|
||||
/>
|
||||
<el-button v-else class="button-new-tag" size="small" @click="showInput">
|
||||
{{ value }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<el-autocomplete
|
||||
v-model="input"
|
||||
clearable
|
||||
:fetch-suggestions="querySearch"
|
||||
@select="handleSelect"
|
||||
@keyup.enter="handleEnter"
|
||||
:placeholder="props.placeholder"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>{{ item.value }}</span>
|
||||
<el-icon @click.stop="deleteHistoryItem(item)">
|
||||
<delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps } from 'vue'
|
||||
import { ElAutocomplete, ElIcon } from 'element-plus'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
maxItems: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: 'history'
|
||||
},
|
||||
storage: {
|
||||
type: String,
|
||||
default: 'localStorage'
|
||||
},
|
||||
callback: {
|
||||
type: Function,
|
||||
default: () => true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Type something'
|
||||
}
|
||||
})
|
||||
|
||||
const input = ref('')
|
||||
const suggestions = ref([])
|
||||
interface HistoryItem {
|
||||
value: string
|
||||
count: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const querySearch = (queryString: string, cb: any) => {
|
||||
const results = suggestions.value.filter((item: HistoryItem) => item.value.includes(queryString))
|
||||
cb(results)
|
||||
}
|
||||
|
||||
const handleSelect = (item: HistoryItem) => {
|
||||
input.value = item.value
|
||||
}
|
||||
|
||||
const handleEnter = async () => {
|
||||
if (props.callback) {
|
||||
const result = await props.callback()
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (input.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const history = JSON.parse(getStorage().getItem(props.group) || '[]')
|
||||
const existingItem = history.find((item: HistoryItem) => item.value === input.value)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.count++
|
||||
existingItem.timestamp = Date.now()
|
||||
} else {
|
||||
history.push({ value: input.value, count: 1, timestamp: Date.now() })
|
||||
}
|
||||
|
||||
if (history.length > props.maxItems) {
|
||||
history.sort((a: HistoryItem, b: HistoryItem) => a.count - b.count || a.timestamp - b.timestamp)
|
||||
history.shift()
|
||||
}
|
||||
|
||||
getStorage().setItem(props.group, JSON.stringify(history))
|
||||
suggestions.value = history
|
||||
}
|
||||
|
||||
const loadHistory = () => {
|
||||
suggestions.value = JSON.parse(getStorage().getItem(props.group) || '[]')
|
||||
}
|
||||
|
||||
const deleteHistoryItem = (item: HistoryItem) => {
|
||||
const history = JSON.parse(getStorage().getItem(props.group) || '[]')
|
||||
const updatedHistory = history.filter((historyItem: HistoryItem) => historyItem.value !== item.value)
|
||||
getStorage().setItem(props.group, JSON.stringify(updatedHistory))
|
||||
suggestions.value = updatedHistory
|
||||
}
|
||||
|
||||
const getStorage = () => {
|
||||
switch (props.storage) {
|
||||
case 'localStorage':
|
||||
return localStorage
|
||||
case 'sessionStorage':
|
||||
return sessionStorage
|
||||
default:
|
||||
return localStorage
|
||||
}
|
||||
}
|
||||
|
||||
loadHistory()
|
||||
</script>
|
|
@ -0,0 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { API } from '@/views/net'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const deviceAuthActive = ref(0)
|
||||
const deviceAuthResponse = ref({
|
||||
user_code: '',
|
||||
verification_uri: '',
|
||||
device_code: ''
|
||||
})
|
||||
const deviceAuthNext = () => {
|
||||
if (deviceAuthActive.value++ > 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (deviceAuthActive.value === 1) {
|
||||
fetch('/oauth2/getLocalCode')
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then((d) => {
|
||||
deviceAuthResponse.value = d
|
||||
})
|
||||
} else if (deviceAuthActive.value === 2) {
|
||||
window.location.href = '/oauth2/getUserInfoFromLocalCode?device_code=' + deviceAuthResponse.value.device_code
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:modelValue="visible"
|
||||
title="You need to login first."
|
||||
width="30%"
|
||||
>
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="Server in cloud" name="1">
|
||||
<a href="/oauth2/token" target="_blank">
|
||||
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle color-fg-default">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="Server in local" name="2">
|
||||
<el-steps :active="deviceAuthActive" finish-status="success">
|
||||
<el-step title="Request Device Code" />
|
||||
<el-step title="Input Code"/>
|
||||
<el-step title="Finished" />
|
||||
</el-steps>
|
||||
|
||||
<div v-if="deviceAuthActive===1">
|
||||
Open <a :href="deviceAuthResponse.verification_uri" target="_blank">this link</a>, and type the code: <span>{{ deviceAuthResponse.user_code }}. Then click the next step button.</span>
|
||||
</div>
|
||||
<el-button style="margin-top: 12px" @click="deviceAuthNext">Next step</el-button>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-dialog>
|
||||
</template>
|
|
@ -0,0 +1,135 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { Suite } from '@/views/types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { API } from '@/views/net'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Store {
|
||||
name: string,
|
||||
description: string,
|
||||
}
|
||||
|
||||
const emit = defineEmits(['created'])
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const stores = ref([] as Store[])
|
||||
function loadStores() {
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
'X-Auth': API.getToken()
|
||||
}
|
||||
}
|
||||
fetch('/api/v1/stores', requestOptions)
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then(async (d) => {
|
||||
stores.value = d.data
|
||||
})
|
||||
}
|
||||
loadStores()
|
||||
|
||||
const suiteCreatingLoading = ref(false)
|
||||
const suiteFormRef = ref<FormInstance>()
|
||||
const testSuiteForm = reactive({
|
||||
name: '',
|
||||
api: '',
|
||||
store: '',
|
||||
kind: ''
|
||||
})
|
||||
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
|
||||
await formEl.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
suiteCreatingLoading.value = true
|
||||
|
||||
API.CreateTestSuite(testSuiteForm, (e) => {
|
||||
suiteCreatingLoading.value = false
|
||||
if (e.error !== "") {
|
||||
ElMessage.error('Oops, ' + e.error)
|
||||
} else {
|
||||
formEl.resetFields()
|
||||
}
|
||||
|
||||
emit('created')
|
||||
}, (e) => {
|
||||
suiteCreatingLoading.value = false
|
||||
ElMessage.error('Oops, ' + e)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const suiteKinds = [{
|
||||
"name": "HTTP",
|
||||
}, {
|
||||
"name": "gRPC",
|
||||
}, {
|
||||
"name": "tRPC",
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :modelValue="visible" :title="t('title.createTestSuite')" width="30%" draggable>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-form
|
||||
:rules="rules"
|
||||
:model="testSuiteForm"
|
||||
ref="suiteFormRef"
|
||||
status-icon label-width="120px">
|
||||
<el-form-item :label="t('field.storageLocation')" prop="store">
|
||||
<el-select v-model="testSuiteForm.store" class="m-2"
|
||||
test-id="suite-form-store"
|
||||
filterable
|
||||
default-first-option
|
||||
placeholder="Storage Location">
|
||||
<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="t('field.suiteKind')" prop="kind">
|
||||
<el-select v-model="testSuiteForm.kind" class="m-2"
|
||||
filterable
|
||||
test-id="suite-form-kind"
|
||||
default-first-option>
|
||||
<el-option
|
||||
v-for="item in suiteKinds"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('field.name')" prop="name">
|
||||
<el-input v-model="testSuiteForm.name" test-id="suite-form-name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API" prop="api">
|
||||
<el-input v-model="testSuiteForm.api" placeholder="http://foo" test-id="suite-form-api" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(suiteFormRef)"
|
||||
:loading="suiteCreatingLoading"
|
||||
test-id="suite-form-submit"
|
||||
>{{ t('button.submit') }}</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
|
@ -0,0 +1,132 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { Suite } from '@/views/types'
|
||||
import { API } from '@/views/net'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits(['created'])
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const importSuiteFormRef = ref<FormInstance>()
|
||||
const importSuiteForm = reactive({
|
||||
url: '',
|
||||
store: '',
|
||||
kind: ''
|
||||
})
|
||||
|
||||
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' }],
|
||||
kind: [{ required: true, message: 'Kind is required', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const importSuiteFormSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
API.ImportTestSuite(importSuiteForm, () => {
|
||||
emit('created')
|
||||
formEl.resetFields()
|
||||
}, (e) => {
|
||||
ElMessage.error(e)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface Store {
|
||||
name: string,
|
||||
description: string,
|
||||
}
|
||||
const stores = ref([] as Store[])
|
||||
function loadStores() {
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
'X-Auth': API.getToken()
|
||||
}
|
||||
}
|
||||
fetch('/api/v1/stores', requestOptions)
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then(async (d) => {
|
||||
stores.value = d.data
|
||||
})
|
||||
}
|
||||
loadStores()
|
||||
|
||||
const importSourceKinds = [{
|
||||
"name": "Postman",
|
||||
"value": "postman",
|
||||
"description": "https://api.postman.com/collections/xxx"
|
||||
}, {
|
||||
"name": "Native",
|
||||
"value": "native",
|
||||
"description": "http://your-server/api/v1/suites/xxx/yaml?x-store-name=xxx"
|
||||
}]
|
||||
const placeholderOfImportURL = ref("")
|
||||
const kindChanged = (e) => {
|
||||
importSourceKinds.forEach(k => {
|
||||
if (k.value === e) {
|
||||
placeholderOfImportURL.value = k.description
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :modelValue="visible" title="Import Test Suite" width="40%"
|
||||
draggable destroy-on-close>
|
||||
<el-form
|
||||
:rules="importSuiteFormRules"
|
||||
:model="importSuiteForm"
|
||||
ref="importSuiteFormRef"
|
||||
status-icon label-width="85px">
|
||||
<el-form-item label="Location" prop="store">
|
||||
<el-select v-model="importSuiteForm.store" class="m-2"
|
||||
test-id="suite-import-form-store"
|
||||
filterable
|
||||
default-first-option
|
||||
placeholder="Storage Location">
|
||||
<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="Kind" prop="kind">
|
||||
<el-select v-model="importSuiteForm.kind" class="m-2"
|
||||
filterable
|
||||
@change="kindChanged"
|
||||
test-id="suite-import-form-kind"
|
||||
default-first-option
|
||||
placeholder="Kind">
|
||||
<el-option
|
||||
v-for="item in importSourceKinds"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="URL" prop="url">
|
||||
<el-input v-model="importSuiteForm.url" test-id="suite-import-form-api" :placeholder="placeholderOfImportURL" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="importSuiteFormSubmit(importSuiteFormRef)"
|
||||
test-id="suite-import-submit"
|
||||
>{{ t('button.import') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"language": "English",
|
||||
"button": {
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
|
@ -9,12 +10,23 @@
|
|||
"delete": "Delete",
|
||||
"send": "Send",
|
||||
"copy": "Copy",
|
||||
"ok": "OK",
|
||||
"reload": "Reload",
|
||||
"insertSample": "Insert Sample",
|
||||
"toolbox": "Tool Box",
|
||||
"refresh": "Refresh",
|
||||
"newtestcase": "New TestCase",
|
||||
"viewYaml":"View YAML",
|
||||
"verify": "Verify",
|
||||
"duplicate": "Duplicate",
|
||||
"generateCode": "Generate Code",
|
||||
"sendWithParam": "Send With Parameter"
|
||||
"sendWithParam": "Send With Parameter",
|
||||
"fullScreen": "Full Screen",
|
||||
"cancelFullScreen": "Cancel Full Screen",
|
||||
"viewHistory": "View History Test Case Result",
|
||||
"revert": "Revert",
|
||||
"goToHistory": "Go to History",
|
||||
"deleteAllHistory": "Delete All History"
|
||||
},
|
||||
"title": {
|
||||
"createTestSuite": "Create Test Suite",
|
||||
|
@ -30,17 +42,23 @@
|
|||
"apiRequestParameter": "API Request Parameter",
|
||||
"codeGenerator": "Code Generator",
|
||||
"testing": "Testing",
|
||||
"history": "Testing History",
|
||||
"mock": "Mock",
|
||||
"welcome": "Welcome",
|
||||
"secrets": "Secrets",
|
||||
"stores": "Stores",
|
||||
"templateQuery": "Template Functions Query",
|
||||
"output": "Output"
|
||||
"functionQuery": "Functions Query",
|
||||
"output": "Output",
|
||||
"proxy": "Proxy",
|
||||
"secure": "Secure",
|
||||
"data": "Data"
|
||||
},
|
||||
"tip": {
|
||||
"filter": "Filter Keyword",
|
||||
"noParameter": "No Parameter",
|
||||
"testsuite": "Test Suite:",
|
||||
"apiAddress": "API Address:"
|
||||
"apiAddress": "API Address:",
|
||||
"runningAt": "Running At:"
|
||||
},
|
||||
"field": {
|
||||
"name": "Name",
|
||||
|
@ -53,10 +71,17 @@
|
|||
"disabled": "Disabled",
|
||||
"status": "Status",
|
||||
"operations": "Operations",
|
||||
"storageLocation": "Storage Location",
|
||||
"storageLocation": "Location",
|
||||
"suiteKind": "Suite Kind",
|
||||
"key": "Key",
|
||||
"value": "Value"
|
||||
"value": "Value",
|
||||
"proxy": "Proxy",
|
||||
"insecure": "Insecure"
|
||||
},
|
||||
"proxy": {
|
||||
"http": "HTTP Proxy",
|
||||
"https": "HTTPS Proxy",
|
||||
"no": "No Proxy"
|
||||
},
|
||||
"//see http spec": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
|
||||
"httpCode": {
|
||||
|
@ -65,12 +90,31 @@
|
|||
"204": "204 No Content",
|
||||
"400": "400 Bad Request",
|
||||
"401": "401 Unauthorized",
|
||||
"402": "402 Payment Required",
|
||||
"403": "403 Forbidden",
|
||||
"404": "404 Not Found",
|
||||
"405": "405 Method Not Allowed",
|
||||
"409": "409 Conflict",
|
||||
"413": "413 Content Too Large",
|
||||
"415": "415 Unsupported Media Type",
|
||||
"422": "422 Unprocessable Content",
|
||||
"500": "500 Internal Server Error",
|
||||
"502": "502 Bad Gateway",
|
||||
"503": "503 Service Unavailable"
|
||||
"503": "503 Service Unavailable",
|
||||
"301": "301 Moved Permanently",
|
||||
"302": "302 Found",
|
||||
"304": "304 Not Modified",
|
||||
"406": "406 Not Acceptable",
|
||||
"407": "407 Proxy Authentication Required",
|
||||
"408": "408 Request Timeout",
|
||||
"410": "410 Gone",
|
||||
"411": "411 Length Required",
|
||||
"412": "412 Precondition Failed",
|
||||
"414": "414 URI Too Long",
|
||||
"416": "416 Range Not Satisfiable",
|
||||
"417": "417 Expectation Failed",
|
||||
"429": "429 Too Many Requests",
|
||||
"501": "501 Not Implemented",
|
||||
"504": "504 Gateway Timeout"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"language": "中文",
|
||||
"button": {
|
||||
"import": "导入",
|
||||
"export": "导出",
|
||||
|
@ -9,12 +10,23 @@
|
|||
"delete": "删除",
|
||||
"send": "发送",
|
||||
"copy": "拷贝",
|
||||
"ok": "确定",
|
||||
"reload": "重新加载",
|
||||
"insertSample": "插入样例",
|
||||
"toolbox": "工具箱",
|
||||
"refresh": "刷新",
|
||||
"newtestcase": "新建测试用例",
|
||||
"viewYaml": "查看 YAML",
|
||||
"verify": "检查",
|
||||
"duplicate": "复制",
|
||||
"generateCode": "生成代码",
|
||||
"sendWithParam": "参数化"
|
||||
"sendWithParam": "参数化",
|
||||
"fullScreen": "全屏显示",
|
||||
"cancelFullScreen": "取消全屏",
|
||||
"viewHistory": "查看历史记录",
|
||||
"revert": "回退",
|
||||
"goToHistory": "跳转历史记录",
|
||||
"deleteAllHistory": "删除所有历史记录"
|
||||
},
|
||||
"title": {
|
||||
"createTestSuite": "创建测试用例集",
|
||||
|
@ -25,18 +37,23 @@
|
|||
"apiRequestParameter": "API 请求参数",
|
||||
"codeGenerator": "代码生成",
|
||||
"testing": "测试",
|
||||
"history": "测试历史",
|
||||
"welcome": "欢迎",
|
||||
"secrets": "凭据",
|
||||
"stores": "存储",
|
||||
"parameter": "参数",
|
||||
"templateQuery": "模板函数查询",
|
||||
"output": "输出"
|
||||
"functionQuery": "函数查询",
|
||||
"output": "输出",
|
||||
"proxy": "代理",
|
||||
"secure": "安全",
|
||||
"data": "数据"
|
||||
},
|
||||
"tip": {
|
||||
"filter": "过滤",
|
||||
"noParameter": "无参数",
|
||||
"testsuite": "测试集:",
|
||||
"apiAddress": "API 地址:"
|
||||
"apiAddress": "API 地址:",
|
||||
"runningAt": "运行于:"
|
||||
},
|
||||
"field": {
|
||||
"name": "名称",
|
||||
|
@ -51,6 +68,46 @@
|
|||
"storageLocation": "保存位置",
|
||||
"suiteKind": "类型",
|
||||
"key": "键",
|
||||
"value": "值"
|
||||
"value": "值",
|
||||
"proxy": "代理",
|
||||
"insecure": "忽略证书验证"
|
||||
},
|
||||
"proxy": {
|
||||
"http": "HTTP 代理",
|
||||
"https": "HTTPS 代理",
|
||||
"no": "跳过代理"
|
||||
},
|
||||
"httpCode": {
|
||||
"200": "200 成功",
|
||||
"201": "201 已创建",
|
||||
"204": "204 无内容",
|
||||
"400": "400 错误的请求",
|
||||
"401": "401 未授权",
|
||||
"402": "402 需要支付",
|
||||
"403": "403 禁止访问",
|
||||
"404": "404 未找到",
|
||||
"405": "405 方法不被允许",
|
||||
"409": "409 冲突",
|
||||
"413": "413 请求实体过大",
|
||||
"415": "415 不支持的媒体类型",
|
||||
"422": "422 处理不了的实体",
|
||||
"500": "500 内部服务器错误",
|
||||
"502": "502 错误的网关",
|
||||
"503": "503 服务不可用",
|
||||
"301": "301 永久移动",
|
||||
"302": "302 临时移动",
|
||||
"304": "304 未修改",
|
||||
"406": "406 不接受",
|
||||
"407": "407 需要代理身份验证",
|
||||
"408": "408 请求超时",
|
||||
"410": "410 已删除",
|
||||
"411": "411 需要内容长度",
|
||||
"412": "412 前提条件失败",
|
||||
"414": "414 请求 URI 过长",
|
||||
"416": "416 范围不可满足",
|
||||
"417": "417 期望失败",
|
||||
"429": "429 请求过多",
|
||||
"501": "501 未实现",
|
||||
"504": "504 网关超时"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { API } from './net'
|
||||
import type { QueryObject } from './net'
|
||||
import type { Store } from './store'
|
||||
import type { Pair } from './types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Codemirror } from 'vue-codemirror'
|
||||
import HistoryInput from '../components/HistoryInput.vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { Refresh, Document } from '@element-plus/icons-vue'
|
||||
|
||||
const stores: Ref<Store[]> = ref([])
|
||||
const kind = ref('')
|
||||
const store = ref('')
|
||||
const query = ref({
|
||||
offset: 0,
|
||||
limit: 10
|
||||
} as QueryObject)
|
||||
const sqlQuery = ref('')
|
||||
const queryResult = ref([] as any[])
|
||||
const queryResultAsJSON = ref('')
|
||||
const columns = ref([] as string[])
|
||||
const queryTip = ref('')
|
||||
const loadingStores = ref(true)
|
||||
const dataFormat = ref('table')
|
||||
const dataFormatOptions = ['table', 'json']
|
||||
const queryDataMeta = ref({} as QueryDataMeta)
|
||||
|
||||
interface TreeItem {
|
||||
label: string
|
||||
}
|
||||
const tablesTree = ref([] as TreeItem[])
|
||||
watch(store, (s) => {
|
||||
kind.value = ''
|
||||
stores.value.forEach((e: Store) => {
|
||||
if (e.name === s) {
|
||||
kind.value = e.kind.name
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
switch (kind.value) {
|
||||
case 'atest-store-elasticsearch':
|
||||
case 'atest-store-etcd':
|
||||
sqlQuery.value = '*'
|
||||
break
|
||||
default:
|
||||
queryDataMeta.value.currentDatabase = ''
|
||||
sqlQuery.value = ''
|
||||
}
|
||||
|
||||
executeQuery()
|
||||
})
|
||||
|
||||
interface QueryDataMeta {
|
||||
databases: string[]
|
||||
tables: string[]
|
||||
currentDatabase: string
|
||||
duration: string
|
||||
labels: Pair[]
|
||||
}
|
||||
|
||||
interface QueryData {
|
||||
items: any[]
|
||||
data: any[]
|
||||
label: string
|
||||
meta: QueryDataMeta
|
||||
}
|
||||
|
||||
const queryDataFromTable = (data: QueryData) => {
|
||||
sqlQuery.value = `@selectTableLImit100_${data.label}`
|
||||
executeQuery()
|
||||
}
|
||||
const describeTable = (data: QueryData) => {
|
||||
switch (kind.value) {
|
||||
case 'atest-store-cassandra':
|
||||
sqlQuery.value = `@describeTable_${queryDataMeta.value.currentDatabase}:${data.label}`
|
||||
break
|
||||
break
|
||||
default:
|
||||
sqlQuery.value = `@describeTable_${data.label}`
|
||||
}
|
||||
executeQuery()
|
||||
}
|
||||
const queryTables = () => {
|
||||
switch (kind.value) {
|
||||
case 'atest-store-elasticsearch':
|
||||
if (sqlQuery.value === '') {
|
||||
sqlQuery.value = '*'
|
||||
}
|
||||
break
|
||||
default:
|
||||
sqlQuery.value = ``
|
||||
}
|
||||
executeQuery()
|
||||
}
|
||||
watch(kind, (k) => {
|
||||
switch (k) {
|
||||
case 'atest-store-orm':
|
||||
case 'atest-store-cassandra':
|
||||
case 'atest-store-iotdb':
|
||||
queryTip.value = 'Enter SQL query'
|
||||
executeQuery()
|
||||
break;
|
||||
case 'atest-store-etcd':
|
||||
case 'atest-store-redis':
|
||||
queryTip.value = 'Enter key'
|
||||
break;
|
||||
case 'atest-store-elasticsearch':
|
||||
queryTip.value = 'field:value OR field:other'
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
API.GetStores((data) => {
|
||||
stores.value = data.data
|
||||
}, (e) => {
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: e.message,
|
||||
type: 'error'
|
||||
});
|
||||
}, () => {
|
||||
loadingStores.value = false
|
||||
})
|
||||
|
||||
const ormDataHandler = (data: QueryData) => {
|
||||
const result = [] as any[]
|
||||
const cols = new Set<string>()
|
||||
|
||||
data.items.forEach(e => {
|
||||
const obj = {}
|
||||
e.data.forEach((item: Pair) => {
|
||||
obj[item.key] = item.value
|
||||
cols.add(item.key)
|
||||
})
|
||||
result.push(obj)
|
||||
})
|
||||
|
||||
data.meta.labels = data.meta.labels.filter((item) => {
|
||||
if (item.key === '_native_sql') {
|
||||
sqlQuery.value = item.value
|
||||
return false
|
||||
}
|
||||
return !item.key.startsWith('_')
|
||||
})
|
||||
|
||||
queryDataMeta.value = data.meta
|
||||
queryResult.value = result
|
||||
queryResultAsJSON.value = JSON.stringify(result, null, 2)
|
||||
columns.value = Array.from(cols).sort((a, b) => {
|
||||
if (a === 'id') return -1;
|
||||
if (b === 'id') return 1;
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
|
||||
tablesTree.value = []
|
||||
queryDataMeta.value.tables.forEach((i) => {
|
||||
tablesTree.value.push({
|
||||
label: i,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const keyValueDataHandler = (data: QueryData) => {
|
||||
queryResult.value = []
|
||||
data.data.forEach(e => {
|
||||
const obj = new Map<string, string>();
|
||||
obj.set('key', e.key)
|
||||
obj.set('value', e.value)
|
||||
queryResult.value.push(obj)
|
||||
|
||||
columns.value = ['key', 'value']
|
||||
})
|
||||
}
|
||||
|
||||
const executeQuery = async () => {
|
||||
return executeWithQuery(sqlQuery.value)
|
||||
}
|
||||
const executeWithQuery = async (sql: string) => {
|
||||
let success = false
|
||||
query.value.store = store.value
|
||||
query.value.key = queryDataMeta.value.currentDatabase
|
||||
query.value.sql = sql
|
||||
|
||||
try {
|
||||
const data = await API.DataQueryAsync(query.value);
|
||||
switch (kind.value) {
|
||||
case 'atest-store-orm':
|
||||
case 'atest-store-cassandra':
|
||||
case 'atest-store-iotdb':
|
||||
case 'atest-store-elasticsearch':
|
||||
ormDataHandler(data)
|
||||
success = true
|
||||
break;
|
||||
case 'atest-store-etcd':
|
||||
keyValueDataHandler(data)
|
||||
break;
|
||||
case 'atest-store-redis':
|
||||
keyValueDataHandler(data)
|
||||
break;
|
||||
default:
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: 'Unsupported store kind',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: e.message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
return success
|
||||
}
|
||||
const nextPage = () => {
|
||||
query.value.offset += query.value.limit
|
||||
executeQuery()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-container style="height: calc(100vh - 50px);">
|
||||
<el-aside v-if="kind === 'atest-store-orm' || kind === 'atest-store-iotdb' || kind === 'atest-store-cassandra' || kind === 'atest-store-elasticsearch'">
|
||||
<el-scrollbar>
|
||||
<el-select v-model="queryDataMeta.currentDatabase" placeholder="Select database"
|
||||
@change="queryTables" filterable>
|
||||
<template #header>
|
||||
<el-button type="primary" :icon="Refresh" @click="executeWithQuery('')"></el-button>
|
||||
</template>
|
||||
<el-option v-for="item in queryDataMeta.databases" :key="item" :label="item"
|
||||
:value="item"></el-option>
|
||||
</el-select>
|
||||
<el-tree :data="tablesTree" node-key="label" highlight-current
|
||||
draggable>
|
||||
<template #default="{node, data}">
|
||||
<span @click="queryDataFromTable(data)">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
<el-icon style="margin-left: 6px;" @click="describeTable(data)" v-if="kind === 'atest-store-orm' || kind === 'atest-store-cassandra'"><Document /></el-icon>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header style="height: auto">
|
||||
<el-form @submit.prevent="executeQuery">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="4">
|
||||
<el-form-item>
|
||||
<el-select v-model="store" placeholder="Select store" filterable
|
||||
:loading="loadingStores">
|
||||
<el-option v-for="item in stores" :key="item.name" :label="item.name"
|
||||
:value="item.name" :disabled="!item.ready"
|
||||
:kind="item.kind.name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item>
|
||||
<HistoryInput :placeholder="queryTip" :callback="executeQuery" v-model="sqlQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="executeQuery">Execute</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-select v-model="dataFormat" placeholder="Select data format">
|
||||
<el-option v-for="item in dataFormatOptions" :key="item" :label="item"
|
||||
:value="item"></el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" v-if="kind === 'atest-store-elasticsearch'">
|
||||
<el-col :span="10">
|
||||
<el-input type="number" v-model="query.offset">
|
||||
<template #prepend>Offset</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-input type="number" v-model="query.limit">
|
||||
<template #prepend>Limit</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button type="primary" @click="nextPage">Next</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-tag type="primary" v-if="queryResult.length > 0">{{ queryResult.length }} rows</el-tag>
|
||||
<el-tag type="primary" v-if="queryDataMeta.duration">{{ queryDataMeta.duration }}</el-tag>
|
||||
<el-tag type="primary" v-for="label in queryDataMeta.labels">{{ label.value }}</el-tag>
|
||||
</div>
|
||||
<el-table :data="queryResult" stripe v-if="dataFormat === 'table'">
|
||||
<el-table-column v-for="col in columns" :key="col" :prop="col" :label="col" sortable />
|
||||
</el-table>
|
||||
<Codemirror v-else-if="dataFormat === 'json'" v-model="queryResultAsJSON" />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,65 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { API } from './net';
|
||||
import {useI18n} from "vue-i18n";
|
||||
import EditButton from '../components/EditButton.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface MockConfig {
|
||||
Config: string
|
||||
Prefix: string
|
||||
Port: number
|
||||
}
|
||||
const mockConfig = ref({} as MockConfig);
|
||||
const link = ref('')
|
||||
API.GetMockConfig((d) => {
|
||||
mockConfig.value = d
|
||||
link.value = window.location.origin + d.Prefix + "/api.json"
|
||||
})
|
||||
const prefixChanged = (p: string) => {
|
||||
mockConfig.value.Prefix = p
|
||||
}
|
||||
const portChanged = (p: number) => {
|
||||
mockConfig.value.Port = p
|
||||
}
|
||||
const tabActive = ref('yaml')
|
||||
const insertSample = () => {
|
||||
mockConfig.value.Config = `objects:
|
||||
- name: projects
|
||||
initCount: 3
|
||||
sample: |
|
||||
{
|
||||
"name": "api-testing",
|
||||
"color": "{{ randEnum "blue" "read" "pink" }}"
|
||||
}
|
||||
items:
|
||||
- name: base64
|
||||
request:
|
||||
path: /v1/base64
|
||||
response:
|
||||
body: aGVsbG8=
|
||||
encoder: base64`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" @click="insertSample">{{t('button.insertSample')}}</el-button>
|
||||
<el-button type="warning" @click="API.ReloadMockServer(mockConfig)">{{t('button.reload')}}</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-link target="_blank" :href="link">{{ link }}</el-link> <!-- Noncompliant -->
|
||||
</div>
|
||||
<div>
|
||||
API Prefix:<EditButton :value="mockConfig.Prefix" @changed="prefixChanged"/>
|
||||
Port:<EditButton :value="mockConfig.Port" @changed="portChanged"/>
|
||||
</div>
|
||||
<div>
|
||||
<el-tabs v-model="tabActive">
|
||||
<el-tab-pane label="YAML" name="yaml">
|
||||
<Codemirror v-model="mockConfig.Config" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
|
@ -2,8 +2,9 @@
|
|||
import { ElMessage } from 'element-plus'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { Edit, Delete } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { API } from './net'
|
||||
import type { Secret } from './net'
|
||||
import { UIAPI } from './net-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
@ -17,11 +18,6 @@ const secret = ref({} as Secret)
|
|||
const createAction = ref(true)
|
||||
const secretForm = reactive(secret)
|
||||
|
||||
interface Secret {
|
||||
Name: string
|
||||
Value: string
|
||||
}
|
||||
|
||||
function loadSecrets() {
|
||||
API.GetSecrets((e) => {
|
||||
secrets.value = e.data
|
||||
|
@ -54,14 +50,11 @@ function addSecret() {
|
|||
createAction.value = true
|
||||
}
|
||||
|
||||
const rules = reactive<FormRules<Secret>>({
|
||||
Name: [{ required: true, message: 'Name is required', trigger: 'blur' }]
|
||||
})
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
UIAPI.CreateOrUpdateSecret(secret.value, createAction.value, () => {
|
||||
UIAPI.CreateOrUpdateSecret(secretForm.value, createAction.value, () => {
|
||||
loadSecrets()
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
|
@ -78,7 +71,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
<el-button type="primary" @click="addSecret" :icon="Edit">{{t('button.new')}}</el-button>
|
||||
</div>
|
||||
<el-table :data="secrets" style="width: 100%">
|
||||
<el-table-column :label="t('field.name')" width="180">
|
||||
<el-table-column :label="t('field.name')">
|
||||
<template #default="scope">
|
||||
<el-text class="mx-1">{{ scope.row.Name }}</el-text>
|
||||
</template>
|
||||
|
@ -101,7 +94,6 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-form
|
||||
:rules="rules"
|
||||
:model="secretForm"
|
||||
ref="secretFormRef"
|
||||
status-icon label-width="120px">
|
||||
|
@ -115,7 +107,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(secretFormRef)"
|
||||
:loading="creatingLoading"
|
||||
v-loading="creatingLoading"
|
||||
test-id="store-form-submit"
|
||||
>{{t('button.submit')}}</el-button
|
||||
>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { Edit, Delete, QuestionFilled, Help } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { Pair } from './types'
|
||||
import { API } from './net'
|
||||
import { UIAPI } from './net-vue'
|
||||
import { SupportedExtensions } from './store'
|
||||
import { SupportedExtensions, SupportedExtension } from './store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Magic } from './magicKeys'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
@ -23,7 +24,8 @@ const emptyStore = function() {
|
|||
},
|
||||
properties: [{
|
||||
key: '',
|
||||
value: ''
|
||||
value: '',
|
||||
description: '',
|
||||
}],
|
||||
disabled: false,
|
||||
readonly: false
|
||||
|
@ -64,6 +66,7 @@ function loadStores() {
|
|||
})
|
||||
}
|
||||
loadStores()
|
||||
Magic.Keys(loadStores, ['Alt+KeyR'])
|
||||
|
||||
function deleteStore(name: string) {
|
||||
API.DeleteStore(name, (e) => {
|
||||
|
@ -97,10 +100,6 @@ function setStoreForm(store: Store) {
|
|||
storeForm.disabled = store.disabled
|
||||
storeForm.readonly = store.readonly
|
||||
storeForm.properties = store.properties
|
||||
storeForm.properties.push({
|
||||
key: '',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
|
||||
function addStore() {
|
||||
|
@ -108,11 +107,11 @@ function addStore() {
|
|||
dialogVisible.value = true
|
||||
createAction.value = true
|
||||
}
|
||||
Magic.Keys(addStore, ['Alt+KeyN'])
|
||||
|
||||
const rules = reactive<FormRules<Store>>({
|
||||
name: [{ required: true, message: 'Name is required', trigger: 'blur' }],
|
||||
url: [{ required: true, message: 'URL is required', trigger: 'blur' }],
|
||||
"kind.name": [{ required: true, message: 'Plugin is required', trigger: 'blur' }]
|
||||
url: [{ required: true, message: 'URL is required', trigger: 'blur' }]
|
||||
})
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
@ -127,6 +126,46 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
})
|
||||
}
|
||||
|
||||
watch(() => storeForm.kind.name, (name) => {
|
||||
const ext = SupportedExtension(name)
|
||||
if (ext) {
|
||||
storeExtLink.value = ext.link
|
||||
let pro = storeForm.properties.slice()
|
||||
|
||||
for (var i = 0; i < pro.length;) {
|
||||
// remove it if the value or key is empty
|
||||
if (pro[i].key === '' || pro[i].value === '') {
|
||||
pro.splice(i, 1)
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// add extension related params
|
||||
ext.params.forEach(p => {
|
||||
const index = pro.findIndex(e => e.key === p.key)
|
||||
if (index === -1) {
|
||||
pro.push({
|
||||
key: p.key,
|
||||
value: '',
|
||||
defaultValue: p.defaultValue,
|
||||
description: p.description,
|
||||
type: p.type,
|
||||
enum: p.enum
|
||||
} as Pair)
|
||||
} else {
|
||||
pro[index].description = p.description
|
||||
}
|
||||
})
|
||||
|
||||
// make sure there is always a empty pair for letting users input
|
||||
pro.push({
|
||||
key: '',
|
||||
value: ''
|
||||
} as Pair)
|
||||
storeForm.properties = pro
|
||||
}
|
||||
})
|
||||
watch(storeForm, (e) => {
|
||||
if (e.kind.name === '') {
|
||||
if (e.url.startsWith('https://github.com') || e.url.startsWith('https://gitee.com')) {
|
||||
|
@ -148,20 +187,21 @@ function storeVerify(formEl: FormInstance | undefined) {
|
|||
ElMessage.error(e.message)
|
||||
}
|
||||
}, (e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
function updateKeys() {
|
||||
const props = storeForm.properties
|
||||
let lastItem = props[props.length - 1]
|
||||
if (lastItem.key !== '') {
|
||||
if (props.findIndex(p => p.key === '') === -1) {
|
||||
storeForm.properties.push({
|
||||
key: '',
|
||||
value: ''
|
||||
})
|
||||
} as Pair)
|
||||
}
|
||||
}
|
||||
|
||||
const storeExtLink = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -173,20 +213,27 @@ function updateKeys() {
|
|||
<el-table :data="stores" style="width: 100%" v-loading=storesLoading>
|
||||
<el-table-column :label="t('field.name')" width="180">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.name" placeholder="Name"/>
|
||||
{{ scope.row.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="URL">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.url" placeholder="URL" />
|
||||
{{ scope.row.url }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('field.plugin')">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.kind.url" placeholder="Plugin" />
|
||||
{{ scope.row.kind.name }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Socket">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
{{ scope.row.kind.url }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
@ -243,15 +290,19 @@ function updateKeys() {
|
|||
v-model="storeForm.kind.name"
|
||||
test-id="store-form-plugin-name"
|
||||
class="m-2"
|
||||
size="middle"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in SupportedExtensions()"
|
||||
:key="item.value"
|
||||
:label="item.key"
|
||||
:value="item.value"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
<el-icon v-if="storeExtLink && storeExtLink !== ''">
|
||||
<el-link :href="storeExtLink" target="_blank">
|
||||
<Help />
|
||||
</el-link>
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('field.pluginURL')" prop="plugin">
|
||||
<el-input v-model="storeForm.kind.url" test-id="store-form-plugin" />
|
||||
|
@ -268,9 +319,23 @@ function updateKeys() {
|
|||
</el-table-column>
|
||||
<el-table-column label="Value">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.value" placeholder="Value" />
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-select v-model="scope.row.value" v-if="scope.row.enum">
|
||||
<el-option
|
||||
v-for="item in scope.row.enum"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number v-model="scope.row.value" v-else-if="scope.row.type === 'number'"/>
|
||||
<el-input v-model="scope.row.value" :placeholder="scope.row.defaultValue" v-else/>
|
||||
<el-tooltip :content="scope.row.description" v-if="scope.row.description">
|
||||
<el-icon>
|
||||
<QuestionFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
@ -285,7 +350,7 @@ function updateKeys() {
|
|||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(storeFormRef)"
|
||||
:loading="creatingLoading"
|
||||
v-loading="creatingLoading"
|
||||
test-id="store-form-submit"
|
||||
>{{t('button.submit')}}</el-button
|
||||
>
|
||||
|
|
|
@ -3,18 +3,24 @@ import { ref } from 'vue'
|
|||
import type { Pair } from './types'
|
||||
import { API } from './net'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Magic } from './magicKeys'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const functionKind = ref('template')
|
||||
const dialogVisible = ref(false)
|
||||
const query = ref('')
|
||||
const funcs = ref([] as Pair[])
|
||||
|
||||
function queryFuncs() {
|
||||
API.FunctionsQuery(query.value, (d) => {
|
||||
API.FunctionsQuery(query.value, functionKind.value, (d) => {
|
||||
funcs.value = d.data
|
||||
})
|
||||
}
|
||||
|
||||
Magic.Keys(() => {
|
||||
dialogVisible.value = true
|
||||
}, ['Alt+KeyT'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -23,25 +29,44 @@ function queryFuncs() {
|
|||
data-intro="You can search your desired template functions.">{{ t('button.toolbox') }}</el-button>
|
||||
</el-affix>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="t('title.templateQuery')" width="40%" draggable destroy-on-close>
|
||||
<template #footer>
|
||||
<el-input v-model="query" placeholder="Query after enter" v-on:keyup.enter="queryFuncs" />
|
||||
<span class="dialog-footer">
|
||||
<el-table :data="funcs" style="width: 100%">
|
||||
<el-table-column label="Key" width="250">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.key" placeholder="Value" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Value">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.value" placeholder="Value" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</span>
|
||||
</template>
|
||||
<el-dialog v-model="dialogVisible" :title="t('title.functionQuery')" width="50%" draggable destroy-on-close>
|
||||
<el-input
|
||||
v-model="query" placeholder="Query after enter" v-on:keyup.enter="queryFuncs">
|
||||
<template #append v-if="funcs.length > 0">
|
||||
{{ funcs.length }}
|
||||
</template>
|
||||
<template #prepend>
|
||||
<el-select
|
||||
v-model="functionKind"
|
||||
>
|
||||
<el-option label="Template" value="template" />
|
||||
<el-option label="Verify" value="verify" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="dialog-footer">
|
||||
<el-table :data="funcs">
|
||||
<el-table-column label="Name" width="250">
|
||||
<template #default="scope">
|
||||
{{ scope.row.key }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Function">
|
||||
<template #default="scope">
|
||||
{{ scope.row.value }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Usage">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.description" readonly />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</span>
|
||||
<div>
|
||||
Powered by <a href="https://masterminds.github.io/sprig/" target="_blank">Sprig</a> and <a href="https://pkg.go.dev/text/template" target="_blank">built-in templates</a>.
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import { Edit, CopyDocument, Delete } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { Suite, TestCase, Pair } from './types'
|
||||
import { NewSuggestedAPIsQuery, GetHTTPMethods } from './types'
|
||||
import { NewSuggestedAPIsQuery, GetHTTPMethods, SwaggerSuggestion } from './types'
|
||||
import EditButton from '../components/EditButton.vue'
|
||||
import HistoryInput from '../components/HistoryInput.vue'
|
||||
import { Cache } from './cache'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { API } from './net'
|
||||
import { Magic } from './magicKeys'
|
||||
import { Codemirror } from 'vue-codemirror'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
name: String
|
||||
})
|
||||
const emit = defineEmits(['updated'])
|
||||
let querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name, props.name!)
|
||||
const querySwaggers = SwaggerSuggestion()
|
||||
|
||||
const suite = ref({
|
||||
name: '',
|
||||
|
@ -28,14 +34,25 @@ const suite = ref({
|
|||
raw: '',
|
||||
protofile: '',
|
||||
serverReflection: false
|
||||
}
|
||||
},
|
||||
secure: {
|
||||
insecure: true
|
||||
}
|
||||
},
|
||||
proxy: {
|
||||
http: '',
|
||||
https: '',
|
||||
no: ''
|
||||
}
|
||||
} as Suite)
|
||||
const shareLink = ref('')
|
||||
function load() {
|
||||
const store = Cache.GetCurrentStore()
|
||||
if (!props.name || store.name === "") return
|
||||
if (!props.name || store.name === '') return
|
||||
|
||||
API.GetTestSuite(props.name, (e) => {
|
||||
API.GetTestSuite(
|
||||
props.name,
|
||||
(e) => {
|
||||
suite.value = e
|
||||
if (suite.value.param.length === 0) {
|
||||
suite.value.param.push({
|
||||
|
@ -43,17 +60,42 @@ function load() {
|
|||
value: ''
|
||||
} as Pair)
|
||||
}
|
||||
}, (e) => {
|
||||
if (!suite.value.proxy) {
|
||||
suite.value.proxy = {
|
||||
http: '',
|
||||
https: '',
|
||||
no: ''
|
||||
}
|
||||
}
|
||||
if (!suite.value.spec.secure) {
|
||||
suite.value.spec.secure = {
|
||||
insecure: false
|
||||
}
|
||||
}
|
||||
|
||||
shareLink.value = `${window.location.href}api/v1/suites/${e.name}/yaml?x-store-name=${store.name}`
|
||||
},
|
||||
(e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
load()
|
||||
watch(props, () => {
|
||||
load()
|
||||
})
|
||||
|
||||
function save() {
|
||||
let oldImportPath = ""
|
||||
const testSuiteFormRef = ref<FormInstance>()
|
||||
const updateTestSuiteForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
saveTestSuite()
|
||||
}
|
||||
})
|
||||
}
|
||||
const saveTestSuite = () => {
|
||||
let oldImportPath = ''
|
||||
let hasImport = false
|
||||
if (suite.value.spec && suite.value.spec.rpc) {
|
||||
oldImportPath = suite.value.spec.rpc.import
|
||||
|
@ -64,8 +106,10 @@ function save() {
|
|||
}
|
||||
}
|
||||
|
||||
API.UpdateTestSuite(suite.value, (e) => {
|
||||
if (e.error === "") {
|
||||
API.UpdateTestSuite(
|
||||
suite.value,
|
||||
(e) => {
|
||||
if (e.error === '') {
|
||||
ElMessage({
|
||||
message: 'Updated.',
|
||||
type: 'success'
|
||||
|
@ -77,46 +121,57 @@ function save() {
|
|||
if (hasImport) {
|
||||
suite.value.spec.rpc.import = oldImportPath
|
||||
}
|
||||
}, (e) => {
|
||||
},
|
||||
(e) => {
|
||||
if (hasImport) {
|
||||
suite.value.spec.rpc.import = oldImportPath
|
||||
}
|
||||
ElMessage.error('Oops, ' + e)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
Magic.Keys(saveTestSuite, ['Alt+S', 'Alt+ß'])
|
||||
|
||||
const isFullScreen = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const testcaseFormRef = ref<FormInstance>()
|
||||
const testCaseForm = reactive({
|
||||
suiteName: '',
|
||||
name: '',
|
||||
api: '',
|
||||
method: 'GET'
|
||||
request: {
|
||||
api: '',
|
||||
method: 'GET'
|
||||
}
|
||||
})
|
||||
const rules = reactive<FormRules<Suite>>({
|
||||
name: [{ required: true, message: 'Please input TestCase name', trigger: 'blur' }]
|
||||
name: [{ required: true, message: 'Please input TestCase name', trigger: 'blur' }],
|
||||
'proxy.http': [{ type: 'url', message: 'Please input a valid URL', trigger: 'blur' }],
|
||||
})
|
||||
|
||||
function openNewTestCaseDialog() {
|
||||
dialogVisible.value = true
|
||||
querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name!, props.name!)
|
||||
}
|
||||
Magic.Keys(openNewTestCaseDialog, ['Alt+N', 'Alt+dead'])
|
||||
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
const submitTestCaseForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid: boolean, fields) => {
|
||||
await formEl.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
suiteCreatingLoading.value = true
|
||||
|
||||
API.CreateTestCase({
|
||||
suiteName: props.name,
|
||||
name: testCaseForm.name,
|
||||
api: testCaseForm.api,
|
||||
method: testCaseForm.method
|
||||
}, () => {
|
||||
suiteCreatingLoading.value = false
|
||||
emit('updated', 'hello from child')
|
||||
})
|
||||
suiteName: props.name,
|
||||
name: testCaseForm.name,
|
||||
request: testCaseForm.request
|
||||
}, () => {
|
||||
suiteCreatingLoading.value = false
|
||||
emit('updated', props.name, testCaseForm.name)
|
||||
}, (e) => {
|
||||
suiteCreatingLoading.value = false
|
||||
ElMessage.error('Oops, ' + e)
|
||||
}
|
||||
)
|
||||
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
@ -124,29 +179,36 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
}
|
||||
|
||||
function del() {
|
||||
API.DeleteTestSuite(props.name, () => {
|
||||
API.DeleteTestSuite(
|
||||
props.name,
|
||||
() => {
|
||||
ElMessage({
|
||||
message: 'Deleted.',
|
||||
type: 'success'
|
||||
})
|
||||
emit('updated')
|
||||
}, (e) => {
|
||||
},
|
||||
(e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function convert() {
|
||||
API.ConvertTestSuite(props.name, 'jmeter', (e) => {
|
||||
const blob = new Blob([e.message], { type: `text/xml;charset=utf-8;` });
|
||||
const link = document.createElement('a');
|
||||
API.ConvertTestSuite(
|
||||
props.name,
|
||||
'jmeter',
|
||||
(e) => {
|
||||
const blob = new Blob([e.message], { type: `text/xml;charset=utf-8;` })
|
||||
const link = document.createElement('a')
|
||||
if (link.download !== undefined) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `jmeter.jmx`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `jmeter.jmx`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
ElMessage({
|
||||
|
@ -154,9 +216,11 @@ function convert() {
|
|||
type: 'success'
|
||||
})
|
||||
emit('updated')
|
||||
}, (e) => {
|
||||
},
|
||||
(e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const suiteCreatingLoading = ref(false)
|
||||
|
@ -173,10 +237,11 @@ const apiSpecKinds = [
|
|||
]
|
||||
|
||||
const handleAPISelect = (item: TestCase) => {
|
||||
testCaseForm.method = item.request.method
|
||||
if (testCaseForm.name === '') {
|
||||
testCaseForm.name = item.name
|
||||
}
|
||||
testCaseForm.method = item.request.method
|
||||
if (testCaseForm.name === '') {
|
||||
testCaseForm.name = item.name
|
||||
}
|
||||
testCaseForm.request = item.request
|
||||
}
|
||||
|
||||
function paramChange() {
|
||||
|
@ -189,86 +254,180 @@ function paramChange() {
|
|||
} as Pair)
|
||||
}
|
||||
}
|
||||
|
||||
const yamlFormat = ref('')
|
||||
const yamlDialogVisible = ref(false)
|
||||
|
||||
function viewYaml() {
|
||||
yamlDialogVisible.value = true
|
||||
API.GetTestSuiteYaml(props.name, (d) => {
|
||||
try {
|
||||
yamlFormat.value = yaml.dump(yaml.load(atob(d.data)))
|
||||
} catch (e) {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openDuplicateDialog = () => {
|
||||
testSuiteDuplicateDialog.value = true
|
||||
targetSuiteDuplicateName.value = props.name + '-copy'
|
||||
}
|
||||
const duplicateTestSuite = () => {
|
||||
API.DuplicateTestSuite(props.name, targetSuiteDuplicateName.value, (d) => {
|
||||
testSuiteDuplicateDialog.value = false
|
||||
ElMessage({
|
||||
message: 'Duplicated.',
|
||||
type: 'success'
|
||||
})
|
||||
emit('updated')
|
||||
})
|
||||
}
|
||||
const testSuiteDuplicateDialog = ref(false)
|
||||
const targetSuiteDuplicateName = ref('')
|
||||
|
||||
const renameTestSuite = (name: string) => {
|
||||
API.RenameTestSuite(props.name, name, (d) => {
|
||||
emit('updated', name)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
{{ t('tip.testsuite') }}<el-text class="mx-1" type="primary">{{ suite.name }}</el-text>
|
||||
<el-form :rules="rules"
|
||||
ref="testSuiteFormRef"
|
||||
:model="suite"
|
||||
label-width="auto">
|
||||
{{ t('tip.testsuite') }}<EditButton :value="suite.name" @changed="renameTestSuite"/>
|
||||
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td style="width: 20%;">
|
||||
{{ t('tip.apiAddress') }}
|
||||
</td>
|
||||
<td style="width: 80%;">
|
||||
<el-input class="w-50 m-2" v-model="suite.api" placeholder="API" test-id="suite-editor-api"></el-input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<el-select v-model="suite.spec.kind" class="m-2" placeholder="API Spec Kind" size="middle">
|
||||
<el-option
|
||||
v-for="item in apiSpecKinds"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</td>
|
||||
<td>
|
||||
<el-input class="mx-1" v-model="suite.spec.url" placeholder="API Spec URL"></el-input>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<el-form-item :label="t('tip.apiAddress')" prop="api">
|
||||
<HistoryInput placeholder="API" v-model="suite.api" group="apiAddress" />
|
||||
</el-form-item>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td>
|
||||
<el-select
|
||||
v-model="suite.spec.kind"
|
||||
class="m-2"
|
||||
placeholder="API Spec Kind"
|
||||
size="default"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiSpecKinds"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</td>
|
||||
<td>
|
||||
<el-autocomplete
|
||||
v-model="suite.spec.url"
|
||||
:fetch-suggestions="querySwaggers"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 10px;">
|
||||
<el-text class="mx-1" type="primary">{{ t('title.parameter') }}</el-text>
|
||||
<el-table :data="suite.param" style="width: 100%">
|
||||
<el-table-column label="Key" width="180">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.key" placeholder="Key" @change="paramChange"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Value">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.value" placeholder="Value" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-divider />
|
||||
</div>
|
||||
<el-collapse>
|
||||
<el-collapse-item :title="t('title.parameter')">
|
||||
<el-table :data="suite.param" style="width: 100%">
|
||||
<el-table-column :label="t('field.key')" width="180">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.key" :placeholder="t('field.key')" @change="paramChange" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('field.value')">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.value" :placeholder="t('field.value')" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item v-if="suite.spec.rpc">
|
||||
<div>
|
||||
<span>{{ t('title.refelction') }}</span>
|
||||
<el-switch v-model="suite.spec.rpc.serverReflection" />
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('title.protoContent') }}</span>
|
||||
<el-input
|
||||
v-model="suite.spec.rpc.raw"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('title.protoImport') }}</span>
|
||||
<el-input class="mx-1" v-model="suite.spec.rpc.import"></el-input>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('title.protoFile') }}</span>
|
||||
<el-input class="mx-1" v-model="suite.spec.rpc.protofile"></el-input>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="t('title.secure')">
|
||||
<el-switch v-model="suite.spec.secure.insecure" active-text="Insecure" inactive-text="Secure" inline-prompt/>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="t('title.proxy')">
|
||||
<div>
|
||||
<el-form-item :label="t('proxy.http')" prop="proxy.http">
|
||||
<el-input class="mx-1" v-model="suite.proxy.http" placeholder="HTTP Proxy"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div>
|
||||
<el-form-item :label="t('proxy.https')" prop="proxy.http">
|
||||
<el-input class="mx-1" v-model="suite.proxy.https" placeholder="HTTPS Proxy"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div>
|
||||
<el-form-item :label="t('proxy.no')">
|
||||
<el-input class="mx-1" v-model="suite.proxy.no" placeholder="No Proxy"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<div v-if="suite.spec.rpc">
|
||||
<div>
|
||||
<span>{{ t('title.refelction') }}</span>
|
||||
<el-switch v-model="suite.spec.rpc.serverReflection" />
|
||||
<div class="button-container">
|
||||
Share link: <el-input readonly v-model="shareLink" style="width: 80%" />
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('title.protoContent') }}</span>
|
||||
<el-input
|
||||
v-model="suite.spec.rpc.raw"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
type="textarea"
|
||||
/>
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="updateTestSuiteForm(testSuiteFormRef)" v-if="!Cache.GetCurrentStore().readOnly">{{
|
||||
t('button.save')
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="updateTestSuiteForm(testSuiteFormRef)" disabled v-if="Cache.GetCurrentStore().readOnly">{{
|
||||
t('button.save')
|
||||
}}</el-button>
|
||||
<el-button type="danger" @click="del" :icon="Delete" test-id="suite-del-but">{{
|
||||
t('button.delete')
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="convert" test-id="convert">{{
|
||||
t('button.export')
|
||||
}}</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="openDuplicateDialog"
|
||||
:icon="CopyDocument"
|
||||
test-id="duplicate"
|
||||
>{{ t('button.duplicate') }}</el-button
|
||||
>
|
||||
<el-button type="primary" @click="viewYaml" test-id="view-yaml">{{
|
||||
t('button.viewYaml')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('title.protoImport') }}</span>
|
||||
<el-input class="mx-1" v-model="suite.spec.rpc.import"></el-input>
|
||||
<div class="button-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="openNewTestCaseDialog"
|
||||
:icon="Edit"
|
||||
test-id="open-new-case-dialog"
|
||||
>{{ t('button.newtestcase') }}</el-button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('title.protoFile') }}</span>
|
||||
<el-input class="mx-1" v-model="suite.spec.rpc.protofile"></el-input>
|
||||
</div>
|
||||
<el-divider />
|
||||
</div>
|
||||
|
||||
<el-button type="primary" @click="save" v-if="!Cache.GetCurrentStore().readOnly">{{ t('button.save') }}</el-button>
|
||||
<el-button type="primary" @click="save" disabled v-if="Cache.GetCurrentStore().readOnly">{{ t('button.save') }}</el-button>
|
||||
<el-button type="primary" @click="del" test-id="suite-del-but">{{ t('button.delete') }}</el-button>
|
||||
<el-button type="primary" @click="openNewTestCaseDialog" :icon="Edit" test-id="open-new-case-dialog">{{ t('button.newtestcase') }}</el-button>
|
||||
<el-button type="primary" @click="convert" test-id="convert">{{ t('button.export') }}</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="t('title.createTestCase')" width="40%" draggable>
|
||||
|
@ -282,11 +441,15 @@ function paramChange() {
|
|||
label-width="60px"
|
||||
>
|
||||
<el-form-item :label="t('field.name')" prop="name">
|
||||
<el-input v-model="testCaseForm.name" test-id="case-form-name"/>
|
||||
<el-input v-model="testCaseForm.name" test-id="case-form-name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Method" prop="method" v-if="suite.spec.kind !== 'tRPC' && suite.spec.kind !== 'gRPC'">
|
||||
<el-form-item
|
||||
label="Method"
|
||||
prop="method"
|
||||
v-if="suite.spec.kind !== 'tRPC' && suite.spec.kind !== 'gRPC'"
|
||||
>
|
||||
<el-select
|
||||
v-model="testCaseForm.method"
|
||||
v-model="testCaseForm.request.method"
|
||||
class="m-2"
|
||||
placeholder="Method"
|
||||
size="middle"
|
||||
|
@ -302,7 +465,7 @@ function paramChange() {
|
|||
</el-form-item>
|
||||
<el-form-item label="API" prop="api">
|
||||
<el-autocomplete
|
||||
v-model="testCaseForm.api"
|
||||
v-model="testCaseForm.request.api"
|
||||
:fetch-suggestions="querySuggestedAPIs"
|
||||
@select="handleAPISelect"
|
||||
placeholder="API Address"
|
||||
|
@ -318,8 +481,8 @@ function paramChange() {
|
|||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(testcaseFormRef)"
|
||||
:loading="suiteCreatingLoading"
|
||||
@click="submitTestCaseForm(testcaseFormRef)"
|
||||
v-loading="suiteCreatingLoading"
|
||||
test-id="case-form-submit"
|
||||
>{{ t('button.submit') }}</el-button
|
||||
>
|
||||
|
@ -328,4 +491,41 @@ function paramChange() {
|
|||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="yamlDialogVisible"
|
||||
:title="t('button.viewYaml')"
|
||||
:fullscreen="isFullScreen"
|
||||
width="40%"
|
||||
draggable
|
||||
>
|
||||
<el-button type="primary" @click="isFullScreen = !isFullScreen" style="margin-bottom: 10px">
|
||||
<p>{{ isFullScreen ? t('button.cancelFullScreen') : t('button.fullScreen') }}</p>
|
||||
</el-button>
|
||||
<el-scrollbar>
|
||||
<Codemirror v-model="yamlFormat" />
|
||||
</el-scrollbar>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="testSuiteDuplicateDialog">
|
||||
<template #default>
|
||||
New Test Suite Name:<el-input v-model="targetSuiteDuplicateName" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="duplicateTestSuite">{{ t('button.ok') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.button-container > .el-button + .el-button {
|
||||
margin-left: 0px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,345 @@
|
|||
<script setup lang="ts">
|
||||
import TestCase from './TestCase.vue'
|
||||
import TestSuite from './TestSuite.vue'
|
||||
import TemplateFunctions from './TemplateFunctions.vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElTree, ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Edit, Refresh } from '@element-plus/icons-vue'
|
||||
import type { Suite } from './types'
|
||||
import { API } from './net'
|
||||
import { Cache } from './cache'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Tree {
|
||||
id: string
|
||||
label: string
|
||||
parent: string
|
||||
parentID: string
|
||||
store: string
|
||||
kind: string
|
||||
suiteLabel: string
|
||||
children?: Tree[]
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
ID: String,
|
||||
})
|
||||
const testCaseName = ref('')
|
||||
const testSuite = ref('')
|
||||
const testKind = ref('')
|
||||
const historySuiteName = ref('')
|
||||
const historyCaseID = ref('')
|
||||
|
||||
const handleNodeClick = (data: Tree) => {
|
||||
if (data.children) {
|
||||
Cache.SetCurrentStore(data.store)
|
||||
viewName.value = 'testsuite'
|
||||
historySuiteName.value = data.label
|
||||
Cache.SetCurrentStore(data.store)
|
||||
|
||||
API.ListTestCase(data.label, data.store, (d) => {
|
||||
if (d.items && d.items.length > 0) {
|
||||
data.children = []
|
||||
d.items.forEach((item: any) => {
|
||||
data.children?.push({
|
||||
id: data.label,
|
||||
label: item.name,
|
||||
kind: data.kind,
|
||||
store: data.store,
|
||||
parent: data.label,
|
||||
parentID: data.id
|
||||
} as Tree)
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Cache.SetCurrentStore(data.store)
|
||||
Cache.SetLastTestCaseLocation(data.parentID, data.id)
|
||||
historySuiteName.value = data.parent
|
||||
testSuite.value = data.suiteLabel
|
||||
testCaseName.value = data.label
|
||||
historyCaseID.value = data.id
|
||||
testKind.value = data.kind
|
||||
viewName.value = 'testcase'
|
||||
}
|
||||
}
|
||||
|
||||
const treeData = ref([] as Tree[])
|
||||
const treeRef = ref<InstanceType<typeof ElTree>>()
|
||||
const currentNodekey = ref('')
|
||||
|
||||
function loadHistoryTestSuites(storeName: string) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Store-Name': storeName,
|
||||
'X-Auth': API.getToken()
|
||||
},
|
||||
}
|
||||
return async () => {
|
||||
await fetch('/api/v1/historySuites', requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((d) => {
|
||||
if (!d.data) {
|
||||
return
|
||||
}
|
||||
const sortedKeys = Object.keys(d.data).sort((a, b) => new Date(a) - new Date(b));
|
||||
sortedKeys.map((k) => {
|
||||
let suite = {
|
||||
id: k,
|
||||
label: k,
|
||||
store: storeName,
|
||||
children: [] as Tree[]
|
||||
} as Tree
|
||||
|
||||
d.data[k].data.forEach((item: any) => {
|
||||
suite.children?.push({
|
||||
id: item.ID,
|
||||
label: item.testcase,
|
||||
suiteLabel: item.suite,
|
||||
store: storeName,
|
||||
kind: item.kind,
|
||||
parent: k,
|
||||
parentID: suite.id
|
||||
} as Tree)
|
||||
})
|
||||
treeData.value.push(suite)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function generateTestCaseID(suiteName: string, caseName: string) {
|
||||
return suiteName + caseName
|
||||
}
|
||||
|
||||
interface Store {
|
||||
name: string,
|
||||
description: string,
|
||||
}
|
||||
|
||||
const loginDialogVisible = ref(false)
|
||||
const stores = ref([] as Store[])
|
||||
const storesLoading = ref(false)
|
||||
function loadStores(lastSuitName?: string, lastCaseName?: string) {
|
||||
if (lastSuitName && lastCaseName && lastSuitName !== '' && lastCaseName !== '') {
|
||||
// get data from emit event
|
||||
Cache.SetLastTestCaseLocation(lastSuitName, generateTestCaseID(lastSuitName, lastCaseName))
|
||||
}
|
||||
|
||||
storesLoading.value = true
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
'X-Auth': API.getToken()
|
||||
}
|
||||
}
|
||||
fetch('/api/v1/stores', requestOptions)
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then(async (d) => {
|
||||
stores.value = d.data
|
||||
treeData.value = [] as Tree[]
|
||||
Cache.SetStores(d.data)
|
||||
|
||||
for (const item of d.data) {
|
||||
if (item.ready && !item.disabled) {
|
||||
await loadHistoryTestSuites(item.name)()
|
||||
}
|
||||
}
|
||||
|
||||
if (treeData.value.length > 0) {
|
||||
const key = Cache.GetLastTestCaseLocation()
|
||||
|
||||
let targetSuite = {} as Tree
|
||||
let targetChild = {} as Tree
|
||||
|
||||
const targetID = props.ID
|
||||
if (targetID && targetID !== '') {
|
||||
for (const suite of treeData.value) {
|
||||
if (suite.children) {
|
||||
const foundChild = suite.children.find(child => child.id === targetID)
|
||||
if (foundChild) {
|
||||
targetSuite = suite
|
||||
targetChild = foundChild
|
||||
handleNodeClick(targetChild)
|
||||
updateTreeSelection(targetSuite, targetChild)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (key.suite !== '' && key.testcase !== '') {
|
||||
for (var i = 0; i < treeData.value.length; i++) {
|
||||
const item = treeData.value[i]
|
||||
if (item.id === key.suite && item.children) {
|
||||
for (var j = 0; j < item.children.length; j++) {
|
||||
const child = item.children[j]
|
||||
if (child.id === key.testcase) {
|
||||
targetSuite = item
|
||||
targetChild = child
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetChild.id || targetChild.id === '') {
|
||||
targetSuite = treeData.value[0]
|
||||
if (targetSuite.children && targetSuite.children.length > 0) {
|
||||
targetChild = targetSuite.children[0]
|
||||
}
|
||||
}
|
||||
|
||||
viewName.value = 'testsuite'
|
||||
updateTreeSelection(targetSuite, targetChild)
|
||||
} else {
|
||||
viewName.value = ""
|
||||
}
|
||||
}).catch((e) => {
|
||||
if (e.message === "Unauthenticated") {
|
||||
loginDialogVisible.value = true
|
||||
} else {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
}
|
||||
}).finally(() => {
|
||||
storesLoading.value = false
|
||||
})
|
||||
}
|
||||
loadStores()
|
||||
|
||||
function updateTreeSelection(targetSuite: Tree, targetChild: Tree) {
|
||||
currentNodekey.value = targetChild.id
|
||||
|
||||
treeRef.value!.setCurrentKey(targetChild.id)
|
||||
treeRef.value!.setCheckedKeys([targetChild.id], false)
|
||||
|
||||
testSuite.value = targetSuite.label
|
||||
Cache.SetCurrentStore(targetSuite.store)
|
||||
testKind.value = targetChild.kind
|
||||
}
|
||||
|
||||
const filterText = ref('')
|
||||
watch(filterText, (val) => {
|
||||
treeRef.value!.filter(val)
|
||||
})
|
||||
const filterTestCases = (value: string, data: Tree) => {
|
||||
if (!value) return true
|
||||
return data.label.includes(value)
|
||||
}
|
||||
|
||||
const viewName = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout" data-title="Welcome!" data-intro="Welcome to use api-testing! 👋">
|
||||
<el-container style="height: 100%">
|
||||
<el-main style="padding-top: 5px; padding-bottom: 5px;">
|
||||
<el-container style="height: 100%">
|
||||
<el-aside>
|
||||
<el-button type="primary" @click="loadStores" :icon="Refresh">{{ t('button.refresh') }}</el-button>
|
||||
<el-input v-model="filterText" :placeholder="t('tip.filter')" test-id="search" style="padding: 5px;" />
|
||||
<el-tree
|
||||
v-loading="storesLoading"
|
||||
:data=treeData
|
||||
highlight-current
|
||||
:check-on-click-node="true"
|
||||
:expand-on-click-node="false"
|
||||
:current-node-key="currentNodekey"
|
||||
ref="treeRef"
|
||||
node-key="id"
|
||||
:filter-node-method="filterTestCases"
|
||||
@node-click="handleNodeClick"
|
||||
data-intro="This is the test history tree. You can click the history test to browse it."
|
||||
/>
|
||||
<TemplateFunctions />
|
||||
</el-aside>
|
||||
|
||||
<el-main style="padding-top: 0px; padding-right: 0px; padding-bottom: 0px;">
|
||||
<TestCase v-if="viewName === 'testcase'" :suite="testSuite" :kindName="testKind" :name="testCaseName"
|
||||
:historySuiteName="historySuiteName" :historyCaseID="historyCaseID" @updated="loadStores" style="height: 100%;"
|
||||
data-intro="This is the test case editor. You can edit the test case here." />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.common-layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-tabs>.el-tabs__content {
|
||||
padding: 32px;
|
||||
color: #6b778c;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -1,21 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import TestCase from './TestCase.vue'
|
||||
import TestSuite from './TestSuite.vue'
|
||||
import { GetHTTPMethod } from './types'
|
||||
import TemplateFunctions from './TemplateFunctions.vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElTree, ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Edit, Refresh } from '@element-plus/icons-vue'
|
||||
import type { Suite } from './types'
|
||||
import { API } from './net'
|
||||
import { Cache } from './cache'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Magic } from './magicKeys'
|
||||
import TestSuiteCreationDialog from '../components/TestSuiteCreationDialog.vue'
|
||||
import TestSuiteImportDialog from '../components/TestSuiteImportDialog.vue'
|
||||
import LoginDialog from '../components/LoginDialog.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Tree {
|
||||
id: string
|
||||
label: string
|
||||
method: string
|
||||
parent: string
|
||||
parentID: string
|
||||
store: string
|
||||
|
@ -26,7 +30,7 @@ interface Tree {
|
|||
const testCaseName = ref('')
|
||||
const testSuite = ref('')
|
||||
const testSuiteKind = ref('')
|
||||
const handleNodeClick = (data: Tree) => {
|
||||
const handleTreeClick = (data: Tree) => {
|
||||
if (data.children) {
|
||||
Cache.SetCurrentStore(data.store)
|
||||
viewName.value = 'testsuite'
|
||||
|
@ -39,8 +43,9 @@ const handleNodeClick = (data: Tree) => {
|
|||
data.children = []
|
||||
d.items.forEach((item: any) => {
|
||||
data.children?.push({
|
||||
id: data.label,
|
||||
id: item.name,
|
||||
label: item.name,
|
||||
method: item.request.method,
|
||||
kind: data.kind,
|
||||
store: data.store,
|
||||
parent: data.label,
|
||||
|
@ -59,20 +64,53 @@ const handleNodeClick = (data: Tree) => {
|
|||
}
|
||||
}
|
||||
|
||||
const data = ref([] as Tree[])
|
||||
Magic.Keys((k) => {
|
||||
const currentKey = currentNodekey.value
|
||||
|
||||
if (treeRef.value) {
|
||||
treeRef.value.data.forEach((n) => {
|
||||
if (n.children) {
|
||||
n.children.forEach((c, index) => {
|
||||
if (c.id === currentKey) {
|
||||
var nextIndex = -1
|
||||
if (k.endsWith('Up')) {
|
||||
if (index > 0) {
|
||||
nextIndex = index - 1
|
||||
}
|
||||
} else {
|
||||
if (index < n.children.length - 1) {
|
||||
nextIndex = index + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (nextIndex >= 0 < n.children.length) {
|
||||
const next = n.children[nextIndex]
|
||||
currentNodekey.value = next.id
|
||||
treeRef.value!.setCurrentKey(next.id)
|
||||
treeRef.value!.setCheckedKeys([next.id], false)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, ['Alt+ArrowUp', 'Alt+ArrowDown'])
|
||||
|
||||
const treeData = ref([] as Tree[])
|
||||
const treeRef = ref<InstanceType<typeof ElTree>>()
|
||||
const currentNodekey = ref('')
|
||||
|
||||
function loadTestSuites(storeName: string) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Store-Name': storeName,
|
||||
'X-Auth': API.getToken()
|
||||
},
|
||||
}
|
||||
return async () => {
|
||||
await fetch('/server.Runner/GetSuites', requestOptions)
|
||||
await fetch('/api/v1/suites', requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((d) => {
|
||||
if (!d.data) {
|
||||
|
@ -89,7 +127,7 @@ function loadTestSuites(storeName: string) {
|
|||
|
||||
d.data[k].data.forEach((item: any) => {
|
||||
suite.children?.push({
|
||||
id: k + item,
|
||||
id: generateTestCaseID(k, item),
|
||||
label: item,
|
||||
store: storeName,
|
||||
kind: suite.kind,
|
||||
|
@ -97,12 +135,16 @@ function loadTestSuites(storeName: string) {
|
|||
parentID: suite.id
|
||||
} as Tree)
|
||||
})
|
||||
data.value.push(suite)
|
||||
treeData.value.push(suite)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function generateTestCaseID(suiteName: string, caseName: string) {
|
||||
return suiteName + caseName
|
||||
}
|
||||
|
||||
interface Store {
|
||||
name: string,
|
||||
description: string,
|
||||
|
@ -111,19 +153,23 @@ interface Store {
|
|||
const loginDialogVisible = ref(false)
|
||||
const stores = ref([] as Store[])
|
||||
const storesLoading = ref(false)
|
||||
function loadStores() {
|
||||
function loadStores(lastSuitName?: string, lastCaseName?: string) {
|
||||
if (lastSuitName && lastCaseName && lastSuitName !== '' && lastCaseName !== '') {
|
||||
// get data from emit event
|
||||
Cache.SetLastTestCaseLocation(lastSuitName, generateTestCaseID(lastSuitName, lastCaseName))
|
||||
}
|
||||
|
||||
storesLoading.value = true
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Auth': API.getToken()
|
||||
}
|
||||
}
|
||||
fetch('/server.Runner/GetStores', requestOptions)
|
||||
fetch('/api/v1/stores', requestOptions)
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then(async (d) => {
|
||||
stores.value = d.data
|
||||
data.value = [] as Tree[]
|
||||
treeData.value = [] as Tree[]
|
||||
Cache.SetStores(d.data)
|
||||
|
||||
for (const item of d.data) {
|
||||
|
@ -132,14 +178,14 @@ function loadStores() {
|
|||
}
|
||||
}
|
||||
|
||||
if (data.value.length > 0) {
|
||||
if (treeData.value.length > 0) {
|
||||
const key = Cache.GetLastTestCaseLocation()
|
||||
|
||||
let targetSuite = {} as Tree
|
||||
let targetChild = {} as Tree
|
||||
if (key.suite !== '' && key.testcase !== '') {
|
||||
for (var i = 0; i < data.value.length; i++) {
|
||||
const item = data.value[i]
|
||||
for (var i = 0; i < treeData.value.length; i++) {
|
||||
const item = treeData.value[i]
|
||||
if (item.id === key.suite && item.children) {
|
||||
for (var j = 0; j < item.children.length; j++) {
|
||||
const child = item.children[j]
|
||||
|
@ -155,7 +201,7 @@ function loadStores() {
|
|||
}
|
||||
|
||||
if (!targetChild.id || targetChild.id === '') {
|
||||
targetSuite = data.value[0]
|
||||
targetSuite = treeData.value[0]
|
||||
if (targetSuite.children && targetSuite.children.length > 0) {
|
||||
targetChild = targetSuite.children[0]
|
||||
}
|
||||
|
@ -163,10 +209,12 @@ function loadStores() {
|
|||
|
||||
viewName.value = 'testsuite'
|
||||
currentNodekey.value = targetChild.id
|
||||
|
||||
treeRef.value!.setCurrentKey(targetChild.id)
|
||||
treeRef.value!.setCheckedKeys([targetChild.id], false)
|
||||
|
||||
testSuite.value = targetSuite.label
|
||||
Cache.SetCurrentStore(targetSuite.store )
|
||||
Cache.SetCurrentStore(targetSuite.store)
|
||||
testSuiteKind.value = targetChild.kind
|
||||
} else {
|
||||
viewName.value = ""
|
||||
|
@ -185,19 +233,6 @@ loadStores()
|
|||
|
||||
const dialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const suiteCreatingLoading = ref(false)
|
||||
const suiteFormRef = ref<FormInstance>()
|
||||
const testSuiteForm = reactive({
|
||||
name: '',
|
||||
api: '',
|
||||
store: '',
|
||||
kind: ''
|
||||
})
|
||||
const importSuiteFormRef = ref<FormInstance>()
|
||||
const importSuiteForm = reactive({
|
||||
url: '',
|
||||
store: ''
|
||||
})
|
||||
|
||||
function openTestSuiteCreateDialog() {
|
||||
dialogVisible.value = true
|
||||
|
@ -207,96 +242,16 @@ 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
|
||||
await formEl.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
suiteCreatingLoading.value = true
|
||||
|
||||
API.CreateTestSuite(testSuiteForm, (e) => {
|
||||
suiteCreatingLoading.value = false
|
||||
if (e.error !== "") {
|
||||
ElMessage.error('Oops, ' + e.error)
|
||||
} else {
|
||||
loadStores()
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
}, (e) => {
|
||||
suiteCreatingLoading.value = false
|
||||
ElMessage.error('Oops, ' + e)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (valid) {
|
||||
suiteCreatingLoading.value = true
|
||||
|
||||
API.ImportTestSuite(importSuiteForm, () => {
|
||||
loadStores()
|
||||
importDialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const filterText = ref('')
|
||||
watch(filterText, (val) => {
|
||||
treeRef.value!.filter(val)
|
||||
})
|
||||
const filterTestCases = (value: string, data: Tree) => {
|
||||
if (!value) return true
|
||||
return data.label.includes(value)
|
||||
return data.label.toLocaleLowerCase().includes(value.toLocaleLowerCase())
|
||||
}
|
||||
|
||||
const viewName = ref('')
|
||||
|
||||
const deviceAuthActive = ref(0)
|
||||
const deviceAuthResponse = ref({
|
||||
user_code: '',
|
||||
verification_uri: '',
|
||||
device_code: ''
|
||||
})
|
||||
const deviceAuthNext = () => {
|
||||
if (deviceAuthActive.value++ > 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (deviceAuthActive.value === 1) {
|
||||
fetch('/oauth2/getLocalCode')
|
||||
.then(API.DefaultResponseProcess)
|
||||
.then((d) => {
|
||||
deviceAuthResponse.value = d
|
||||
})
|
||||
} else if (deviceAuthActive.value === 2) {
|
||||
window.location.href = '/oauth2/getUserInfoFromLocalCode?device_code=' + deviceAuthResponse.value.device_code
|
||||
}
|
||||
}
|
||||
|
||||
const suiteKinds = [{
|
||||
"name": "HTTP",
|
||||
}, {
|
||||
"name": "gRPC",
|
||||
}, {
|
||||
"name": "tRPC",
|
||||
}]
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -316,7 +271,7 @@ const suiteKinds = [{
|
|||
|
||||
<el-tree
|
||||
v-loading="storesLoading"
|
||||
:data=data
|
||||
:data=treeData
|
||||
highlight-current
|
||||
:check-on-click-node="true"
|
||||
:expand-on-click-node="false"
|
||||
|
@ -324,9 +279,15 @@ const suiteKinds = [{
|
|||
ref="treeRef"
|
||||
node-key="id"
|
||||
:filter-node-method="filterTestCases"
|
||||
@node-click="handleNodeClick"
|
||||
@current-change="handleTreeClick"
|
||||
data-intro="This is the test suite tree. You can click the test suite to edit it."
|
||||
/>
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="custom-tree-node">
|
||||
<el-text class="mx-1" :type="GetHTTPMethod(data.method).type">{{ node.label }}</el-text>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<TemplateFunctions/>
|
||||
</el-aside>
|
||||
|
||||
|
@ -336,6 +297,7 @@ const suiteKinds = [{
|
|||
:suite="testSuite"
|
||||
:kindName="testSuiteKind"
|
||||
:name="testCaseName"
|
||||
@toHistoryPanel="handleToHistoryPanel"
|
||||
@updated="loadStores"
|
||||
style="height: 100%;"
|
||||
data-intro="This is the test case editor. You can edit the test case here."
|
||||
|
@ -352,128 +314,16 @@ const suiteKinds = [{
|
|||
</el-container>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="t('title.createTestSuite')" width="30%" draggable>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-form
|
||||
:rules="rules"
|
||||
:model="testSuiteForm"
|
||||
ref="suiteFormRef"
|
||||
status-icon label-width="120px">
|
||||
<el-form-item :label="t('field.storageLocation')" prop="store">
|
||||
<el-select v-model="testSuiteForm.store" class="m-2"
|
||||
test-id="suite-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="t('field.suiteKind')" prop="kind">
|
||||
<el-select v-model="testSuiteForm.kind" class="m-2"
|
||||
filterable=true
|
||||
test-id="suite-form-kind"
|
||||
default-first-option=true
|
||||
size="middle">
|
||||
<el-option
|
||||
v-for="item in suiteKinds"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('field.name')" prop="name">
|
||||
<el-input v-model="testSuiteForm.name" test-id="suite-form-name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API" prop="api">
|
||||
<el-input v-model="testSuiteForm.api" placeholder="http://foo" test-id="suite-form-api" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(suiteFormRef)"
|
||||
:loading="suiteCreatingLoading"
|
||||
test-id="suite-form-submit"
|
||||
>{{ t('button.submit') }}</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<TestSuiteCreationDialog
|
||||
:visible="dialogVisible"
|
||||
@created="dialogVisible=false; loadStores()"/>
|
||||
|
||||
<el-dialog v-model="importDialogVisible" title="Import Test Suite" width="30%" draggable>
|
||||
<span>Supported source URL: Postman collection share link</span>
|
||||
<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" placeholder="https://api.postman.com/collections/xxx" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="importSuiteFormSubmit(importSuiteFormRef)"
|
||||
test-id="suite-import-submit"
|
||||
>{{ t('button.import') }}</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<TestSuiteImportDialog
|
||||
:visible="importDialogVisible"
|
||||
@created="importDialogVisible=false; loadStores()"/>
|
||||
|
||||
<el-dialog
|
||||
v-model="loginDialogVisible"
|
||||
title="You need to login first."
|
||||
width="30%"
|
||||
>
|
||||
<el-collapse accordion="true">
|
||||
<el-collapse-item title="Server in cloud" name="1">
|
||||
<a href="/oauth2/token" target="_blank">
|
||||
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle color-fg-default">
|
||||
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="Server in local" name="2">
|
||||
<el-steps :active="deviceAuthActive" finish-status="success">
|
||||
<el-step title="Request Device Code" />
|
||||
<el-step title="Input Code"/>
|
||||
<el-step title="Finished" />
|
||||
</el-steps>
|
||||
|
||||
<div v-if="deviceAuthActive===1">
|
||||
Open <a :href="deviceAuthResponse.verification_uri" target="_blank">this link</a>, and type the code: <span>{{ deviceAuthResponse.user_code }}. Then click the next step button.</span>
|
||||
</div>
|
||||
<el-button style="margin-top: 12px" @click="deviceAuthNext">Next step</el-button>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-dialog>
|
||||
<LoginDialog
|
||||
:visible="loginDialogVisible"/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { API } from '../views/net'
|
||||
|
||||
interface SBOM {
|
||||
go: {}
|
||||
js: {
|
||||
dependencies: {}
|
||||
devDependencies: {}
|
||||
}
|
||||
}
|
||||
const sbomItems = ref({} as SBOM)
|
||||
API.SBOM((d) => {
|
||||
sbomItems.value = d
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Welcome to use atest to improve your code quality!</div>
|
||||
<div>Please read the following guide if this is your first time to use atest.</div>
|
||||
|
@ -8,4 +25,31 @@
|
|||
<div>
|
||||
Please get more details from the <a href="https://linuxsuren.github.io/api-testing/" target="_blank" rel="noopener">official document</a>.
|
||||
</div>
|
||||
|
||||
<el-divider/>
|
||||
|
||||
<div>
|
||||
Golang dependencies:
|
||||
<div>
|
||||
<el-scrollbar height="200px" always>
|
||||
<li v-for="k, v in sbomItems.go">
|
||||
{{ v }}@{{ k }}
|
||||
</li>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
JavaScript dependencies:
|
||||
<div>
|
||||
<el-scrollbar height="200px" always>
|
||||
<li v-for="k, v in sbomItems.js.dependencies">
|
||||
{{ v }}@{{ k }}
|
||||
</li>
|
||||
<li v-for="k, v in sbomItems.js.devDependencies">
|
||||
{{ v }}@{{ k }}
|
||||
</li>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Cache, SetPreference, TestCaseResponse, Store, Stores} from '../cache'
|
||||
import { SetupStorage } from './common'
|
||||
|
||||
SetupStorage()
|
||||
|
||||
describe('TestCaseResponseCache', () => {
|
||||
test('should set and get test case response cache', () => {
|
||||
const id = 'test-case-id'
|
||||
const resp = {
|
||||
output: 'test-body',
|
||||
body: {},
|
||||
statusCode: 200,
|
||||
} as TestCaseResponse
|
||||
Cache.SetTestCaseResponseCache(id, resp)
|
||||
const result = Cache.GetTestCaseResponseCache(id)
|
||||
expect(result).toEqual(resp)
|
||||
})
|
||||
|
||||
test('get a non-existent test case response cache', () => {
|
||||
expect(Cache.GetTestCaseResponseCache('non-existent-id')).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('LastTestCaseLocation', () => {
|
||||
test('should get empty object when no last test case location', () => {
|
||||
expect(Cache.GetLastTestCaseLocation()).toEqual({})
|
||||
})
|
||||
|
||||
test('should set and get last test case location', () => {
|
||||
const suite = 'test-suite'
|
||||
const testcase = 'test-case'
|
||||
Cache.SetLastTestCaseLocation(suite, testcase)
|
||||
const result = Cache.GetLastTestCaseLocation()
|
||||
expect(result).toEqual({ suite, testcase })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Preference', () => {
|
||||
test('get the default preference', () => {
|
||||
expect(Cache.GetPreference()).toEqual({
|
||||
darkTheme: false,
|
||||
requestActiveTab: 'body',
|
||||
responseActiveTab: 'body',
|
||||
language: 'en',
|
||||
})
|
||||
})
|
||||
|
||||
test('set and get preference', () => {
|
||||
const preference = {
|
||||
darkTheme: true,
|
||||
requestActiveTab: 'header',
|
||||
responseActiveTab: 'header',
|
||||
language: 'zh-cn',
|
||||
}
|
||||
SetPreference(preference)
|
||||
expect(Cache.GetPreference()).toEqual(preference)
|
||||
})
|
||||
|
||||
test('set and get dark theme', () => {
|
||||
Cache.WithDarkTheme(true)
|
||||
expect(Cache.GetPreference().darkTheme).toEqual(true)
|
||||
})
|
||||
|
||||
test('set and get request active tab', () => {
|
||||
Cache.WithRequestActiveTab('request')
|
||||
expect(Cache.GetPreference().requestActiveTab).toEqual('request')
|
||||
})
|
||||
|
||||
test('set and get response active tab', () => {
|
||||
Cache.WithResponseActiveTab('response')
|
||||
expect(Cache.GetPreference().responseActiveTab).toEqual('response')
|
||||
})
|
||||
|
||||
it('set and get language', () => {
|
||||
Cache.WithLocale('zh-cn')
|
||||
expect(Cache.GetPreference().language).toEqual('zh-cn')
|
||||
})
|
||||
})
|
||||
|
||||
describe('stores', () => {
|
||||
test('should get empty object when no stores', () => {
|
||||
expect(Cache.GetCurrentStore()).toEqual({})
|
||||
})
|
||||
|
||||
test('should set and get stores', () => {
|
||||
const stores = {
|
||||
current: 'test-store',
|
||||
items: [
|
||||
{
|
||||
name: 'test-store',
|
||||
readOnly: false,
|
||||
} as Store,
|
||||
{
|
||||
name: 'read-only-store',
|
||||
readOnly: true,
|
||||
} as Store,
|
||||
],
|
||||
}
|
||||
Cache.SetStores(stores)
|
||||
expect(Cache.GetCurrentStore()).toEqual({
|
||||
name: 'test-store',
|
||||
readOnly: false,
|
||||
})
|
||||
|
||||
Cache.SetCurrentStore('read-only-store')
|
||||
expect(Cache.GetCurrentStore()).toEqual({
|
||||
name: 'read-only-store',
|
||||
readOnly: true,
|
||||
})
|
||||
|
||||
Cache.SetStores({} as Stores)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function SetupStorage() {
|
||||
const localStorageMock = (() => {
|
||||
let store = new Map<string, string>;
|
||||
|
||||
return {
|
||||
getItem(key: string) {
|
||||
return store.get(key) || null;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store.set(key, value);
|
||||
},
|
||||
removeItem(key: string) {
|
||||
store.delete(key)
|
||||
},
|
||||
clear() {
|
||||
store.clear();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(global, 'sessionStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: {
|
||||
language: 'en'
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {API} from '../net'
|
||||
import { type TestCase } from '../net'
|
||||
import { SetupStorage } from './common'
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
|
||||
fetchMock.enableMocks();
|
||||
SetupStorage()
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.resetMocks();
|
||||
});
|
||||
|
||||
describe('net', () => {
|
||||
test('GetVersion', () => {
|
||||
fetchMock.mockResponseOnce(`{"version":"v0.0.2"}`)
|
||||
API.GetVersion((d) => {
|
||||
expect(d.version).toEqual('v0.0.2')
|
||||
})
|
||||
})
|
||||
|
||||
test('CreateTestSuite', () => {
|
||||
fetchMock.mockResponseOnce(`{"version":"v0.0.1"}`)
|
||||
API.CreateTestSuite({
|
||||
store: 'store',
|
||||
name: 'name',
|
||||
api: 'api',
|
||||
kind: 'kind',
|
||||
}, (d) => {
|
||||
expect(d).toEqual({})
|
||||
}, () => {})
|
||||
})
|
||||
|
||||
test('UpdateTestSuite', () => {
|
||||
API.UpdateTestSuite({}, () => {}, () => {})
|
||||
})
|
||||
|
||||
test('GetTestSuite', () => {
|
||||
API.GetTestSuite('fake', () => {}, () => {})
|
||||
})
|
||||
|
||||
test('DeleteTestSuite', () => {
|
||||
API.DeleteTestSuite('fake', () => {}, () => {})
|
||||
})
|
||||
|
||||
test('ConvertTestSuite', () => {
|
||||
API.ConvertTestSuite('fake', 'generator', () => {}, () => {})
|
||||
})
|
||||
|
||||
test('DuplicateTestSuite', () => {
|
||||
API.DuplicateTestSuite('source', 'target', () => {}, () => {})
|
||||
})
|
||||
|
||||
test('GetTestSuiteYaml', () => {
|
||||
API.GetTestSuiteYaml('fake', () => {}, () => {})
|
||||
})
|
||||
|
||||
test('CreateTestCase', () => {
|
||||
API.CreateTestCase({
|
||||
suiteName: 'store',
|
||||
name: 'name'
|
||||
} as TestCase, (d) => {
|
||||
expect(d).toEqual({})
|
||||
}, () => {})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SupportedExtension, SupportedExtensions } from "../store";
|
||||
|
||||
describe("SupportedExtensions", () => {
|
||||
test('length check', () => {
|
||||
const extensions = SupportedExtensions()
|
||||
expect(extensions.length).toBe(9)
|
||||
})
|
||||
|
||||
for (const extension of SupportedExtensions()) {
|
||||
test(`${extension.name} check`, () => {
|
||||
expect(SupportedExtension(extension.name)).not.toBeUndefined()
|
||||
})
|
||||
}
|
||||
})
|
|
@ -1,22 +1,24 @@
|
|||
import { describe } from 'node:test'
|
||||
/*
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { NewSuggestedAPIsQuery, CreateFilter, GetHTTPMethods, FlattenObject } from '../types'
|
||||
import type { Pair } from '../types'
|
||||
import fetchMock from "jest-fetch-mock";
|
||||
|
||||
const fakeFetch: { [key:string]:string; } = {};
|
||||
function matchFake(url: string, data: string) {
|
||||
fakeFetch[url] = data
|
||||
}
|
||||
|
||||
global.fetch = jest.fn((key: string) =>
|
||||
Promise.resolve({
|
||||
json: () => {
|
||||
if (fakeFetch[key] === undefined) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
return Promise.resolve(JSON.parse(fakeFetch[key]))
|
||||
},
|
||||
}),
|
||||
) as jest.Mock;
|
||||
fetchMock.enableMocks();
|
||||
|
||||
describe('NewSuggestedAPIsQuery', () => {
|
||||
test('empty data', () => {
|
||||
|
|
|
@ -19,12 +19,12 @@ export interface TestCaseResponse {
|
|||
statusCode: number
|
||||
}
|
||||
|
||||
interface Store {
|
||||
export interface Store {
|
||||
name: string
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
interface Stores {
|
||||
export interface Stores {
|
||||
current: string
|
||||
items: Store[]
|
||||
}
|
||||
|
@ -72,8 +72,10 @@ export function GetPreference() {
|
|||
if (val && val !== '') {
|
||||
return JSON.parse(val)
|
||||
} else {
|
||||
const navLanguage = navigator.language != null ? navigator.language : 'zh-CN';
|
||||
return {
|
||||
darkTheme: false,
|
||||
language: navLanguage,
|
||||
requestActiveTab: "body",
|
||||
responseActiveTab: "body"
|
||||
} as Preference
|
||||
|
@ -85,30 +87,36 @@ export function SetPreference(preference: Preference) {
|
|||
return
|
||||
}
|
||||
|
||||
export function WatchRequestActiveTab(tab: string) {
|
||||
export function WithRequestActiveTab(tab: string) {
|
||||
const preference = GetPreference()
|
||||
preference.requestActiveTab = tab
|
||||
SetPreference(preference)
|
||||
}
|
||||
|
||||
function WatchResponseActiveTab(tab: string) {
|
||||
function WithResponseActiveTab(tab: string) {
|
||||
const preference = GetPreference()
|
||||
preference.responseActiveTab = tab
|
||||
SetPreference(preference)
|
||||
}
|
||||
|
||||
function WatchDarkTheme(darkTheme: boolean) {
|
||||
function WithDarkTheme(darkTheme: boolean) {
|
||||
const preference = GetPreference()
|
||||
preference.darkTheme = darkTheme
|
||||
SetPreference(preference)
|
||||
}
|
||||
|
||||
function WithLocale(locale: string) {
|
||||
const preference = GetPreference()
|
||||
preference.language = locale
|
||||
SetPreference(preference)
|
||||
}
|
||||
|
||||
const storeKey = "stores"
|
||||
function GetCurrentStore() {
|
||||
const val = sessionStorage.getItem(storeKey)
|
||||
if (val && val !== '') {
|
||||
const stores = JSON.parse(val)
|
||||
for (var i = 0; i < stores.items.length; i++) {
|
||||
for (let i = 0; i < stores.items.length; i++) {
|
||||
if (stores.items[i].name === stores.current) {
|
||||
return stores.items[i]
|
||||
}
|
||||
|
@ -116,6 +124,7 @@ function GetCurrentStore() {
|
|||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function SetCurrentStore(name: string) {
|
||||
const val = sessionStorage.getItem(storeKey)
|
||||
if (val && val !== '') {
|
||||
|
@ -124,6 +133,7 @@ function SetCurrentStore(name: string) {
|
|||
SetStores(stores)
|
||||
}
|
||||
}
|
||||
|
||||
function SetStores(stores: Stores | Store[]) {
|
||||
if ('current' in stores) {
|
||||
sessionStorage.setItem(storeKey, JSON.stringify(stores))
|
||||
|
@ -141,8 +151,9 @@ export const Cache = {
|
|||
GetLastTestCaseLocation,
|
||||
SetLastTestCaseLocation,
|
||||
GetPreference,
|
||||
WatchRequestActiveTab,
|
||||
WatchResponseActiveTab,
|
||||
WatchDarkTheme,
|
||||
WithRequestActiveTab,
|
||||
WithResponseActiveTab,
|
||||
WithDarkTheme,
|
||||
WithLocale,
|
||||
GetCurrentStore, SetStores, SetCurrentStore
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { watch } from 'vue'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
|
||||
function Keys(func: (() => void) | ((k: string) => void), keys: string[]) {
|
||||
const magicKeys = useMagicKeys()
|
||||
keys.forEach(k => {
|
||||
watch(magicKeys[k], (v) => {
|
||||
if (v) func(k)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const Magic = {
|
||||
Keys
|
||||
}
|
|
@ -47,10 +47,8 @@ function CreateOrUpdateStore(payload: any, create: boolean,
|
|||
})
|
||||
}
|
||||
|
||||
function ErrorTip(e: {
|
||||
statusText:''
|
||||
}) {
|
||||
ElMessage.error('Oops, ' + e.statusText)
|
||||
const ErrorTip = (e: any) => {
|
||||
ElMessage.error(e.message)
|
||||
}
|
||||
|
||||
export const UIAPI = {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023 API Testing Authors.
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,27 +15,118 @@ limitations under the License.
|
|||
*/
|
||||
import type { Pair } from './types'
|
||||
|
||||
export interface Store {
|
||||
name: string;
|
||||
link: string;
|
||||
ready: boolean;
|
||||
kind: {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
params: Pair[];
|
||||
}
|
||||
|
||||
const storeExtensions = [
|
||||
{
|
||||
name: 'atest-store-git',
|
||||
params: [{
|
||||
key: 'insecure'
|
||||
}, {
|
||||
key: 'timeout'
|
||||
}, {
|
||||
key: 'targetpath'
|
||||
}, {
|
||||
key: 'branch'
|
||||
}, {
|
||||
key: 'email',
|
||||
description: 'See also: git config --local user.email xxx@xxx.com'
|
||||
}, {
|
||||
key: 'name',
|
||||
description: 'See also: git config --local user.name xxx'
|
||||
}],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-git'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-s3',
|
||||
params: [{
|
||||
key: 'accesskeyid'
|
||||
}, {
|
||||
key: 'secretaccesskey'
|
||||
}, {
|
||||
key: 'sessiontoken'
|
||||
}, {
|
||||
key: 'region'
|
||||
}, {
|
||||
key: 'disablessl'
|
||||
}, {
|
||||
key: 'forcepathstyle'
|
||||
}, {
|
||||
key: 'bucket'
|
||||
}],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-s3'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-orm',
|
||||
params: [{
|
||||
key: 'driver',
|
||||
defaultValue: 'mysql',
|
||||
enum: ['mysql', 'postgres', 'sqlite', 'tdengine'],
|
||||
description: 'Supported: mysql, postgres, sqlite, tdengine'
|
||||
}, {
|
||||
key: 'database',
|
||||
defaultValue: 'atest'
|
||||
}, {
|
||||
key: 'historyLimit',
|
||||
defaultValue: '',
|
||||
// type: 'number',
|
||||
description: 'Set the limit of the history record count'
|
||||
}],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-orm'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-iotdb',
|
||||
params: [],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-iotdb'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-cassandra',
|
||||
params: [{
|
||||
key: 'keyspace',
|
||||
defaultValue: ''
|
||||
}],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-cassandra'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-etcd',
|
||||
params: [],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-etcd'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-redis',
|
||||
params: [],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-redis'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-mongodb',
|
||||
params: [{
|
||||
key: 'collection'
|
||||
}, {
|
||||
key: 'database',
|
||||
defaultValue: 'atest'
|
||||
}],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-mongodb'
|
||||
},
|
||||
{
|
||||
name: 'atest-store-elasticsearch',
|
||||
params: [],
|
||||
link: 'https://github.com/LinuxSuRen/atest-ext-store-elasticsearch'
|
||||
}
|
||||
] as Store[]
|
||||
|
||||
export function SupportedExtensions() {
|
||||
return [
|
||||
{
|
||||
value: 'atest-store-git',
|
||||
key: 'atest-store-git'
|
||||
},
|
||||
{
|
||||
value: 'atest-store-s3',
|
||||
key: 'atest-store-s3'
|
||||
},
|
||||
{
|
||||
value: 'atest-store-orm',
|
||||
key: 'atest-store-orm'
|
||||
},
|
||||
{
|
||||
value: 'atest-store-etcd',
|
||||
key: 'atest-store-etcd'
|
||||
},
|
||||
{
|
||||
value: 'atest-store-mongodb',
|
||||
key: 'atest-store-mongodb'
|
||||
}
|
||||
] as Pair[]
|
||||
}
|
||||
return storeExtensions
|
||||
}
|
||||
|
||||
export function SupportedExtension(name: string) {
|
||||
return storeExtensions.find(e => e.name === name)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023 API Testing Authors.
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -24,24 +24,37 @@ export interface Suite {
|
|||
spec: {
|
||||
kind: string
|
||||
url: string
|
||||
secure: {
|
||||
insecure: boolean
|
||||
}
|
||||
}
|
||||
proxy: {
|
||||
http: string
|
||||
https: string
|
||||
no: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface TestResult {
|
||||
body: string
|
||||
bodyObject: {}
|
||||
bodyText: string
|
||||
bodyLength: number
|
||||
output: string
|
||||
error: string
|
||||
statusCode: number
|
||||
header: Pair[]
|
||||
|
||||
// inner fileds
|
||||
// inner fields
|
||||
originBodyObject:{}
|
||||
}
|
||||
|
||||
export interface Pair {
|
||||
key: string
|
||||
value: string
|
||||
type: string
|
||||
defaultValue: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface TestCaseWithSuite {
|
||||
|
@ -51,6 +64,7 @@ export interface TestCaseWithSuite {
|
|||
|
||||
export interface TestCase {
|
||||
name: string
|
||||
server: string
|
||||
request: TestCaseRequest
|
||||
response: TestCaseResponse
|
||||
}
|
||||
|
@ -59,9 +73,11 @@ export interface TestCaseRequest {
|
|||
api: string
|
||||
method: string
|
||||
header: Pair[]
|
||||
cookie: Pair[]
|
||||
query: Pair[]
|
||||
form: Pair[]
|
||||
body: string
|
||||
filepath: string
|
||||
}
|
||||
|
||||
export interface TestCaseResponse {
|
||||
|
@ -86,6 +102,28 @@ export function NewSuggestedAPIsQuery(store: string, suite: string) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface SwaggerItem {
|
||||
value: string
|
||||
}
|
||||
|
||||
export function SwaggerSuggestion() {
|
||||
return function (queryString: string, cb: (arg: any) => void) {
|
||||
API.GetSwaggers((e) => {
|
||||
var swaggers = [] as SwaggerItem[]
|
||||
e.forEach((item: string) => {
|
||||
swaggers.push({
|
||||
"value": `atest://${item}`
|
||||
})
|
||||
})
|
||||
|
||||
const results = queryString ? swaggers.filter((item: SwaggerItem) => {
|
||||
return item.value.toLowerCase().indexOf(queryString.toLowerCase()) != -1
|
||||
}) : swaggers
|
||||
cb(results.slice(0, 10))
|
||||
})
|
||||
}
|
||||
}
|
||||
export function CreateFilter(queryString: string) {
|
||||
return (v: Pair) => {
|
||||
return v.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1
|
||||
|
@ -116,35 +154,51 @@ export function GetHTTPMethods() {
|
|||
return [
|
||||
{
|
||||
value: 'GET',
|
||||
key: 'GET'
|
||||
key: 'GET',
|
||||
type: ''
|
||||
},
|
||||
{
|
||||
value: 'POST',
|
||||
key: 'POST'
|
||||
key: 'POST',
|
||||
type: 'success'
|
||||
},
|
||||
{
|
||||
value: 'DELETE',
|
||||
key: 'DELETE'
|
||||
key: 'DELETE',
|
||||
type: 'danger'
|
||||
},
|
||||
{
|
||||
value: 'PUT',
|
||||
key: 'PUT'
|
||||
key: 'PUT',
|
||||
type: 'warning'
|
||||
},
|
||||
{
|
||||
value: 'HEAD',
|
||||
key: 'HEAD'
|
||||
key: 'HEAD',
|
||||
type: ''
|
||||
},
|
||||
{
|
||||
value: 'PATCH',
|
||||
key: 'PATCH'
|
||||
key: 'PATCH',
|
||||
type: ''
|
||||
},
|
||||
{
|
||||
value: 'OPTIONS',
|
||||
key: 'OPTIONS'
|
||||
key: 'OPTIONS',
|
||||
type: ''
|
||||
}
|
||||
] as Pair[]
|
||||
}
|
||||
|
||||
export function GetHTTPMethod(name: string) {
|
||||
for (const method of GetHTTPMethods()) {
|
||||
if (method.key === name) {
|
||||
return method
|
||||
}
|
||||
}
|
||||
return {} as Pair
|
||||
}
|
||||
|
||||
export function FlattenObject(obj: any): any {
|
||||
function _flattenPairs(obj: any, prefix: string): [string, any][] {
|
||||
if (!_.isObject(obj)) {
|
||||
|
@ -181,4 +235,4 @@ export function FlattenObject(obj: any): any {
|
|||
acc[pair[0]] = pair[1]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
//go:embed package.json
|
||||
var packageJSON []byte
|
||||
|
||||
type JSON struct {
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
DevDependencies map[string]string `json:"devDependencies"`
|
||||
}
|
||||
|
||||
func GetPackageJSON() (data JSON) {
|
||||
data = JSON{}
|
||||
_ = json.Unmarshal(packageJSON, &data)
|
||||
return
|
||||
}
|
|
@ -37,6 +37,14 @@ export default defineConfig({
|
|||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/server.Mock': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/mock/server': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/browser': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
|
@ -49,6 +57,10 @@ export default defineConfig({
|
|||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -324,11 +324,13 @@ atest server --auth oauth --client-id your-id --client-secret your-secret
|
|||
Developers can have storage, secret extensions. Implementing a gRPC server according to [loader.proto](../pkg/testing/remote/loader.proto) is required.
|
||||
|
||||
## Official Images
|
||||
You can find the official images from both [Docker Hub](https://hub.docker.com/r/linuxsuren/api-testing) and [GitHub Images](https://github.com/users/LinuxSuRen/packages/container/package/api-testing). See the image path:
|
||||
You can find the official images from both [Docker Hub](https://hub.docker.com/r/linuxsuren/api-testing) and others. See the image path:
|
||||
|
||||
* `ghcr.io/linuxsuren/api-testing:latest`
|
||||
* `linuxsuren/api-testing:latest`
|
||||
* `docker.m.daocloud.io/linuxsuren/api-testing` (mirror)
|
||||
* `ghcr.io/linuxsuren/api-testing:master`
|
||||
* `docker.io/linuxsuren/api-testing:master`
|
||||
* `registry.aliyuncs.com/linuxsuren/api-testing:master`
|
||||
* `ccr.ccs.tencentyun.com/linuxsuren/api-testing:master`
|
||||
* `docker.m.daocloud.io/linuxsuren/api-testing:master` (mirror)
|
||||
|
||||
The tag `latest` represents the latest release version. The tag `master` represents the image of the latest master branch. We highly recommend you to use a fixed version instead of those in a production environment.
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Mock Server Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"initCount": {"type": "integer"},
|
||||
"sample": {"type": "string"}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"request": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"method": {"type": "string"},
|
||||
"header": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"}
|
||||
},
|
||||
"body": {"type": "string"}
|
||||
},
|
||||
"required": ["path"]
|
||||
},
|
||||
"response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"encoder": {"type": "string"},
|
||||
"body": {"type": "string"},
|
||||
"header": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"}
|
||||
},
|
||||
"statusCode": {"type": "integer"},
|
||||
"bodyData": {"type": "string", "contentEncoding": "base64"}
|
||||
}
|
||||
},
|
||||
"param": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"required": ["name", "request", "response"]
|
||||
}
|
||||
},
|
||||
"proxies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"target": {"type": "string"}
|
||||
},
|
||||
"required": ["path", "target"]
|
||||
}
|
||||
},
|
||||
"webhooks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"timer": {"type": "string"},
|
||||
"request": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"method": {"type": "string"},
|
||||
"header": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"}
|
||||
},
|
||||
"body": {"type": "string"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
"required": ["name", "timer", "request"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,24 @@
|
|||
"title": "Param",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"proxy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"http": {
|
||||
"type": "string",
|
||||
"description": "HTTP proxy URL"
|
||||
},
|
||||
"https": {
|
||||
"type": "string",
|
||||
"description": "HTTPS proxy URL"
|
||||
},
|
||||
"no": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of hosts to exclude from proxying"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -204,7 +222,8 @@
|
|||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE"
|
||||
"DELETE",
|
||||
"HEAD"
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
|
@ -219,6 +238,12 @@
|
|||
"title": "Header",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"cookie": {
|
||||
"description": "HTTP request cookie",
|
||||
"type": "object",
|
||||
"title": "Cookie",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"form": {
|
||||
"description": "HTTP request form",
|
||||
"type": "object",
|
||||
|
|
|
@ -1,6 +1,46 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package docs
|
||||
|
||||
import _ "embed"
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
yamlconv "github.com/ghodss/yaml"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
)
|
||||
|
||||
//go:embed api-testing-schema.json
|
||||
var Schema string
|
||||
|
||||
//go:embed api-testing-mock-schema.json
|
||||
var MockSchema string
|
||||
|
||||
func Validate(data []byte, schema string) (err error) {
|
||||
// convert YAML to JSON
|
||||
var jsonData []byte
|
||||
if jsonData, err = yamlconv.YAMLToJSON(data); err == nil {
|
||||
schemaLoader := gojsonschema.NewStringLoader(schema)
|
||||
documentLoader := gojsonschema.NewBytesLoader(jsonData)
|
||||
|
||||
var result *gojsonschema.Result
|
||||
if result, err = gojsonschema.Validate(schemaLoader, documentLoader); err == nil {
|
||||
if !result.Valid() {
|
||||
err = fmt.Errorf("%v", result.Errors())
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
console/atest-ui/node_modules
|
||||
console/atest-ui/dist
|
||||
.git/
|
||||
bin/
|
||||
dist/
|
||||
.vscode/
|
|
@ -0,0 +1,5 @@
|
|||
/public
|
||||
resources/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.hugo_build.lock
|
|
@ -0,0 +1 @@
|
|||
lts/*
|
|
@ -0,0 +1,4 @@
|
|||
FROM klakegg/hugo:ext-alpine@sha256:536dd4805d0493ee13bf1f3df3852ed1f26d1625983507c8c56242fc029b44c7
|
||||
|
||||
RUN apk add git && \
|
||||
git config --global --add safe.directory /src
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue