diff --git a/Dockerfile b/Dockerfile
index 511e5110..0b7b9232 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,8 @@ 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/
@@ -21,6 +23,8 @@ 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
diff --git a/cmd/run.go b/cmd/run.go
index f9a40a47..dabc995b 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -51,6 +51,7 @@ type runOption struct {
duration time.Duration
requestTimeout time.Duration
requestIgnoreError bool
+ caseFilter string
thread int64
context context.Context
qps int32
@@ -116,6 +117,7 @@ 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.caseFilter, "case-filter", "", "", "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")
@@ -130,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://") {
@@ -345,8 +353,15 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter
suiteRunner.WithOutputWriter(os.Stdout)
suiteRunner.WithWriteLevel(o.level)
suiteRunner.WithSuite(testSuite)
- runLogger.Info("run test suite", "name", testSuite.Name)
+ 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 && !strings.Contains(testCase.Name, caseFilterObj.(string)) {
+ continue
+ }
if !testCase.InScope(o.caseItems) {
continue
}
diff --git a/cmd/server.go b/cmd/server.go
index 11e5c78f..c104821b 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -337,6 +337,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
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)
postRequestProxyFunc := postRequestProxy(o.skyWalking)
mux.HandlePath(http.MethodPost, "/browser/{app}", postRequestProxyFunc)
diff --git a/console/atest-ui/src/views/TemplateFunctions.vue b/console/atest-ui/src/views/TemplateFunctions.vue
index 7cd2c5b7..956ca619 100644
--- a/console/atest-ui/src/views/TemplateFunctions.vue
+++ b/console/atest-ui/src/views/TemplateFunctions.vue
@@ -56,5 +56,8 @@ Magic.Keys(() => {
+
diff --git a/console/atest-ui/src/views/WelcomePage.vue b/console/atest-ui/src/views/WelcomePage.vue
index 4219d595..e53f49c1 100644
--- a/console/atest-ui/src/views/WelcomePage.vue
+++ b/console/atest-ui/src/views/WelcomePage.vue
@@ -1,3 +1,20 @@
+
+
Welcome to use atest to improve your code quality!
Please read the following guide if this is your first time to use atest.
@@ -8,4 +25,31 @@
+
+
+
+
+ Golang dependencies:
+
+
+
+ {{ v }}@{{ k }}
+
+
+
+
+
+
+ JavaScript dependencies:
+
+
+
+ {{ v }}@{{ k }}
+
+
+ {{ v }}@{{ k }}
+
+
+
+
diff --git a/console/atest-ui/src/views/net.ts b/console/atest-ui/src/views/net.ts
index 58aad000..5fb6aae9 100644
--- a/console/atest-ui/src/views/net.ts
+++ b/console/atest-ui/src/views/net.ts
@@ -767,6 +767,11 @@ function DownloadResponseFile(testcase,
.then(callback).catch(errHandle)
}
+var SBOM = (callback: (d: any) => void) => {
+ fetch(`/api/sbom`, {})
+ .then(DefaultResponseProcess)
+ .then(callback)
+}
export const API = {
DefaultResponseProcess,
@@ -780,6 +785,6 @@ export const API = {
FunctionsQuery,
GetSecrets, DeleteSecret, CreateOrUpdateSecret,
GetSuggestedAPIs,
- ReloadMockServer, GetMockConfig,
+ ReloadMockServer, GetMockConfig, SBOM,
getToken
}
diff --git a/console/atest-ui/ui.go b/console/atest-ui/ui.go
new file mode 100644
index 00000000..aa6ff641
--- /dev/null
+++ b/console/atest-ui/ui.go
@@ -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
+}
diff --git a/docs/site/content/zh/latest/tasks/quickstart.md b/docs/site/content/zh/latest/tasks/quickstart.md
index 0ad04203..2d86448b 100644
--- a/docs/site/content/zh/latest/tasks/quickstart.md
+++ b/docs/site/content/zh/latest/tasks/quickstart.md
@@ -6,4 +6,10 @@ description: 只需几个简单的步骤即可开始使用 API Testing。
本指南将帮助您通过几个简单的步骤开始使用 API Testing。
-// TBD
+## 执行部分测试用例
+
+下面的命令会执行名称中包含 `sbom` 的所有测试用例:
+
+```shell
+atest run -p test-suite.yaml --case-filter sbom
+```
diff --git a/docs/site/content/zh/latest/tasks/verify.md b/docs/site/content/zh/latest/tasks/verify.md
index e3e0d984..131a7c00 100644
--- a/docs/site/content/zh/latest/tasks/verify.md
+++ b/docs/site/content/zh/latest/tasks/verify.md
@@ -16,3 +16,30 @@ title = "测试用例验证"
verify:
- len(data.data) == 6
```
+
+## 数组值检查
+
+```yaml
+- name: popularHeaders
+ request:
+ api: /popularHeaders
+ expect:
+ verify:
+ - any(data.data, {.key == "Content-Type"})
+```
+
+[更多用法](https://expr-lang.org/docs/language-definition#any).
+
+## 字符串判断
+
+```yaml
+- name: metrics
+ request:
+ api: |
+ {{.param.server}}/metrics
+ expect:
+ verify:
+ - indexOf(data, "atest_execution_count") != -1
+```
+
+[更多用法](https://expr-lang.org/docs/language-definition#indexOf).
diff --git a/e2e/test-suite-common.yaml b/e2e/test-suite-common.yaml
index 27d6d5fb..54a088fd 100644
--- a/e2e/test-suite-common.yaml
+++ b/e2e/test-suite-common.yaml
@@ -376,3 +376,12 @@ items:
- indexOf(data, "atest_execution_success") != -1
- indexOf(data, "atest_runners_count") != -1
- indexOf(data, "http_requests_total") != -1
+
+- name: sbom
+ request:
+ api: /sbom
+ expect:
+ verify:
+ - len(data.go) > 0
+ - len(data.js.dependencies) > 0
+ - len(data.js.devDependencies) > 0
diff --git a/go.mod b/go.mod
index 530e6eb9..55d22a49 100644
--- a/go.mod
+++ b/go.mod
@@ -41,6 +41,8 @@ require (
gopkg.in/yaml.v3 v3.0.1
)
+require golang.org/x/mod v0.22.0 // indirect
+
require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
diff --git a/go.sum b/go.sum
index 0a1907cf..f663f032 100644
--- a/go.sum
+++ b/go.sum
@@ -224,6 +224,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
diff --git a/go.work.sum b/go.work.sum
index c33b60cd..24f0e71c 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -2154,6 +2154,7 @@ google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9
google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA=
google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk=
google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
diff --git a/main.go b/main.go
index 693f7861..fed4097e 100644
--- a/main.go
+++ b/main.go
@@ -1,17 +1,22 @@
package main
import (
+ _ "embed"
+ "github.com/linuxsuren/api-testing/pkg/version"
"os"
- // _ "github.com/apache/skywalking-go"
"github.com/linuxsuren/api-testing/cmd"
"github.com/linuxsuren/api-testing/pkg/server"
exec "github.com/linuxsuren/go-fake-runtime"
)
func main() {
+ version.SetMod(goMod)
c := cmd.NewRootCmd(exec.NewDefaultExecer(), server.NewDefaultHTTPServer())
if err := c.Execute(); err != nil {
os.Exit(1)
}
}
+
+//go:embed go.mod
+var goMod string
diff --git a/pkg/render/template.go b/pkg/render/template.go
index 4cfa43b3..9c3b5734 100644
--- a/pkg/render/template.go
+++ b/pkg/render/template.go
@@ -26,6 +26,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
+ "github.com/linuxsuren/api-testing/pkg/version"
"io"
mathrand "math/rand"
"strings"
@@ -193,6 +194,11 @@ func GetAdvancedFuncs() []AdvancedFunc {
return advancedFuncs
}
+func GetEngineVersion() (ver string) {
+ ver, _ = version.GetModVersion("github.com/Masterminds/sprig", "")
+ return
+}
+
func generateJSONString(fields []string) (result string) {
data := make(map[string]string)
for _, item := range fields {
diff --git a/pkg/render/template_test.go b/pkg/render/template_test.go
index 78ab7aa0..543fe813 100644
--- a/pkg/render/template_test.go
+++ b/pkg/render/template_test.go
@@ -289,3 +289,8 @@ func TestFuncUsages(t *testing.T) {
assert.NotEmpty(t, usage)
}
}
+
+func TestGetEngineVersion(t *testing.T) {
+ ver := GetEngineVersion()
+ assert.Empty(t, ver)
+}
diff --git a/pkg/runner/http.go b/pkg/runner/http.go
index 36aced22..200dd24b 100644
--- a/pkg/runner/http.go
+++ b/pkg/runner/http.go
@@ -176,9 +176,8 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
Value: v,
})
}
- r.log.Info("request method: %s\n", request.Method)
+ r.log.Info("start to send request to %v with method %s\n", request.URL, request.Method)
r.log.Info("request header %v\n", request.Header)
- r.log.Info("start to send request to %v\n", request.URL)
// TODO only do this for unit testing, should remove it once we have a better way
if strings.HasPrefix(testcase.Request.API, "http://") {
diff --git a/pkg/server/remote_server_test.go b/pkg/server/remote_server_test.go
index 6975aced..3fbcfbe0 100644
--- a/pkg/server/remote_server_test.go
+++ b/pkg/server/remote_server_test.go
@@ -148,7 +148,7 @@ func TestRunTestCase(t *testing.T) {
})
assert.NoError(t, err)
assert.Equal(t, sampleBody, result.Body)
- assert.Contains(t, result.Output, "request method: GET")
+ assert.Contains(t, result.Output, "with method GET")
assert.Contains(t, result.Output, "request header")
assert.Contains(t, result.Output, "start to send request to http://foo")
assert.Contains(t, result.Output, "test case \"get\", status code: 200")
diff --git a/pkg/service/sbom.go b/pkg/service/sbom.go
new file mode 100644
index 00000000..bfe37fea
--- /dev/null
+++ b/pkg/service/sbom.go
@@ -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.
+*/
+
+package service
+
+import (
+ "net/http"
+
+ ui "github.com/linuxsuren/api-testing/console/atest-ui"
+ "github.com/linuxsuren/api-testing/pkg/util"
+ "github.com/linuxsuren/api-testing/pkg/version"
+)
+
+func SBomHandler(w http.ResponseWriter, r *http.Request,
+ params map[string]string) {
+ modMap, err := version.GetModVersions("")
+ packageJSON := ui.GetPackageJSON()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ } else {
+ data := make(map[string]interface{})
+ data["js"] = packageJSON
+ data["go"] = modMap
+ util.WriteAsJSON(data, w)
+ }
+}
diff --git a/pkg/util/http.go b/pkg/util/http.go
index b61c0705..4176d906 100644
--- a/pkg/util/http.go
+++ b/pkg/util/http.go
@@ -18,6 +18,7 @@ package util
import (
"bytes"
"crypto/tls"
+ "encoding/json"
"io"
"net/http"
)
@@ -68,3 +69,12 @@ func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) {
return resp, err
}
+
+func WriteAsJSON(obj interface{}, w http.ResponseWriter) (n int, err error) {
+ w.Header().Set(ContentType, JSON)
+ var data []byte
+ if data, err = json.Marshal(obj); err == nil {
+ n, err = w.Write(data)
+ }
+ return
+}
diff --git a/pkg/version/mod.go b/pkg/version/mod.go
new file mode 100644
index 00000000..448a00a3
--- /dev/null
+++ b/pkg/version/mod.go
@@ -0,0 +1,57 @@
+/*
+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 version
+
+import (
+ "golang.org/x/mod/modfile"
+)
+
+var goMod string
+
+func SetMod(mod string) {
+ goMod = mod
+}
+
+func GetModVersions(mod string) (verMap map[string]string, err error) {
+ if mod == "" {
+ mod = goMod
+ }
+
+ var f *modfile.File
+ f, err = modfile.Parse("go.mod", []byte(mod), nil)
+ if err != nil {
+ return
+ }
+
+ verMap = make(map[string]string, len(f.Require))
+ for _, req := range f.Require {
+ verMap[req.Mod.Path] = req.Mod.Version
+ }
+ return
+}
+
+func GetModVersion(name, mod string) (ver string, err error) {
+ var verMap map[string]string
+ if verMap, err = GetModVersions(mod); err == nil {
+ for k, v := range verMap {
+ if k == name {
+ ver = v
+ break
+ }
+ }
+ }
+ return
+}
diff --git a/pkg/version/mod_test.go b/pkg/version/mod_test.go
new file mode 100644
index 00000000..019ddbfb
--- /dev/null
+++ b/pkg/version/mod_test.go
@@ -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.
+*/
+package version
+
+import (
+ _ "embed"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestGetModVersion(t *testing.T) {
+ t.Run("empty mod", func(t *testing.T) {
+ SetMod("")
+ _, err := GetModVersion("", "")
+ assert.NoError(t, err)
+ })
+
+ t.Run("a simple mod", func(t *testing.T) {
+ ver, err := GetModVersion("github.com/a/b", simpleMod)
+ assert.NoError(t, err)
+ assert.Equal(t, "v0.0.1", ver)
+ })
+
+ t.Run("not found in mod", func(t *testing.T) {
+ ver, err := GetModVersion("github.com/a/b/c", simpleMod)
+ assert.NoError(t, err)
+ assert.Equal(t, "", ver)
+ })
+
+ t.Run("invalid mod", func(t *testing.T) {
+ _, err := GetModVersion("github.com/a/b", `invalid`)
+ assert.Error(t, err)
+ })
+}
+
+//go:embed testdata/go.mod.txt
+var simpleMod string
diff --git a/pkg/version/testdata/go.mod.txt b/pkg/version/testdata/go.mod.txt
new file mode 100644
index 00000000..8019431b
--- /dev/null
+++ b/pkg/version/testdata/go.mod.txt
@@ -0,0 +1,7 @@
+module github.com/linuxsuren/api-testing
+
+go 1.22.4
+
+require (
+ github.com/a/b v0.0.1
+)