diff --git a/cmd/extension.go b/cmd/extension.go
index 6d04e6c7..b61ca027 100644
--- a/cmd/extension.go
+++ b/cmd/extension.go
@@ -31,6 +31,7 @@ type extensionOption struct {
ociDownloader downloader.PlatformAwareOCIDownloader
output string
registry string
+ kind string
tag string
os string
arch string
@@ -53,6 +54,7 @@ func createExtensionCommand(ociDownloader downloader.PlatformAwareOCIDownloader)
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")
@@ -66,6 +68,7 @@ func (o *extensionOption) runE(cmd *cobra.Command, args []string) (err error) {
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 {
diff --git a/cmd/server.go b/cmd/server.go
index 8a904d1c..35f63744 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -22,6 +22,7 @@ import (
"context"
"errors"
"fmt"
+ "github.com/linuxsuren/api-testing/pkg/apispec"
"net"
"net/http"
"os"
@@ -302,6 +303,15 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
_ = o.httpServer.Shutdown(ctx)
}()
+ 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{
@@ -342,6 +352,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
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)
diff --git a/console/atest-ui/src/views/TestSuite.vue b/console/atest-ui/src/views/TestSuite.vue
index d4c9806b..3489d8c3 100644
--- a/console/atest-ui/src/views/TestSuite.vue
+++ b/console/atest-ui/src/views/TestSuite.vue
@@ -4,7 +4,7 @@ import { reactive, ref, watch } from '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 { Cache } from './cache'
import { useI18n } from 'vue-i18n'
@@ -20,6 +20,7 @@ const props = defineProps({
})
const emit = defineEmits(['updated'])
let querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name, props.name!)
+const querySwaggers = SwaggerSuggestion()
const suite = ref({
name: '',
@@ -325,7 +326,10 @@ const renameTestSuite = (name: string) => {
-
+
|
diff --git a/console/atest-ui/src/views/net.ts b/console/atest-ui/src/views/net.ts
index 6553e27a..ee632ebf 100644
--- a/console/atest-ui/src/views/net.ts
+++ b/console/atest-ui/src/views/net.ts
@@ -620,6 +620,12 @@ function GetSuggestedAPIs(name: string,
.then(callback)
}
+function GetSwaggers(callback: (d: any) => void) {
+ fetch(`/api/v1/swaggers`, {})
+ .then(DefaultResponseProcess)
+ .then(callback)
+}
+
function ReloadMockServer(config: any) {
const requestOptions = {
method: 'POST',
@@ -812,7 +818,7 @@ export const API = {
CreateOrUpdateStore, GetStores, DeleteStore, VerifyStore,
FunctionsQuery,
GetSecrets, DeleteSecret, CreateOrUpdateSecret,
- GetSuggestedAPIs,
+ GetSuggestedAPIs, GetSwaggers,
ReloadMockServer, GetMockConfig, SBOM, DataQuery,
getToken
}
diff --git a/console/atest-ui/src/views/types.ts b/console/atest-ui/src/views/types.ts
index dd97ff7b..66afe234 100644
--- a/console/atest-ui/src/views/types.ts
+++ b/console/atest-ui/src/views/types.ts
@@ -102,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
diff --git a/docs/site/content/zh/latest/tasks/extension.md b/docs/site/content/zh/latest/tasks/extension.md
index 0820d86f..acb0e72a 100644
--- a/docs/site/content/zh/latest/tasks/extension.md
+++ b/docs/site/content/zh/latest/tasks/extension.md
@@ -31,3 +31,9 @@ atest extension orm
```shell
atest extension orm --registry ghcr.io --timeout 2ms
```
+
+想要下载其他类型的插件的话,可以使用下面的命令:
+
+```shell
+atest extension --kind data swagger
+```
diff --git a/extensions/README.md b/extensions/README.md
index b94dd1ae..f6ca271f 100644
--- a/extensions/README.md
+++ b/extensions/README.md
@@ -11,6 +11,7 @@ Ports in extensions:
| Monitor | [docker-monitor](https://github.com/LinuxSuRen/atest-ext-monitor-docker) | |
| Agent | [collector](https://github.com/LinuxSuRen/atest-ext-collector) | |
| Secret | [Vault](https://github.com/LinuxSuRen/api-testing-vault-extension) | |
+| Data | [Swagger](https://github.com/LinuxSuRen/atest-ext-data-swagger) | |
## Contribute a new extension
diff --git a/pkg/apispec/remote_swagger.go b/pkg/apispec/remote_swagger.go
new file mode 100644
index 00000000..4504e785
--- /dev/null
+++ b/pkg/apispec/remote_swagger.go
@@ -0,0 +1,134 @@
+/*
+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 apispec
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "encoding/json"
+ "fmt"
+ "github.com/linuxsuren/api-testing/pkg/downloader"
+ "github.com/linuxsuren/api-testing/pkg/util/home"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func DownloadSwaggerData(output string, dw downloader.PlatformAwareOCIDownloader) (err error) {
+ dw.WithKind("data")
+ dw.WithOS("")
+
+ var reader io.Reader
+ if reader, err = dw.Download("swagger", "", ""); err != nil {
+ return
+ }
+
+ extFile := dw.GetTargetFile()
+
+ if output == "" {
+ output = home.GetUserDataDir()
+ }
+ if err = os.MkdirAll(filepath.Dir(output), 0755); err != nil {
+ return
+ }
+
+ targetFile := filepath.Base(extFile)
+ fmt.Println("start to save", filepath.Join(output, targetFile))
+ if err = downloader.WriteTo(reader, output, targetFile); err == nil {
+ err = decompressData(filepath.Join(output, targetFile))
+ }
+ return
+}
+
+func SwaggersHandler(w http.ResponseWriter, _ *http.Request,
+ _ map[string]string) {
+ swaggers := GetSwaggerList()
+ if data, err := json.Marshal(swaggers); err == nil {
+ _, _ = w.Write(data)
+ } else {
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+}
+
+func GetSwaggerList() (swaggers []string) {
+ dataDir := home.GetUserDataDir()
+ _ = filepath.WalkDir(dataDir, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if !d.IsDir() && filepath.Ext(path) == ".json" {
+ swaggers = append(swaggers, filepath.Base(path))
+ }
+ return nil
+ })
+ return
+}
+
+func decompressData(dataFile string) (err error) {
+ var file *os.File
+ file, err = os.Open(dataFile)
+ if err != nil {
+ return
+ }
+ defer file.Close()
+
+ var gzipReader *gzip.Reader
+ gzipReader, err = gzip.NewReader(file)
+ if err != nil {
+ return
+ }
+ defer gzipReader.Close()
+
+ tarReader := tar.NewReader(gzipReader)
+
+ for {
+ header, err := tarReader.Next()
+ if err == io.EOF {
+ break // 退出循环
+ }
+ if err != nil {
+ panic(err)
+ }
+
+ // Ensure the file path does not contain directory traversal sequences
+ if strings.Contains(header.Name, "..") {
+ fmt.Printf("Skipping entry with unsafe path: %s\n", header.Name)
+ continue
+ }
+
+ destPath := filepath.Join(filepath.Dir(dataFile), filepath.Base(header.Name))
+
+ switch header.Typeflag {
+ case tar.TypeReg:
+ destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
+ if err != nil {
+ panic(err)
+ }
+ defer destFile.Close()
+
+ if _, err := io.Copy(destFile, tarReader); err != nil {
+ panic(err)
+ }
+ default:
+ fmt.Printf("Skipping entry type %c: %s\n", header.Typeflag, header.Name)
+ }
+ }
+ return
+}
diff --git a/pkg/apispec/swagger.go b/pkg/apispec/swagger.go
index 2b305365..958a8755 100644
--- a/pkg/apispec/swagger.go
+++ b/pkg/apispec/swagger.go
@@ -18,8 +18,11 @@ package apispec
import (
"github.com/go-openapi/spec"
+ "github.com/linuxsuren/api-testing/pkg/util/home"
"io"
"net/http"
+ "os"
+ "path/filepath"
"regexp"
"strings"
)
@@ -123,6 +126,12 @@ func ParseToSwagger(data []byte) (swagger *spec.Swagger, err error) {
}
func ParseURLToSwagger(swaggerURL string) (swagger *spec.Swagger, err error) {
+ if strings.HasPrefix(swaggerURL, "atest://") {
+ swaggerURL = strings.ReplaceAll(swaggerURL, "atest://", "")
+ swagger, err = ParseFileToSwagger(filepath.Join(home.GetUserDataDir(), swaggerURL))
+ return
+ }
+
var resp *http.Response
if resp, err = http.Get(swaggerURL); err == nil && resp != nil && resp.StatusCode == http.StatusOK {
swagger, err = ParseStreamToSwagger(resp.Body)
@@ -130,6 +139,14 @@ func ParseURLToSwagger(swaggerURL string) (swagger *spec.Swagger, err error) {
return
}
+func ParseFileToSwagger(dataFile string) (swagger *spec.Swagger, err error) {
+ var data []byte
+ if data, err = os.ReadFile(dataFile); err == nil {
+ swagger, err = ParseToSwagger(data)
+ }
+ return
+}
+
func ParseStreamToSwagger(stream io.Reader) (swagger *spec.Swagger, err error) {
var data []byte
if data, err = io.ReadAll(stream); err == nil {
diff --git a/pkg/downloader/store.go b/pkg/downloader/extension.go
similarity index 59%
rename from pkg/downloader/store.go
rename to pkg/downloader/extension.go
index 2d4256e6..62b30b68 100644
--- a/pkg/downloader/store.go
+++ b/pkg/downloader/extension.go
@@ -1,5 +1,5 @@
/*
-Copyright 2024 API Testing Authors.
+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.
@@ -25,30 +25,37 @@ import (
"strings"
)
-type storeDownloader struct {
+type extensionDownloader struct {
OCIDownloader
os, arch string
+ kind string
extFile string
imagePrefix string
}
func NewStoreDownloader() PlatformAwareOCIDownloader {
- ociDownloader := &storeDownloader{
+ ociDownloader := &extensionDownloader{
OCIDownloader: NewDefaultOCIDownloader(),
}
ociDownloader.WithOS(runtime.GOOS)
ociDownloader.WithArch(runtime.GOARCH)
ociDownloader.WithImagePrefix("linuxsuren")
+ ociDownloader.WithKind("store")
return ociDownloader
}
-func (d *storeDownloader) Download(name, tag, _ string) (reader io.Reader, err error) {
- name = strings.TrimPrefix(name, "atest-store-")
- d.extFile = fmt.Sprintf("atest-store-%s_%s_%s/atest-store-%s", name, d.os, d.arch, name)
- if d.os == "windows" {
- d.extFile = fmt.Sprintf("%s.exe", d.extFile)
+func (d *extensionDownloader) Download(name, tag, _ string) (reader io.Reader, err error) {
+ name = strings.TrimPrefix(name, fmt.Sprintf("atest-%s-", d.kind))
+ if d.os == "" {
+ d.extFile = fmt.Sprintf("atest-%s-%s.tar.gz", d.kind, name)
+ } else {
+ d.extFile = fmt.Sprintf("atest-%s-%s_%s_%s/atest-%s-%s", d.kind, name, d.os, d.arch, d.kind, name)
+ if d.os == "windows" {
+ d.extFile = fmt.Sprintf("%s.exe", d.extFile)
+ }
}
- image := fmt.Sprintf("%s/atest-ext-store-%s", d.imagePrefix, name)
+
+ image := fmt.Sprintf("%s/atest-ext-%s-%s", d.imagePrefix, d.kind, name)
reader, err = d.OCIDownloader.Download(image, tag, d.extFile)
return
}
@@ -64,21 +71,25 @@ func WriteTo(reader io.Reader, dir, file string) (err error) {
return
}
-func (d *storeDownloader) GetTargetFile() string {
+func (d *extensionDownloader) GetTargetFile() string {
return d.extFile
}
-func (d *storeDownloader) WithOS(os string) {
+func (d *extensionDownloader) WithOS(os string) {
d.os = os
}
-func (d *storeDownloader) WithImagePrefix(imagePrefix string) {
+func (d *extensionDownloader) WithImagePrefix(imagePrefix string) {
d.imagePrefix = imagePrefix
}
-func (d *storeDownloader) WithArch(arch string) {
+func (d *extensionDownloader) WithArch(arch string) {
d.arch = arch
if d.arch == "amd64" {
d.arch = "amd64_v1"
}
}
+
+func (d *extensionDownloader) WithKind(kind string) {
+ d.kind = kind
+}
diff --git a/pkg/downloader/oci.go b/pkg/downloader/oci.go
index 558c024d..304f8cfc 100644
--- a/pkg/downloader/oci.go
+++ b/pkg/downloader/oci.go
@@ -45,6 +45,7 @@ type PlatformAwareOCIDownloader interface {
WithOS(string)
WithArch(string)
GetTargetFile() string
+ WithKind(string)
WithImagePrefix(string)
}
@@ -71,6 +72,8 @@ func (d *defaultOCIDownloader) WithBasicAuth(username string, password string) {
}
func (d *defaultOCIDownloader) Download(image, tag, file string) (reader io.Reader, err error) {
+ fmt.Println("start to download", image)
+
if d.registry == "" {
d.registry = getRegistry(image)
}
diff --git a/pkg/server/store_ext_manager.go b/pkg/server/store_ext_manager.go
index c6148de2..516aba09 100644
--- a/pkg/server/store_ext_manager.go
+++ b/pkg/server/store_ext_manager.go
@@ -176,6 +176,8 @@ var ErrDownloadNotSupport = errors.New("no support")
type nonDownloader struct{}
+var _ downloader.PlatformAwareOCIDownloader = &nonDownloader{}
+
func (n *nonDownloader) WithBasicAuth(username string, password string) {
// Do nothing because this is an empty implementation
}
@@ -197,6 +199,10 @@ func (n *nonDownloader) WithRegistry(string) {
// Do nothing because this is an empty implementation
}
+func (n *nonDownloader) WithKind(string) {
+ // Do nothing because this is an empty implementation
+}
+
func (n *nonDownloader) WithImagePrefix(imagePrefix string) {
// Do nothing because this is an empty implementation
}
diff --git a/pkg/util/home/common.go b/pkg/util/home/common.go
index 15962f3d..ab6608d1 100644
--- a/pkg/util/home/common.go
+++ b/pkg/util/home/common.go
@@ -1,5 +1,5 @@
/*
-Copyright 2024 API Testing Authors.
+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.
@@ -33,6 +33,10 @@ func GetUserBinDir() string {
return filepath.Join(GetUserConfigDir(), "bin")
}
+func GetUserDataDir() string {
+ return filepath.Join(GetUserConfigDir(), "data")
+}
+
func GetExtensionSocketPath(name string) string {
return filepath.Join(GetUserConfigDir(), fmt.Sprintf("%s.sock", name))
}
diff --git a/pkg/util/home/common_test.go b/pkg/util/home/common_test.go
new file mode 100644
index 00000000..d0022940
--- /dev/null
+++ b/pkg/util/home/common_test.go
@@ -0,0 +1,29 @@
+/*
+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 home
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestGetUserBinDir(t *testing.T) {
+ assert.Contains(t, GetUserConfigDir(), "atest")
+ assert.Contains(t, GetUserBinDir(), "bin")
+ assert.Contains(t, GetUserDataDir(), "data")
+ assert.Contains(t, GetExtensionSocketPath("fake"), "fake.sock")
+}