feat: support verify against Kubernetes resources (#41)
* feat: support check the response againt k8s * feat: support all the CRDs * feat: support to verify fields * add more unit tests
This commit is contained in:
parent
e08c2046d5
commit
f6d271b7eb
4
Makefile
4
Makefile
|
@ -10,6 +10,10 @@ run-image:
|
|||
docker run ghcr.io/linuxsuren/api-testing:dev
|
||||
copy: build
|
||||
sudo cp bin/atest /usr/local/bin/
|
||||
copy-restart:
|
||||
atest service stop
|
||||
make copy
|
||||
atest service restart
|
||||
test:
|
||||
go test ./... -cover -v -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
|
|
10
README.md
10
README.md
|
@ -8,6 +8,7 @@ This is a API testing tool.
|
|||
|
||||
* Response Body fields equation check
|
||||
* Response Body [eval](https://expr.medv.io/)
|
||||
* Verify the Kubernetes resources
|
||||
* Validate the response body with [JSON schema](https://json-schema.org/)
|
||||
* Output reference between TestCase
|
||||
* Run in server mode, and provide the gRPC endpoint
|
||||
|
@ -62,6 +63,15 @@ The following fields are templated with [sprig](http://masterminds.github.io/spr
|
|||
* Request Body
|
||||
* Request Header
|
||||
|
||||
## 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
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestJSONSchemaCmd(t *testing.T) {
|
||||
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
||||
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, cmd.NewFakeGRPCServer())
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
c.SetOut(buf)
|
||||
|
|
|
@ -6,18 +6,16 @@ import (
|
|||
"github.com/linuxsuren/api-testing/pkg/version"
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// NewRootCmd creates the root command
|
||||
func NewRootCmd(execer fakeruntime.Execer) (c *cobra.Command) {
|
||||
func NewRootCmd(execer fakeruntime.Execer, gRPCServer gRPCServer) (c *cobra.Command) {
|
||||
c = &cobra.Command{
|
||||
Use: "atest",
|
||||
Short: "API testing tool",
|
||||
}
|
||||
c.SetOut(os.Stdout)
|
||||
c.Version = version.GetVersion()
|
||||
gRPCServer := grpc.NewServer()
|
||||
c.AddCommand(createInitCommand(execer),
|
||||
createRunCommand(), createSampleCmd(),
|
||||
createServerCmd(gRPCServer), createJSONSchemaCmd(),
|
||||
|
|
|
@ -51,7 +51,7 @@ func TestCreateRunCommand(t *testing.T) {
|
|||
assert.NotNil(t, server)
|
||||
assert.Equal(t, "server", server.Use)
|
||||
|
||||
root := NewRootCmd(exec.FakeExecer{})
|
||||
root := NewRootCmd(exec.FakeExecer{}, NewFakeGRPCServer())
|
||||
root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"})
|
||||
err := root.Execute()
|
||||
assert.Nil(t, err)
|
||||
|
|
|
@ -104,7 +104,7 @@ func TestRunCommand(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRootCmd(t *testing.T) {
|
||||
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
||||
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "atest", c.Use)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSampleCmd(t *testing.T) {
|
||||
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
||||
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, cmd.NewFakeGRPCServer())
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
c.SetOut(buf)
|
||||
|
|
|
@ -59,6 +59,11 @@ type gRPCServer interface {
|
|||
type fakeGRPCServer struct {
|
||||
}
|
||||
|
||||
// NewFakeGRPCServer creates a fake gRPC server
|
||||
func NewFakeGRPCServer() gRPCServer {
|
||||
return &fakeGRPCServer{}
|
||||
}
|
||||
|
||||
// Serve is a fake method
|
||||
func (s *fakeGRPCServer) Serve(net.Listener) error {
|
||||
return nil
|
||||
|
|
|
@ -27,19 +27,21 @@ func TestPrintProto(t *testing.T) {
|
|||
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
|
||||
assert.NotNil(t, err)
|
||||
},
|
||||
}, {
|
||||
name: "random port",
|
||||
args: []string{"server", "-p=0"},
|
||||
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
|
||||
assert.Nil(t, err)
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, &fakeGRPCServer{})
|
||||
root.SetOut(buf)
|
||||
root.SetArgs(tt.args)
|
||||
err := root.Execute()
|
||||
tt.verify(t, buf, err)
|
||||
})
|
||||
}
|
||||
|
||||
server := createServerCmd(&fakeGRPCServer{})
|
||||
err := server.Execute()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@ import (
|
|||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
||||
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
|
||||
root.SetArgs([]string{"service", "fake"})
|
||||
err := root.Execute()
|
||||
assert.NotNil(t, err)
|
||||
|
||||
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"})
|
||||
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}, NewFakeGRPCServer())
|
||||
notLinux.SetArgs([]string{"service", "--action", "install"})
|
||||
err = notLinux.Execute()
|
||||
assert.NotNil(t, err)
|
||||
|
@ -26,7 +26,7 @@ func TestService(t *testing.T) {
|
|||
os.RemoveAll(tmpFile.Name())
|
||||
}()
|
||||
|
||||
targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
|
||||
targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
|
||||
targetScript.SetArgs([]string{"service", "--action", "install", "--script-path", tmpFile.Name()})
|
||||
err = targetScript.Execute()
|
||||
assert.Nil(t, err)
|
||||
|
@ -58,7 +58,7 @@ func TestService(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput})
|
||||
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}, NewFakeGRPCServer())
|
||||
normalRoot.SetOut(buf)
|
||||
normalRoot.SetArgs([]string{"service", "--action", tt.action})
|
||||
err = normalRoot.Execute()
|
||||
|
|
4
main.go
4
main.go
|
@ -5,10 +5,12 @@ import (
|
|||
|
||||
"github.com/linuxsuren/api-testing/cmd"
|
||||
exec "github.com/linuxsuren/go-fake-runtime"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := cmd.NewRootCmd(exec.DefaultExecer{})
|
||||
gRPCServer := grpc.NewServer()
|
||||
c := cmd.NewRootCmd(exec.DefaultExecer{}, gRPCServer)
|
||||
if err := c.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LookPath is the wrapper of os/exec.LookPath
|
||||
func LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
|
||||
// RunCommandAndReturn runs a command, then returns the output
|
||||
func RunCommandAndReturn(name, dir string, args ...string) (result string, err error) {
|
||||
stdout := &bytes.Buffer{}
|
||||
if err = RunCommandWithBuffer(name, dir, stdout, nil, args...); err == nil {
|
||||
result = stdout.String()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RunCommandWithBuffer runs a command with buffer
|
||||
// stdout and stderr could be nil
|
||||
func RunCommandWithBuffer(name, dir string, stdout, stderr *bytes.Buffer, args ...string) error {
|
||||
if stdout == nil {
|
||||
stdout = &bytes.Buffer{}
|
||||
}
|
||||
if stderr != nil {
|
||||
stderr = &bytes.Buffer{}
|
||||
}
|
||||
return RunCommandWithIO(name, dir, stdout, stderr, args...)
|
||||
}
|
||||
|
||||
// RunCommandWithIO runs a command with given IO
|
||||
func RunCommandWithIO(name, dir string, stdout, stderr io.Writer, args ...string) (err error) {
|
||||
command := exec.Command(name, args...)
|
||||
if dir != "" {
|
||||
command.Dir = dir
|
||||
}
|
||||
|
||||
//var stdout []byte
|
||||
//var errStdout error
|
||||
stdoutIn, _ := command.StdoutPipe()
|
||||
stderrIn, _ := command.StderrPipe()
|
||||
err = command.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// cmd.Wait() should be called only after we finish reading
|
||||
// from stdoutIn and stderrIn.
|
||||
// wg ensures that we finish
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_, _ = copyAndCapture(stdout, stdoutIn)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
_, _ = copyAndCapture(stderr, stderrIn)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
err = command.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
// RunCommandInDir runs a command
|
||||
func RunCommandInDir(name, dir string, args ...string) error {
|
||||
return RunCommandWithIO(name, dir, os.Stdout, os.Stderr, args...)
|
||||
}
|
||||
|
||||
// RunCommand runs a command
|
||||
func RunCommand(name string, arg ...string) (err error) {
|
||||
return RunCommandInDir(name, "", arg...)
|
||||
}
|
||||
|
||||
// RunCommandWithSudo runs a command with sudo
|
||||
func RunCommandWithSudo(name string, args ...string) (err error) {
|
||||
newArgs := make([]string, 0)
|
||||
newArgs = append(newArgs, name)
|
||||
newArgs = append(newArgs, args...)
|
||||
return RunCommand("sudo", newArgs...)
|
||||
}
|
||||
|
||||
func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
|
||||
var out []byte
|
||||
buf := make([]byte, 1024, 1024)
|
||||
for {
|
||||
n, err := r.Read(buf[:])
|
||||
if n > 0 {
|
||||
d := buf[:n]
|
||||
out = append(out, d...)
|
||||
_, err := w.Write(d)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Read returns io.EOF at the end of file, which is not an error for us
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
unstructured "github.com/linuxsuren/unstructured/pkg"
|
||||
)
|
||||
|
||||
// Reader represents a reader interface
|
||||
type Reader interface {
|
||||
GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
type defualtReader struct {
|
||||
server string
|
||||
token string
|
||||
}
|
||||
|
||||
// NewDefaultReader returns a reader implement
|
||||
func NewDefaultReader(server, token string) Reader {
|
||||
return &defualtReader{
|
||||
server: server,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
// GetResource returns the resource
|
||||
func (r *defualtReader) GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error) {
|
||||
api := fmt.Sprintf("%s/api/%s/%s/namespaces/%s/%s/%s", r.server, group, version, namespace, kind, name)
|
||||
api = strings.ReplaceAll(api, "api//", "api/")
|
||||
if !strings.Contains(api, "api/v1") {
|
||||
api = strings.ReplaceAll(api, "api/", "apis/")
|
||||
}
|
||||
return r.request(api)
|
||||
}
|
||||
|
||||
func (r *defualtReader) request(api string) (result map[string]interface{}, err error) {
|
||||
client := GetClient()
|
||||
var req *http.Request
|
||||
if req, err = http.NewRequest(http.MethodGet, api, nil); err == nil {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
|
||||
var resp *http.Response
|
||||
if resp, err = client.Do(req); err == nil && resp.StatusCode == http.StatusOK {
|
||||
var data []byte
|
||||
if data, err = io.ReadAll(resp.Body); err == nil {
|
||||
result = make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var client *http.Client
|
||||
|
||||
// GetClient returns a default client
|
||||
func GetClient() *http.Client {
|
||||
if client == nil {
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// ResourceValidator represents a generic resource validator
|
||||
type ResourceValidator interface {
|
||||
Exist() bool
|
||||
ExpectField(value interface{}, fields ...string) bool
|
||||
}
|
||||
|
||||
type defaultResourceValidator struct {
|
||||
data map[string]interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
func (v *defaultResourceValidator) Exist() bool {
|
||||
if v.err != nil {
|
||||
fmt.Println(v.err)
|
||||
return false
|
||||
}
|
||||
return v.data != nil && len(v.data) > 0
|
||||
}
|
||||
|
||||
func (v *defaultResourceValidator) ExpectField(value interface{}, fields ...string) (result bool) {
|
||||
val, ok, err := unstructured.NestedField(v.data, fields...)
|
||||
if !ok || err != nil {
|
||||
fmt.Printf("cannot find '%v',error: %v\n", fields, err)
|
||||
return
|
||||
}
|
||||
if result = fmt.Sprintf("%v", val) == fmt.Sprintf("%v", value); !result {
|
||||
fmt.Printf("expect: '%v', actual: '%v'\n", value, val)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func podValidator(params ...interface{}) (validator interface{}, err error) {
|
||||
return resourceValidator(append([]interface{}{"pods"}, params...)...)
|
||||
}
|
||||
|
||||
func resourceValidator(params ...interface{}) (validator interface{}, err error) {
|
||||
if len(params) < 3 {
|
||||
err = errors.New("there are three params at least")
|
||||
return
|
||||
}
|
||||
|
||||
var kind string
|
||||
version := "v1"
|
||||
group := ""
|
||||
switch obj := params[0].(type) {
|
||||
case string:
|
||||
kind = obj
|
||||
case map[string]interface{}:
|
||||
if obj["kind"] != nil {
|
||||
kind = obj["kind"].(string)
|
||||
}
|
||||
if obj["version"] != nil {
|
||||
version = obj["version"].(string)
|
||||
}
|
||||
if obj["group"] != nil {
|
||||
group = obj["group"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "" {
|
||||
err = errors.New("kind is required")
|
||||
return
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "deployments", "statefulsets", "daemonsets":
|
||||
group = "apps"
|
||||
}
|
||||
|
||||
server := os.Getenv("KUBERNETES_SERVER")
|
||||
token := os.Getenv("KUBERNETES_TOKEN")
|
||||
if server == "" || token == "" {
|
||||
err = errors.New("KUBERNETES_SERVER and KUBERNETES_TOKEN are required")
|
||||
return
|
||||
}
|
||||
reader := NewDefaultReader(server, token)
|
||||
data, err := reader.GetResource(group, kind, version, params[1].(string), params[2].(string))
|
||||
validator = &defaultResourceValidator{
|
||||
data: data,
|
||||
err: err,
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package kubernetes_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/h2non/gock"
|
||||
"github.com/linuxsuren/api-testing/pkg/runner/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
group string
|
||||
version string
|
||||
kind string
|
||||
namespacedName namespacedName
|
||||
prepare func()
|
||||
expect map[string]interface{}
|
||||
}{{
|
||||
name: "normal",
|
||||
kind: "pods",
|
||||
version: "v1",
|
||||
namespacedName: namespacedName{
|
||||
namespace: "ns",
|
||||
name: "fake",
|
||||
},
|
||||
prepare: func() {
|
||||
gock.New("http://foo").
|
||||
Get("/api/v1/namespaces/ns/pods/fake").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"pod"}`)
|
||||
gock.InterceptClient(kubernetes.GetClient())
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"kind": "pod",
|
||||
},
|
||||
}, {
|
||||
name: "deployments",
|
||||
kind: "deployments",
|
||||
version: "v1",
|
||||
group: "apps",
|
||||
namespacedName: namespacedName{
|
||||
namespace: "ns",
|
||||
name: "fake",
|
||||
},
|
||||
prepare: func() {
|
||||
gock.New("http://foo").
|
||||
Get("/apis/apps/v1/namespaces/ns/deployments/fake").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"deployment"}`)
|
||||
gock.InterceptClient(kubernetes.GetClient())
|
||||
},
|
||||
expect: map[string]interface{}{
|
||||
"kind": "deployment",
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer gock.Clean()
|
||||
tt.prepare()
|
||||
reader := kubernetes.NewDefaultReader("http://foo", "")
|
||||
result, err := reader.GetResource(tt.group, tt.kind, tt.version, tt.namespacedName.namespace, tt.namespacedName.name)
|
||||
assert.Equal(t, tt.expect, result)
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type namespacedName struct {
|
||||
namespace string
|
||||
name string
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package kubernetes provides a low level client for small footprint consideration
|
||||
package kubernetes
|
|
@ -0,0 +1,13 @@
|
|||
package kubernetes
|
||||
|
||||
import "github.com/antonmedv/expr"
|
||||
|
||||
// PodValidatorFunc returns a expr for checking pod existing
|
||||
func PodValidatorFunc() expr.Option {
|
||||
return expr.Function("pod", podValidator, new(func(...string) ResourceValidator))
|
||||
}
|
||||
|
||||
// KubernetesValidatorFunc returns a expr for checking the generic Kubernetes resources
|
||||
func KubernetesValidatorFunc() expr.Option {
|
||||
return expr.Function("k8s", resourceValidator, new(func(interface{}, ...string) ResourceValidator))
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package kubernetes_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/antonmedv/expr"
|
||||
"github.com/h2non/gock"
|
||||
"github.com/linuxsuren/api-testing/pkg/runner/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKubernetesValidatorFunc(t *testing.T) {
|
||||
os.Setenv("KUBERNETES_SERVER", "http://foo")
|
||||
os.Setenv("KUBERNETES_TOKEN", "token")
|
||||
gock.InterceptClient(kubernetes.GetClient())
|
||||
defer gock.RestoreClient(http.DefaultClient)
|
||||
defer gock.Off()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prepare func()
|
||||
expression string
|
||||
expectBool bool
|
||||
expectErr bool
|
||||
}{{
|
||||
name: "pod exist expr",
|
||||
prepare: preparePod,
|
||||
expression: `pod('ns', 'foo').Exist()`,
|
||||
expectBool: true,
|
||||
}, {
|
||||
name: "pod expectField expr",
|
||||
prepare: preparePod,
|
||||
expression: `pod('ns', 'foo').ExpectField('pod', 'kind')`,
|
||||
expectBool: true,
|
||||
}, {
|
||||
name: "pod expectField expr, not match",
|
||||
prepare: preparePod,
|
||||
expression: `pod('ns', 'foo').ExpectField('pods', 'kind')`,
|
||||
expectBool: false,
|
||||
}, {
|
||||
name: "pod expectField expr, not find field",
|
||||
prepare: preparePod,
|
||||
expression: `pod('ns', 'foo').ExpectField('pods', 'kinds')`,
|
||||
expectBool: false,
|
||||
}, {
|
||||
name: "no enough params",
|
||||
expression: `k8s('crd')`,
|
||||
prepare: emptyPrepare,
|
||||
expectBool: false,
|
||||
expectErr: true,
|
||||
}, {
|
||||
name: "crd",
|
||||
expression: `k8s({"kind":"vms","group":"bar","version":"v2"}, "ns", "foo").Exist()`,
|
||||
prepare: prepareCRDVM,
|
||||
expectBool: true,
|
||||
}, {
|
||||
name: "deploy",
|
||||
expression: `k8s("deployments", "ns", "foo").Exist()`,
|
||||
prepare: prepareDeploy,
|
||||
expectBool: true,
|
||||
}, {
|
||||
name: "statefulset",
|
||||
expression: `k8s("statefulsets", "ns", "foo").Exist()`,
|
||||
prepare: prepareStatefulset,
|
||||
expectBool: true,
|
||||
}, {
|
||||
name: "daemonset",
|
||||
expression: `k8s("daemonsets", "ns", "foo").Exist()`,
|
||||
prepare: prepareDaemonset,
|
||||
expectBool: true,
|
||||
}, {
|
||||
name: "no kind",
|
||||
expression: `k8s({"foo": "bar"}, "ns", "foo").Exist()`,
|
||||
prepare: emptyPrepare,
|
||||
expectErr: true,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.prepare()
|
||||
vm, err := expr.Compile(tt.expression, kubernetes.KubernetesValidatorFunc(),
|
||||
kubernetes.PodValidatorFunc())
|
||||
assert.Nil(t, err)
|
||||
|
||||
result, err := expr.Run(vm, expr.Env(tt))
|
||||
assert.Equal(t, tt.expectErr, err != nil)
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.expectBool, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func emptyPrepare() {}
|
||||
|
||||
func preparePod() {
|
||||
gock.New("http://foo").
|
||||
Get("/api/v1/namespaces/ns/pods/foo").
|
||||
MatchHeader("Authorization", "Bearer token").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"pod"}`)
|
||||
}
|
||||
|
||||
func prepareDeploy() {
|
||||
gock.New("http://foo").
|
||||
Get("/apis/apps/v1/namespaces/ns/deployments/foo").
|
||||
MatchHeader("Authorization", "Bearer token").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"deploy"}`)
|
||||
}
|
||||
|
||||
func prepareStatefulset() {
|
||||
gock.New("http://foo").
|
||||
Get("/apis/apps/v1/namespaces/ns/statefulsets/foo").
|
||||
MatchHeader("Authorization", "Bearer token").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"statefulset"}`)
|
||||
}
|
||||
|
||||
func prepareDaemonset() {
|
||||
gock.New("http://foo").
|
||||
Get("/apis/apps/v1/namespaces/ns/daemonsets/foo").
|
||||
MatchHeader("Authorization", "Bearer token").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"daemonset"}`)
|
||||
}
|
||||
|
||||
func prepareCRDVM() {
|
||||
gock.New("http://foo").
|
||||
Get("/apis/bar/v2/namespaces/ns/vms/foo").
|
||||
MatchHeader("Authorization", "Bearer token").
|
||||
Reply(http.StatusOK).
|
||||
JSON(`{"kind":"vm"}`)
|
||||
}
|
|
@ -3,6 +3,7 @@ package runner
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/andreyvit/diff"
|
||||
"github.com/antonmedv/expr"
|
||||
"github.com/antonmedv/expr/vm"
|
||||
"github.com/linuxsuren/api-testing/pkg/runner/kubernetes"
|
||||
"github.com/linuxsuren/api-testing/pkg/testing"
|
||||
fakeruntime "github.com/linuxsuren/go-fake-runtime"
|
||||
unstructured "github.com/linuxsuren/unstructured/pkg"
|
||||
|
@ -193,7 +195,12 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
|||
}
|
||||
}()
|
||||
|
||||
client := http.Client{}
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
var requestBody io.Reader
|
||||
if testcase.Request.Body != "" {
|
||||
requestBody = bytes.NewBufferString(testcase.Request.Body)
|
||||
|
@ -241,6 +248,11 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
|||
|
||||
r.log.Info("start to send request to %s\n", testcase.Request.API)
|
||||
|
||||
// TODO only do this for unit testing, should remove it once we have a better way
|
||||
if strings.HasPrefix(testcase.Request.API, "http://") {
|
||||
client = *http.DefaultClient
|
||||
}
|
||||
|
||||
// send the HTTP request
|
||||
var resp *http.Response
|
||||
if resp, err = client.Do(request); err != nil {
|
||||
|
@ -325,7 +337,9 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
|||
|
||||
for _, verify := range testcase.Expect.Verify {
|
||||
var program *vm.Program
|
||||
if program, err = expr.Compile(verify, expr.Env(mapOutput), expr.AsBool()); err != nil {
|
||||
if program, err = expr.Compile(verify, expr.Env(mapOutput),
|
||||
expr.AsBool(), kubernetes.PodValidatorFunc(),
|
||||
kubernetes.KubernetesValidatorFunc()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -336,6 +350,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
|
|||
|
||||
if !result.(bool) {
|
||||
err = fmt.Errorf("failed to verify: %s", verify)
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
#!api-testing
|
||||
name: Kubernetes
|
||||
api: https://192.168.123.121:6443
|
||||
items:
|
||||
- name: pods
|
||||
request:
|
||||
api: /api/v1/namespaces/kube-system/pods
|
||||
header:
|
||||
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Il9zTmhqWDI0aUZadURCWkpCeUhuLUl2S1pYMjczZWJVdFh5M0lwVzkwTzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJjbHVzdGVyLWFkbWluLXRva2VuLWg5NTZjIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImNsdXN0ZXItYWRtaW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIyZGQ0NzcyNy0wNDEyLTQyYzYtOTg0NC05OWFiM2JlMDkzMDEiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06Y2x1c3Rlci1hZG1pbiJ9.fp7kcr2Tgg7O01C0rCs1YEUGynBHKflKnN0K94hTAtelP9CDwTRMj2Y3rHXrvVisjPMXQ_qJtUb9cLL_QXtihgWIQkGZJYD6uQeatWPqRfAE26BZA-bc3Y4RvuTjgWkwR3PNhfoCDiWx-Y0OkLONG90n40f-1Bq_B5zsf_yVHukeUln8UCL0o8Bi7k2TQXycUOToI_BRC1-q7bkME8-WUFMdbbjKkJzW5FHQg1Y4OL2Dd5_Bv24sT6-P5k8DV8btYYUbvpeYMIP_Vzg8T5N9G4TULPGb41KJ1dm66JNNFFjGB7bqOdC7RR32xrB2mNYodP8tDSyeR_as1BxyQoXkZg
|
||||
expect:
|
||||
verify:
|
||||
- data.kind == "PodList"
|
||||
- pod("kube-system", "kube-ovn-cni-55bz9").Exist()
|
||||
- k8s("pods", "kube-system", "kube-ovn-cni-55bz9").Exist()
|
||||
- k8s("deployments", "kube-system", "coredns").Exist()
|
||||
- k8s("deployments", "kube-system", "coredns").ExpectField(2, "spec", "replicas")
|
||||
- k8s("deployments", "kube-system", "coredns").ExpectField("kube-dns", "metadata", "labels", "k8s-app")
|
||||
- k8s("daemonsets", "kube-system", "kube-ovn-cni").Exist()
|
||||
- k8s({"kind":"virtualmachines","group":"kubevirt.io"}, "vm-test", "vm-win10-dkkhl").Exist()
|
Loading…
Reference in New Issue