Compare commits

...

199 Commits

Author SHA1 Message Date
Rick 4a26a8c711
feat: support to query next page in elastic (#663)
* feat: support to query next page in elastic

* fix the typescript syntax error

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-04-12 20:31:07 +08:00
Rick 6c9ab38114
Merge pull request #661 from LinuxSuRen/feat/elasticsearch
feat: add elasticsearch extension support
2025-04-11 15:12:21 +08:00
Rick 9d3e7ad6d4
Merge pull request #656 from LinuxSuRen/feat/cassandra
feat: add cassandra database support
2025-03-31 16:17:33 +08:00
rick 5f80de803c fix the frontend ut error 2025-03-31 07:29:33 +00:00
rick 355390b68a feat: add cassandra database support 2025-03-31 07:01:25 +00:00
Rick d96e2b5485
Merge pull request #654 from LinuxSuRen/feat/refresh-tables
feat: support to refresh tables
2025-03-28 19:04:06 +08:00
rick 13fe6ee1ce support to show the table struct 2025-03-28 10:53:51 +00:00
rick 185fb0de7e change from testSuite api from input to history-input 2025-03-28 09:56:46 +00:00
rick 2558d5dee3 feat: support to refresh tables 2025-03-28 03:42:12 +00:00
Rick 34eeb2a253
Merge pull request #653 from LinuxSuRen/feat/iotdb
feat: add iotdb support
2025-03-27 17:17:37 +08:00
rick 05d9194b43 add missing part of iotdb 2025-03-27 09:05:09 +00:00
rick 6c2cb6726f fix the ui unit testing 2025-03-27 08:40:35 +00:00
rick 4b42dadc50 fix the conflicts 2025-03-27 08:34:38 +00:00
rick 9252000e37 remove useless print statements 2025-03-27 05:56:17 +00:00
rick 739e356f5d adjust the iotdb query 2025-03-27 03:48:21 +00:00
rick c4eca6cbc1 feat: add iotdb support 2025-03-26 11:45:03 +00:00
Rick 12a0db899e
Merge pull request #648 from LinuxSuRen/feat/data-query-labels
feat: add more data as lables for the data query
2025-03-24 15:26:18 +08:00
rick 47cbd75f95 support to show the native sql 2025-03-24 06:40:28 +00:00
rick 41c9b16681 feat: add more data as lables for the data query 2025-03-19 03:10:03 +00:00
Rick 9c243c29eb
Merge pull request #641 from LinuxSuRen/feat/ui-improve-input
feat: create a history based autocomplete input
2025-03-11 16:03:44 +08:00
rick 8a1e72f23d
feat: enhance DataManager to display query duration and result count 2025-03-11 08:29:47 +08:00
rick dd08c128ca feat: support to show sql query duration 2025-03-10 09:30:28 +00:00
rick 7b8a5bbce2
chore: upgrade element-plugin to 2.9.6 2025-03-09 21:28:51 +08:00
rick 66b2f1eade
feat: create a history based autocomplete input 2025-03-08 21:11:34 +08:00
Rick 11ab39f11f
support to load swaggers from an extension (#639)
* feat: support to download differnt kinds of ext

* support to load swaggers from an extension

* docs(extension): add swagger data extension and update documentation

- Add swagger data extension to the extensions list
- Update documentation to include new extension usage

* Potential fix for code scanning alert no. 69: Arbitrary file access during archive extraction ("Zip Slip")

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>

---------

Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-03-06 23:17:57 +08:00
Rick 2f409ca68d
feat: support proxy request body amend in mock (#638)
* chore: let the sql query input be empty

* feat: support proxy request body amend in mock

* test pass with the basic feature

---------

Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-03-04 20:13:25 +08:00
Rick 5372c556df
chore: let the sql query input be empty (#637)
* chore: let the sql query input be empty

* empty the sql query when switch the store

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-03-04 19:55:45 +08:00
Rick 1372e6d21d
feat: support to show databases and tables on ui (#631)
* feat: support to show databases and tables

* support to show database and tables

* support to show databases and tables

* feat: add urlEncode and urlDecode functions to template rendering

* fix: update copyright year and add password placeholder handling in store update

* feat(DataManager): add support for multiple databases

- Add currentDatabase parameter to DataQuery function
- Update API request to include X-Database header
- Modify query object for atest-store-orm to include database key

* feat(console): enhance database manager functionality and layout

- Add queryDataFromTable and queryTables functions
- Implement database and table selection
- Adjust layout for better usability
- Optimize SQL query execution

* fix the scrollbar issues on the data manager page

* add etcd and orm compitable

* add redis key-value query support

* support to let user to choose store param from list

* update readme file

* fix the frontend unit test issues

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-03-02 16:38:13 +08:00
Rick 6908433f7e
fix: mock body render missing context (#630)
* fix: mock body render missing context

* feat: add support for repo parameter in mock response body

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-02-25 17:15:58 +08:00
Rick c8df84cbeb
refactor(console): add more HTTP status codes and proxy settings to localization files- Added new HTTP status codes to both English and Chinese localization files (#625)
- Updated proxy settings in English localization file
- Expanded HTTP status code list in Chinese localization file

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-02-24 21:28:14 +08:00
Rick cc5a463b4d
add mysql/etcd query support (#624)
* feat: add data query service

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-02-23 20:17:33 +08:00
Rick 4a38bd2188
fix: webhook auth should support go template (#623)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-02-20 09:26:55 +08:00
Rick 4b4ee3a60c
feat: add webhook bearer auth support (#619)
* feat: add webhook bearer auth support

* add tpl func randFloat

* refactor run webhook

* add template func uptimeSeconds

* add random enum with weight feature

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-02-15 14:54:17 +08:00
Rick 430d9127c6
fix: content disposition filename parsing error (#613)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-02-08 16:42:08 +08:00
Rick accdb0e4ef
feat: add metrics support to mock server (#606)
* feat: add metrics support to mock server

* add unit tests

* fix the unit testing

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-02-08 09:28:39 +08:00
Rick 664451e6ee
fix: the shell, python generator is incorrect (#601)
* fix: the shell, python generator is incorrect

* update go.sum

* update the end-of-line

* remove go.work.sum

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-01-26 10:54:25 +08:00
Rick 768b79f0c3
fix: the desktop icon is incorrect on macos (#604)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-01-26 10:18:26 +08:00
Rick 4f7f5a085a
chore: migrate operator into a new repository (#605)
* chore: migrate operator into a new repository

see also https://github.com/LinuxSuRen/atest-operator

* remove go.work

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-01-26 10:06:18 +08:00
Rick c827476bc9
[wip] feat: add http proxy support (#597)
* feat: add http proxy support

* feat: enhance server command to print local IPs on startup

- Added a new function `printLocalIPs` to display available local IP addresses when the server starts.
- Integrated the IP printing functionality into both the mock command and the server command, improving visibility of server accessibility.

This change helps users easily identify the server's available addresses for better connectivity.

* feat: support to set proxy on ui

* update the test suite page

* test pass with the http proxy mode

* update grpc files

* update grpc files

* support insecure during testing

* fix the unit tests

* add more unit testing

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2025-01-20 23:10:08 +08:00
Rick 7b25fa373a
docs: introduce how to generate a random phone number (#594)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2025-01-12 20:33:32 +08:00
Rick 73d71a3321
docs: give more examples of the verify (#590)
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2025-01-06 15:21:34 +08:00
Rick 87fe3ee61a
chore: support to print test case count in stdout (#589)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-12-24 21:27:59 +08:00
Rick efb2235754
test: add unit tests of binary upload (#587)
* test: add unit tests of binary upload

* add more http status code desc

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-12-17 22:54:19 +08:00
Rick b0d18fcdf8
fix: the ui component binding errors (#586)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-12-16 21:29:18 +08:00
Rick 7fe64b67cb
feat: add a template func to read local file (#582)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-12-14 22:13:13 +08:00
dependabot[bot] 6d754cf8bc
chore(deps): bump the npm_and_yarn group across 2 directories with 6 updates (#571)
Bumps the npm_and_yarn group with 2 updates in the /console/atest-desktop directory: [braces](https://github.com/micromatch/braces) and [cross-spawn](https://github.com/moxystudio/node-cross-spawn).
Bumps the npm_and_yarn group with 6 updates in the /console/atest-ui directory:

| Package | From | To |
| --- | --- | --- |
| [braces](https://github.com/micromatch/braces) | `3.0.2` | `3.0.3` |
| [cross-spawn](https://github.com/moxystudio/node-cross-spawn) | `6.0.5` | `6.0.6` |
| [jsonpath-plus](https://github.com/s3u/JSONPath) | `7.2.0` | `10.0.7` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `4.3.9` | `4.5.5` |
| [nanoid](https://github.com/ai/nanoid) | `3.3.6` | `3.3.8` |
| [ws](https://github.com/websockets/ws) | `8.13.0` | `8.18.0` |



Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `cross-spawn` from 6.0.5 to 7.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.5...v7.0.6)

Updates `cross-spawn` from 7.0.3 to 7.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.5...v7.0.6)

Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `cross-spawn` from 6.0.5 to 6.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.5...v7.0.6)

Updates `jsonpath-plus` from 7.2.0 to 10.0.7
- [Release notes](https://github.com/s3u/JSONPath/releases)
- [Changelog](https://github.com/JSONPath-Plus/JSONPath/blob/main/CHANGES.md)
- [Commits](https://github.com/s3u/JSONPath/compare/v7.2.0...v10.0.7)

Updates `vite` from 4.3.9 to 4.5.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.5/packages/vite)

Updates `nanoid` from 3.3.6 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8)

Updates `ws` from 8.13.0 to 8.18.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.18.0)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: braces
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: jsonpath-plus
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: ws
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 22:15:15 +08:00
dependabot[bot] 4780d26111
chore(deps): bump golang.org/x/crypto from 0.21.0 to 0.31.0 (#568)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 22:14:34 +08:00
dependabot[bot] ad9b4ba14c
chore(deps): bump github.com/spf13/cobra from 1.8.0 to 1.8.1 (#581)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 22:14:11 +08:00
Rick 66008a7b1e
chore: set package ecosystem of dependabot (#572)
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-12-12 19:48:33 +08:00
Rick 0720cef5ea
Create dependabot.yml (#570)
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-12-12 19:41:00 +08:00
dependabot[bot] 8faaf2b275
chore(deps-dev): bump happy-dom in /console/atest-ui (#559)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 6.0.4 to 15.10.2.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v6.0.4...v15.10.2)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 19:28:22 +08:00
Rick 66f724ae78
fix: the filename is missing when downloading (#566)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-12-06 20:26:06 +08:00
Rick 072cd42c16
feat: support to get headers from binary request (#565)
* feat: support to get headers from binary request

* change base image from ubuntu to alpine

* install bash in alpine

* install openssl in alpine

* fix e2e

* support to download bianry files on ui

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-29 09:04:22 +08:00
Rick 0f0298455e
feat: support to generate robot-framework script (#563)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-18 19:59:28 +08:00
Rick 33703da27b
feat: support to query verify functions (#561)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-12 10:31:31 +08:00
Rick 9e4259f09f
support to run test case with filter (#560)
* feat: support to get go mod version

* support to run test case with filter

* fix unit tests

* fix dockerfile

* add missing console/atest-ui/package.json

* fix the e2e

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-11-11 11:42:53 +08:00
Rick 19871a5d89
fix the release note link in pr template (#556)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-01 20:37:32 +08:00
Rick 6f4aef81e0
doc: add release note of v0.0.18 (#555)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-01 20:33:26 +08:00
Rick 28a32a03ca
fix: the root path of docs is incorrect (#554)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-01 19:16:45 +08:00
Rick 2db16248ef
feat: imporove mock object with standard crud (#553)
* feat: imporove mock object with standard crud

* add more document of mock

* add a common proxy on mock server

* test pass with the mock proxy

* update mock json schema

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-11-01 16:44:28 +08:00
Rick bbaf91f73c
feat: support to config mock server on ui (#552)
* doc: add document of atest extension

* feat: support to config mock server on ui

* add more document

* Fix code scanning alert no. 61: Incorrect conversion between integer types

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>

---------

Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-10-29 21:03:45 +08:00
Rick 7d9a97f46f
feat: support to rename test suite and case (#550)
* feat: support to rename test suite and case

* try to make the e2e be more stable

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-10-25 09:24:36 +08:00
shown 908fbfeb05
chore: add project years badge (#551) 2024-10-24 15:44:39 +08:00
Rick 72c53c21df
feat: support to run test case in batch mode (#548)
* feat: support to run test case in batch mode

* support to the interval on batch mode

* support to use a template as interval

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-10-15 13:47:18 +08:00
Rick dcdac9e032
fix: the grpc connect is incorrect (#546)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-10-10 16:29:42 +08:00
Rick 7de67ca381
chore: show server address on the test case page (#545)
* chore(deps): bump rollup from 3.25.1 to 3.29.5 in /console/atest-ui

Bumps [rollup](https://github.com/rollup/rollup) from 3.25.1 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.25.1...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: show server address on the test case page

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-10-09 23:17:58 +08:00
Rick fe9d168713
feat: support to print the progress when oci download (#544)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-10-05 15:41:12 +08:00
Rick af36027460
refactor: create components of the atest ui (#520) 2024-10-03 22:41:53 +08:00
Rick 3a9bde8c06
feat: support to upload random image files (#541)
* feat: support to upload random image files

* support to upload pdf file

* support to generate zip file

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-10-03 17:45:49 +08:00
Rick eff529225b
improve style of the page ui (#540)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-09-30 15:23:22 +08:00
Rick 3620c5475a
feat: support to import native data (#539)
* feat: support to import native data

* add unit tests

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-09-30 09:10:31 +08:00
Rick 834a28fc67
feat: support embed file upload (#538) 2024-09-29 15:34:17 +08:00
Rick 344ed1bb62
chore: support to close message tip (#537)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-09-26 08:03:16 +08:00
Rick 296fd4084f
feat: support to show the body size on page (#536)
increase the body size limit default value to be 5120

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-09-25 13:41:11 +08:00
SamYSF 71e1aafb1b
fix: support view history only in orm store for now (#534) 2024-09-20 23:40:44 +08:00
SamYSF 95adf09eaf
feat: add History api and ui (#524) 2024-09-18 20:41:43 +08:00
SamYSF 4aadd0611c
feat: add download extension store image prefix (#532) 2024-09-14 19:54:58 +08:00
shown 96d3b4452c
feat: add testcase debug log (#530)
* feat: add testcase debug log

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-09-14 14:00:55 +08:00
Rick e68c9961b1
fix: overwrite test case when creating duplicated (#531)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-09-12 10:08:34 +08:00
Rick d00314334b
fix: the ui loading function is not working (#527)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-09-10 19:27:26 +08:00
Rick b6188a0e68
fix: the new test case request is always empty (#526)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-09-06 10:29:53 +08:00
YuLuo 0998b2f4b6
Delete .github/workflows/stale.yml 2024-08-12 09:32:28 +08:00
Rick f295eb58b1
Delete .github/workflows/stale.yml
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-08-12 09:01:25 +08:00
Rick f63520e765
move auth module into a new project (#522)
* move auth module into a new project

* upgrade go 1.22.2. to 1.22.4

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-08-09 14:28:10 +08:00
Rick ad1521bbd1
publish api-testing mock server JSON schema (#521)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-08-09 09:36:47 +08:00
Rick b880911da6
chore: support to parse the front-end coverge (#519)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-07-30 13:34:52 +08:00
Rick d108b0fd85
chore: improve the store manager ui (#518)
* chore: improve the store manager ui

* improve the extension type selection

* add more unit tests of ts

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-07-30 09:52:35 +08:00
Rick 6e0c70e07d
feat: support session scope store (#517)
* chore: stop using the image from docker hub

* feat: support session scope store

* support to set the suggested api cout limit

* add more unit tests

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-21 11:16:43 +08:00
Rick 62383ba78b
feat: support local sercret service (#516)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-19 13:43:18 +08:00
Rick b7f05034d1
feat: improve the test case creating process on ui (#515)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-07-16 10:36:12 +08:00
Rick 1d5f54f0b9
fix: the suggested apis query is missing (#514)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-15 17:54:47 +08:00
Rick 6ce9363d5a
feat: support getting version from extension (#513)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-07-12 22:38:14 +08:00
Rick 33c89be15c
chore: remove the extension sock file before start (#512)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-07-12 09:31:24 +08:00
Rick a8c52fa891
feat: enable refelction of the extension server (#511)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-11 14:06:06 +08:00
Rick 2b98b95017
feat: support test case nav via shortcut (#510)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-10 11:28:06 +08:00
Rick 491ebdfe01
chore: add more magic keys on ui page (#509)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-07 13:08:03 +08:00
Rick ea4c0d4dbf
feat: add more shortcuts (#508)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-05 22:27:12 +08:00
Rick 03a614ad23
feat: add testCase page json lint (#506)
* feat: add testCase page json lint

* fix the test case save and run method

* add shortcut for sending request

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-05 10:45:28 +08:00
Rick 3aceced901
Add RSA encryption function to templates (#502)
* Add RSA encryption function to templates

* fix the compile errors

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-03 22:49:46 +08:00
Rick 2e50d1c4ea
ui: add colors for some texts and buttons (#504)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-03 13:46:08 +08:00
Rick d9724e8d2d
fix: the test suite tree cannot be selected (#503)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-07-02 15:29:13 +08:00
Rick b7926da2c0
feat: custom the grpc gateway restful path (#497) 2024-07-01 08:02:07 +08:00
Rick 896197110f
chore: add more content-type headers (#500)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-28 12:21:05 +08:00
Rick 157253e9c3
Add JSON schema for mock types (#499)
* Add JSON schema for mock types

* add unit tests

* fix the unit test failure

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-28 08:26:29 +08:00
Rick 6495a50f51
feat: support JSON compaitble contentType parse (#496)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-21 09:29:46 +08:00
Rick dabb95542d
feat: add http requests metrics (#495)
* feat: add http requests metrics

* add more error log

* move the api-testing-scema.json

* copy api-testing-schema.json during the docs build process

* do not ignore helm directory

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-19 14:43:33 +08:00
Rick f10f441a53
chore: add the codeOwners file (#492) 2024-06-17 20:29:50 +08:00
Rick ee312cd07f
feat: add test case execution related promethus metrics (#488) 2024-06-16 15:01:16 +08:00
YuLuo 7af3cb2007
docs: fix docs site zh show bug (#489)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-06-15 18:51:35 +08:00
Rick 605cf9addc
docs: add release note v0.0.17 (#484)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-06-13 18:12:49 +08:00
rick 7df7d75b58 fix the broken links on the home page 2024-06-13 10:05:18 +00:00
SamYSF 2ba9c11413
feat: add serviceMonitor in the helm chart (#485) 2024-06-13 17:56:01 +08:00
rick 666d4bbb8a always run docker build 2024-06-13 09:42:29 +00:00
rick c8eece849d skip run docs-check-links 2024-06-13 09:40:33 +00:00
Rick a5d8eabd4d
fix: the deployment of site has errors (#487)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-13 17:37:17 +08:00
Rick 2b332eb25a
fix: try to change the site sub-path (#486)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-13 17:22:36 +08:00
Rick f38d299bd9
fix: the docs lint errors (#481) 2024-06-13 16:55:23 +08:00
YuLuo d6162c7f2c
fix: ci error due to the docs lint (#482)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-06-12 21:14:48 +08:00
YuLuo 4a7f92c824
feat: add docs stie for api-testing (#469)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-06-12 17:43:54 +08:00
Rick e866727d53
feat: support to run test suite via http (#478)
* feat: support to run test suite via http

* improve the extension download

* download extenion in the e2e

* add extension-registry into the helm values.yaml file

* update helm install in e2e test

* fix the k8s helm chart testing

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-06-10 18:52:33 +08:00
DWJ 2e0779dfdc
feat: add TLS for gRPC server endpoint (#477) 2024-06-09 17:42:01 +08:00
Rick dd90d1e90e
fix: the file path checking is incorrect on windows (#475)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-05 14:01:28 +08:00
Rick c415efadbe
chore: improve the home dir getting (#474)
* chore: improve the home dir getting

* replace all home dir calls

* upgrade goreleaser

* add missing imports

* fix the socket filepath

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-06-05 11:01:01 +08:00
Rick 485dffc8ea
fix: cannot download oci artifact on windows (#473)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-04 14:33:41 +08:00
Rick 0cc0bb8be1
feat: support to download extensions automatically (#471)
* feat: add the extension sub-commad to download it

* do not put store-ext in the main image

* support to fetch the latest image tag

* finish the basic feature of extension downloading

* add some unit tests

* support different image registry

* add more unit tests

* remove useless files

* fix the unit test case

* simplify the apt install in the dockerfile

* add explanation for the empty methods

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-06-04 10:30:38 +08:00
WT 1735b8f326
fix: python generated code error when without HTTP headers (#465) 2024-06-01 22:09:36 +08:00
Rick c0d9a8af62
fix: missing the container exit code check (#468)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-31 21:58:53 +08:00
Rick a11b7ff550
fix: not work when grpc port is a random one (#467)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-31 13:40:57 +08:00
SamYSF b9533ee974
fix: testcase pages show badge question (#462) 2024-05-29 09:43:46 +08:00
Rick 1a720c03f8
fix: avoid exit the main process when plugin start failed (#463)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-29 09:43:28 +08:00
Rick 92d517f7b9
e2e: add e2e tests for code generators (#458)
* e2e: add e2e tests for code generators

* improve the verifying of java code generating

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-28 17:46:38 +08:00
WT 1765d139c0
fix: missing imports in generated golang code fron testcatse (#457) 2024-05-27 21:05:03 +08:00
YuLuo 775aab1d30
chore: add docs style guide links (#456)
* chore: add docs style guide links

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>

* chore: optimzie pr tmpl

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>

* Update PULL_REQUEST_TEMPLATE.md

Signed-off-by: YuLuo <yuluo08290126@gmail.com>

* add the contribution document link

Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Signed-off-by: YuLuo <yuluo08290126@gmail.com>
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
Co-authored-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-05-27 17:24:32 +08:00
Rick 00e53452fc
feat: support to duplicate test suite and case (#455)
* feat: support to duplicate test suite and case

* add e2e testing for the new apis

* fix the e2e testing

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-27 09:57:23 +08:00
Rick e7620a4a0f
feat: improve the desktop system control features (#454)
* feat: improve the desktop system control features

* add preference support

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-25 21:44:22 +08:00
Rick ce5ad55216
feat: support to return the body string even it is invalid (#453)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-24 08:36:18 +08:00
Rick 98b4dae698
chore: push the image to tencent registry (#452)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-23 20:18:15 +08:00
SamYSF e10bc5988c
feat: add a language switch button on the web page (#447) 2024-05-22 22:05:46 +08:00
DWJ 811047ada1
fix: error when opening atest-desktop on Windows (#451) 2024-05-22 17:55:56 +08:00
SamYSF ba485ce922
feat: add view YAML full screen (#443) 2024-05-20 15:24:38 +08:00
Rick 38a309f9f2
chore: cp atest to the desktop dir (#445)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-20 13:58:11 +08:00
Rick b40eea5afa
fix: some issues on darwin (#440) 2024-05-20 09:03:28 +08:00
dshyjtdes8888 1dd9dd449a
refactor: replace the go model from custom to the official (#439) 2024-05-18 20:05:04 +08:00
SamYSF 966a635768
feat: support to view testSuite as YAML on web page (#438) 2024-05-17 21:33:47 +08:00
Ziyi Li 50798ffcb5
feat: support send report to a gRPC server (#431) 2024-05-17 13:33:12 +08:00
Rick 8b2c8f289c
fix: the win config is incorrect (#437)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-17 12:43:33 +08:00
Rick 0d481b31ab
feat: support to choose install dir on win (#436)
* feat: support to choose install dir on win

* force to update the draft release

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-16 21:16:02 +08:00
Rick a503d757a6
chore: add macos-latest-large into the build matrix (#435)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-16 16:26:58 +08:00
Rick 3f7749ae66
fix: using powershell on windows (#434)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-16 09:38:30 +08:00
Rick 344694031b
fix: missing the npm install during publish (#433)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-16 09:03:35 +08:00
Rick ce31b7145a
chore: remove the prefix v in the version (#432)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-05-16 09:00:17 +08:00
Rick 95205fe4e5
fix: the dir is incorrect for publishing desktop (#430)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-15 22:32:32 +08:00
Rick d31a4c5dc9
fix: remove the not found command: tree (#429)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-15 22:27:12 +08:00
Rick f7fdbace6d
feat: add electron based desktop application (#428) 2024-05-15 22:22:57 +08:00
Rick ea033d94b6
feat: support image registry mock server (#425)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-14 12:23:44 +08:00
SamYSF 76c60b3e06
fix: cannot import sub-collection of postman (#426) 2024-05-13 15:42:19 +08:00
Rick 7896523e66
docs: remove zeabur logo due to no more support (#423)
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-05-09 16:32:17 +08:00
YuLuo 595b82620b
chore: add docker image desc label (#420)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-05-09 16:32:00 +08:00
YuLuo c2fcf50f2d
fix: ci failure when specifying container registration address (#424)
Signed-off-by: YuLuo <yuluo08290126@gmail.com>
2024-05-09 16:19:10 +08:00
YuLuo 9f400bbcb9
fix: Unified helm version number (#422)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-05-09 13:43:00 +08:00
YuLuo 369a8d28a4
ci: update release ci yml (#421)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-05-09 10:54:01 +08:00
Rick 1f0b7ca5e9
fix: using a better header struct (#418)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-09 10:14:34 +08:00
Rick f41f5a9e14
docs: add links between Chinese and English version (#417)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-05-09 10:14:22 +08:00
YuLuo fc085fe337
optimize: optimize makefile and tools (#414)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-05-08 21:27:55 +08:00
YuLuo d6485c8603
chore: delete package-lock.json which in the root dir (#413)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-05-06 10:19:35 +08:00
Rick 6bf626d72d
fix: the status code not match error ignored (#411)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-04-30 15:49:05 +08:00
Rick d344a14502
revert darwin/amd64
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-04-30 14:13:24 +08:00
Rick 02026b2199
feat: support to set the prefix of mock server (#402) (#410)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-04-30 14:06:15 +08:00
Rick ffac34fd9d
feat: support to set the prefix of mock server (#402)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-04-28 08:30:52 +08:00
Rick 72fc008ea1
Revert "chore(ci): add pull request retest action (#404)" (#406)
This reverts commit d95d2ffd73.
2024-04-26 16:34:48 +08:00
Haibara Ai 42c4eb259b
fix:Handle gRPC messages larger than max size (#399)
* fix:Handle gRPC messages larger than max size
2024-04-26 16:29:19 +08:00
YuLuo d95d2ffd73
chore(ci): add pull request retest action (#404)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-26 15:33:52 +08:00
YuLuo 2d3c1737b7
chore: rename CODE_OF_CONDUCT.md (#403)
Signed-off-by: YuLuo <yuluo08290126@gmail.com>
2024-04-26 10:40:08 +08:00
YuLuo c8378aee85
feat: add code of conduct docs (#379)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-26 09:52:09 +08:00
Kurisu 6f2fed9519
feat: Support to generator JavaScript code from a HTTP testCase. (#400)
* feat: Support to generator JavaScript code from a HTTP testCase.

* more friendly prompts in JavaScript code generator.
2024-04-26 09:48:10 +08:00
YuLuo eb1973a486
fix: golang.org/x/net bugs CVE-2023-45288 (#401)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-25 21:09:21 +08:00
zhouzhou1017 e9974374b1
feat: support to generator Python code from a HTTP testCase 2024-04-24 19:36:25 +08:00
YuLuo 02dd80fee0
chore: update readme & add community-related (#394)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-23 11:36:50 +08:00
YuLuo 9156fb85a4
chore: add security-related (#391)
* chore: add security-related

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-22 21:37:47 +08:00
Rick f4f1d4c312
feat: support combine mock and core server together (#390)
Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-04-22 21:29:52 +08:00
YuLuo 1943e7d82e
feat: add logging support (#389)
* feat: add logging support

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Signed-off-by: YuLuo <yuluo08290126@gmail.com>
Signed-off-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
Co-authored-by: Rick <1450685+LinuxSuRen@users.noreply.github.com>
2024-04-22 18:15:46 +08:00
hahahashen ea911ae8c2
feat: support write the report to a HTTP server (#367) 2024-04-22 16:28:14 +08:00
Agility6 ab38bef403
fix: incorrect HTTP request body with the Golang code generator (#383)
* fix: incorrect HTTP request body with the Golang code generator

* feat: add go body request test

* fix: go_generator error
2024-04-22 14:42:36 +08:00
YuLuo de36cc490d
chore: add issue comment (#382)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-22 11:21:52 +08:00
YuLuo ac992a8d5a
chore: move pr tmpl (#386)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-22 08:15:10 +08:00
Rick 062f96f965
feat: support detailed mock server (#377)
* feat: support detailed mock server

* make the constant be private

---------

Co-authored-by: rick <linuxsuren@users.noreply.github.com>
2024-04-21 17:49:44 +08:00
Agility6 c17b3e7c89
chore: add license header (#385) 2024-04-21 06:42:07 +08:00
YuLuo 7ef28e3127
chore(fix): update issue/config.yml file name (#381)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-20 21:51:15 +08:00
YuLuo 5ce297db68
docs: update contribution docs (#380)
* docs: update contribution docs

Add the Chinese version of the contribution document.

---------

Signed-off-by: YuLuo <yuluo08290126@gmail.com>
2024-04-20 21:47:11 +08:00
YuLuo 2bd1433b70
chore: update the PR template (#375)
* chore: update the PR template

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-20 21:30:33 +08:00
YuLuo 6159ae3739
chore: add stale bot (#378)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-20 21:23:01 +08:00
YuLuo 26599052af
chore: add issue template (#374)
* chore: add issue template

---------

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
2024-04-20 18:19:26 +08:00
Agility6 887c35ff81
feat: create a PR template (#372) 2024-04-20 17:19:05 +08:00
Rick e53a8c5892
feat: add a simple mock server (#368)
* feat: add a simple mock server

* support to render the initial data as template

---------

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-04-19 13:34:25 +08:00
Agility6 9ce40e3370
feat: support to generator Java code from a HTTP testCase (#369)
* feat: support to generator Java code from a HTTP testCase

---------

Signed-off-by: Agility6 <agility1013@gmail.com>
2024-04-19 10:14:10 +08:00
Alex Shi e05ff4b485
feat: add the cookie setting for golang code generator (#363) 2024-04-14 20:40:21 +08:00
Rick 77526e2170
docs: add a tech table for the beginner (#357)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-04-12 12:53:19 +08:00
Rick cc662bdd17
fix: the extension cannot run in startup (#356)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-04-10 10:36:03 +08:00
Rick 4322123586
feat: add cookie support for the requesting (#355)
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
2024-04-10 08:39:01 +08:00
472 changed files with 43344 additions and 12544 deletions

View File

@ -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/

15
.editorconfig Normal file
View File

@ -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

9
.gitattributes vendored Normal file
View File

@ -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

7
.github/CODEOWNERS vendored Normal file
View File

@ -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

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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."

View File

@ -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.

View File

@ -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.
>

14
.github/ISSUE_TEMPLATE/other.md vendored Normal file
View File

@ -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.

29
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -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 #

15
.github/dependabot.yml vendored Normal file
View File

@ -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"

54
.github/markdown_lint_config.json vendored Normal file
View File

@ -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
}

2
.github/pre-commit vendored
View File

@ -1,3 +1,3 @@
#!/bin/sh
make fmt test-all
make fmt test

View File

@ -52,7 +52,7 @@ change-template: '* $TITLE (#$NUMBER) @$AUTHOR'
template: |
## Whats 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

View File

@ -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}}"}

View File

@ -8,6 +8,7 @@ spec:
rpc:
import:
- ./pkg/server
- ./pkg/apispec/data/proto
protofile: server.proto
items:
- name: GetVersion

View File

@ -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"

View File

@ -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.

View File

@ -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

99
.github/workflows/docs.yaml vendored Normal file
View File

@ -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

33
.github/workflows/issue-comment.yml vendored Normal file
View File

@ -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 }}"

View File

@ -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

View File

@ -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:

View File

@ -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

8
.gitignore vendored
View File

@ -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

29
CODE_OF_CONDUCT.md Normal file
View File

@ -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 others 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/)

95
CONTRIBUTING-ZH.md Normal file
View File

@ -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`.

95
CONTRIBUTING.md Normal file
View File

@ -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`

View File

@ -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`

View File

@ -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
View File

@ -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)

141
README-ZH.md Normal file
View File

@ -0,0 +1,141 @@
[![CLA assistant](https://cla-assistant.io/readme/badge/LinuxSuRen/api-testing)](https://cla-assistant.io/LinuxSuRen/api-testing)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/3f16717cd6f841118006f12c346e9341)](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/3f16717cd6f841118006f12c346e9341)](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![GitHub All Releases](https://img.shields.io/github/downloads/linuxsuren/api-testing/total)](https://tooomm.github.io/github-release-stats/?username=linuxsuren&repository=api-testing)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxsuren/api-testing)](https://hub.docker.com/r/linuxsuren/api-testing)
[![LinuxSuRen/open-source-best-practice](https://img.shields.io/static/v1?label=OSBP&message=%E5%BC%80%E6%BA%90%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5&color=blue)](https://github.com/LinuxSuRen/open-source-best-practice)
![GitHub Created At](https://img.shields.io/github/created-at/linuxsuren/api-testing)
> 中文 | [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 监控
## 快速开始
[![Try in PWD](https://github.com/play-with-docker/stacks/raw/cff22438cb4195ace27f9b15784bbb497047afa7/assets/images/button.png)](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
View File

@ -1,122 +1,141 @@
[![CLA assistant](https://cla-assistant.io/readme/badge/LinuxSuRen/api-testing)](https://cla-assistant.io/LinuxSuRen/api-testing)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/3f16717cd6f841118006f12c346e9341)](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/3f16717cd6f841118006f12c346e9341)](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![GitHub All Releases](https://img.shields.io/github/downloads/linuxsuren/api-testing/total)](https://tooomm.github.io/github-release-stats/?username=linuxsuren&repository=api-testing)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxsuren/api-testing)](https://hub.docker.com/r/linuxsuren/api-testing)
[![LinuxSuRen/open-source-best-practice](https://img.shields.io/static/v1?label=OSBP&message=%E5%BC%80%E6%BA%90%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5&color=blue)](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
[![Deployed on Zeabur](https://zeabur.com/deployed-on-zeabur-dark.svg)](https://zeabur.com?referralCode=LinuxSuRen&utm_source=LinuxSuRen&utm_campaign=oss) [![Try in PWD](https://github.com/play-with-docker/stacks/raw/cff22438cb4195ace27f9b15784bbb497047afa7/assets/images/button.png)](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
[![CLA assistant](https://cla-assistant.io/readme/badge/LinuxSuRen/api-testing)](https://cla-assistant.io/LinuxSuRen/api-testing)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/3f16717cd6f841118006f12c346e9341)](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/3f16717cd6f841118006f12c346e9341)](https://app.codacy.com/gh/LinuxSuRen/api-testing/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![GitHub All Releases](https://img.shields.io/github/downloads/linuxsuren/api-testing/total)](https://tooomm.github.io/github-release-stats/?username=linuxsuren&repository=api-testing)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxsuren/api-testing)](https://hub.docker.com/r/linuxsuren/api-testing)
[![LinuxSuRen/open-source-best-practice](https://img.shields.io/static/v1?label=OSBP&message=%E5%BC%80%E6%BA%90%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5&color=blue)](https://github.com/LinuxSuRen/open-source-best-practice)
![GitHub Created At](https://img.shields.io/github/created-at/linuxsuren/api-testing)
> 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
[![Try in PWD](https://github.com/play-with-docker/stacks/raw/cff22438cb4195ace27f9b15784bbb497047afa7/assets/images/button.png)](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)

18
SECURITY.md Normal file
View File

@ -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, dont 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.

View File

@ -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})

91
cmd/extension.go Normal file
View File

@ -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
}

73
cmd/extension_test.go Normal file
View File

@ -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)
})
}

101
cmd/mock.go Normal file
View File

@ -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
}

65
cmd/mock_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -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)
}

12
cmd/testdata/stores.yaml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
});
})

View File

@ -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
}
}
]
};

View File

@ -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>

View File

@ -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.

9622
console/atest-desktop/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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

View File

@ -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"
},

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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 网关超时"
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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
>

View File

@ -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
>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
})
})

View File

@ -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'
}
});
}

View File

@ -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({})
}, () => {})
})
})

View File

@ -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()
})
}
})

View File

@ -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', () => {

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}, {})
}
}

20
console/atest-ui/ui.go Normal file
View File

@ -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
}

View File

@ -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,
},
},
},
})

View File

@ -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.

View File

@ -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"]
}
}
}
}

View File

@ -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",

View File

@ -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
}

6
docs/site/.dockerignore Normal file
View File

@ -0,0 +1,6 @@
console/atest-ui/node_modules
console/atest-ui/dist
.git/
bin/
dist/
.vscode/

5
docs/site/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/public
resources/
node_modules/
package-lock.json
.hugo_build.lock

1
docs/site/.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/*

4
docs/site/Dockerfile Normal file
View File

@ -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