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(() => { +
+ Powered by Sprig and built-in templates. +
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 @@ + + 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 +)