support to load swaggers from an extension

This commit is contained in:
rick 2025-03-05 10:32:24 +00:00
parent f6fa6eb9dd
commit 91ae93a9c9
11 changed files with 293 additions and 66 deletions

View File

@ -22,6 +22,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/linuxsuren/api-testing/pkg/apispec"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -302,6 +303,15 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
_ = o.httpServer.Shutdown(ctx) _ = 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), mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc),
runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{ runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{ 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.MethodGet, "/get", o.getAtestBinary)
mux.HandlePath(http.MethodPost, "/runner/{suite}/{case}", service.WebRunnerHandler) mux.HandlePath(http.MethodPost, "/runner/{suite}/{case}", service.WebRunnerHandler)
mux.HandlePath(http.MethodGet, "/api/v1/sbom", service.SBomHandler) mux.HandlePath(http.MethodGet, "/api/v1/sbom", service.SBomHandler)
mux.HandlePath(http.MethodGet, "/api/v1/swaggers", apispec.SwaggersHandler)
postRequestProxyFunc := postRequestProxy(o.skyWalking) postRequestProxyFunc := postRequestProxy(o.skyWalking)
mux.HandlePath(http.MethodPost, "/browser/{app}", postRequestProxyFunc) mux.HandlePath(http.MethodPost, "/browser/{app}", postRequestProxyFunc)

View File

@ -4,7 +4,7 @@ import { reactive, ref, watch } from 'vue'
import { Edit, CopyDocument, Delete } from '@element-plus/icons-vue' import { Edit, CopyDocument, Delete } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { Suite, TestCase, Pair } from './types' 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 EditButton from '../components/EditButton.vue'
import { Cache } from './cache' import { Cache } from './cache'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -20,6 +20,7 @@ const props = defineProps({
}) })
const emit = defineEmits(['updated']) const emit = defineEmits(['updated'])
let querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name, props.name!) let querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name, props.name!)
const querySwaggers = SwaggerSuggestion()
const suite = ref({ const suite = ref({
name: '', name: '',
@ -325,7 +326,10 @@ const renameTestSuite = (name: string) => {
</el-select> </el-select>
</td> </td>
<td> <td>
<el-input class="mx-1" v-model="suite.spec.url" placeholder="API Spec URL"></el-input> <el-autocomplete
v-model="suite.spec.url"
:fetch-suggestions="querySwaggers"
/>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -620,6 +620,12 @@ function GetSuggestedAPIs(name: string,
.then(callback) .then(callback)
} }
function GetSwaggers(callback: (d: any) => void) {
fetch(`/api/v1/swaggers`, {})
.then(DefaultResponseProcess)
.then(callback)
}
function ReloadMockServer(config: any) { function ReloadMockServer(config: any) {
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
@ -812,7 +818,7 @@ export const API = {
CreateOrUpdateStore, GetStores, DeleteStore, VerifyStore, CreateOrUpdateStore, GetStores, DeleteStore, VerifyStore,
FunctionsQuery, FunctionsQuery,
GetSecrets, DeleteSecret, CreateOrUpdateSecret, GetSecrets, DeleteSecret, CreateOrUpdateSecret,
GetSuggestedAPIs, GetSuggestedAPIs, GetSwaggers,
ReloadMockServer, GetMockConfig, SBOM, DataQuery, ReloadMockServer, GetMockConfig, SBOM, DataQuery,
getToken getToken
} }

View File

@ -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) { export function CreateFilter(queryString: string) {
return (v: Pair) => { return (v: Pair) => {
return v.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1 return v.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1

View File

@ -0,0 +1,127 @@
/*
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"
)
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)
}
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
}

View File

@ -18,8 +18,11 @@ package apispec
import ( import (
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
"github.com/linuxsuren/api-testing/pkg/util/home"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
) )
@ -123,6 +126,12 @@ func ParseToSwagger(data []byte) (swagger *spec.Swagger, err error) {
} }
func ParseURLToSwagger(swaggerURL string) (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 var resp *http.Response
if resp, err = http.Get(swaggerURL); err == nil && resp != nil && resp.StatusCode == http.StatusOK { if resp, err = http.Get(swaggerURL); err == nil && resp != nil && resp.StatusCode == http.StatusOK {
swagger, err = ParseStreamToSwagger(resp.Body) swagger, err = ParseStreamToSwagger(resp.Body)
@ -130,6 +139,14 @@ func ParseURLToSwagger(swaggerURL string) (swagger *spec.Swagger, err error) {
return 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) { func ParseStreamToSwagger(stream io.Reader) (swagger *spec.Swagger, err error) {
var data []byte var data []byte
if data, err = io.ReadAll(stream); err == nil { if data, err = io.ReadAll(stream); err == nil {

View File

@ -46,10 +46,15 @@ func NewStoreDownloader() PlatformAwareOCIDownloader {
func (d *extensionDownloader) Download(name, tag, _ string) (reader io.Reader, err error) { func (d *extensionDownloader) Download(name, tag, _ string) (reader io.Reader, err error) {
name = strings.TrimPrefix(name, fmt.Sprintf("atest-%s-", d.kind)) 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) 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" { if d.os == "windows" {
d.extFile = fmt.Sprintf("%s.exe", d.extFile) d.extFile = fmt.Sprintf("%s.exe", d.extFile)
} }
}
image := fmt.Sprintf("%s/atest-ext-%s-%s", d.imagePrefix, d.kind, name) image := fmt.Sprintf("%s/atest-ext-%s-%s", d.imagePrefix, d.kind, name)
reader, err = d.OCIDownloader.Download(image, tag, d.extFile) reader, err = d.OCIDownloader.Download(image, tag, d.extFile)
return return

View File

@ -72,6 +72,8 @@ func (d *defaultOCIDownloader) WithBasicAuth(username string, password string) {
} }
func (d *defaultOCIDownloader) Download(image, tag, file string) (reader io.Reader, err error) { func (d *defaultOCIDownloader) Download(image, tag, file string) (reader io.Reader, err error) {
fmt.Println("start to download", image)
if d.registry == "" { if d.registry == "" {
d.registry = getRegistry(image) d.registry = getRegistry(image)
} }

View File

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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") return filepath.Join(GetUserConfigDir(), "bin")
} }
func GetUserDataDir() string {
return filepath.Join(GetUserConfigDir(), "data")
}
func GetExtensionSocketPath(name string) string { func GetExtensionSocketPath(name string) string {
return filepath.Join(GetUserConfigDir(), fmt.Sprintf("%s.sock", name)) return filepath.Join(GetUserConfigDir(), fmt.Sprintf("%s.sock", name))
} }

View File

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