Skip to content

Commit

Permalink
feat: support to verify fields
Browse files Browse the repository at this point in the history
  • Loading branch information
LinuxSuRen committed Apr 15, 2023
1 parent 702ece2 commit 622abe1
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 58 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,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
Expand Down
2 changes: 1 addition & 1 deletion cmd/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/sample_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 4 additions & 4 deletions cmd/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
75 changes: 36 additions & 39 deletions pkg/runner/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import (
"os"
"strings"

"github.com/antonmedv/expr"
unstructured "github.com/linuxsuren/unstructured/pkg"
)

// Reader represents a reader interface
type Reader interface {
GetPod(namespace, name string) map[string]interface{}
GetDeploy(namespace, name string) map[string]interface{}
GetResource(group, kind, version, namespace, name string) map[string]interface{}
GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error)
}

type defualtReader struct {
Expand All @@ -33,19 +31,7 @@ func NewDefaultReader(server, token string) Reader {
}
}

// GetPod gets a pod by namespace and name
func (r *defualtReader) GetPod(namespace, name string) (result map[string]interface{}) {
api := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s", r.server, namespace, name)
return r.request(api)
}

// GetDeploy gets a pod by namespace and name
func (r *defualtReader) GetDeploy(namespace, name string) (result map[string]interface{}) {
api := fmt.Sprintf("%s/api/v1/namespaces/%s/deployments/%s", r.server, namespace, name)
return r.request(api)
}

func (r *defualtReader) GetResource(group, kind, version, namespace, name string) (result map[string]interface{}) {
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") {
Expand All @@ -54,19 +40,19 @@ func (r *defualtReader) GetResource(group, kind, version, namespace, name string
return r.request(api)
}

func (r *defualtReader) request(api string) (result map[string]interface{}) {
func (r *defualtReader) request(api string) (result map[string]interface{}, err error) {
client := GetClient()

req, err := http.NewRequest(http.MethodGet, api, nil)
if err != nil {
return
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
if resp, err := client.Do(req); err == nil && resp.StatusCode == http.StatusOK {
if data, err := io.ReadAll(resp.Body); err == nil {
result = make(map[string]interface{})

_ = json.Unmarshal(data, &result)
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
Expand All @@ -88,16 +74,34 @@ func GetClient() *http.Client {

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...)...)
}
Expand Down Expand Up @@ -143,17 +147,10 @@ func resourceValidator(params ...interface{}) (validator interface{}, err error)
return
}
reader := NewDefaultReader(server, token)
data, err := reader.GetResource(group, kind, version, params[1].(string), params[2].(string))
validator = &defaultResourceValidator{
data: reader.GetResource(group, kind, version, params[1].(string), params[2].(string)),
data: data,
err: err,
}
return
}

// PodValidatorFunc returns a expr for checking pod existing
func PodValidatorFunc() expr.Option {
return expr.Function("pod", podValidator, new(func(...string) ResourceValidator))
}

func KubernetesValidatorFunc() expr.Option {
return expr.Function("k8s", resourceValidator, new(func(interface{}, ...string) ResourceValidator))
}
29 changes: 27 additions & 2 deletions pkg/runner/kubernetes/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ import (
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",
name: "normal",
kind: "pods",
version: "v1",
namespacedName: namespacedName{
namespace: "ns",
name: "fake",
Expand All @@ -31,14 +36,34 @@ func TestGetPod(t *testing.T) {
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 := reader.GetPod(tt.namespacedName.namespace, tt.namespacedName.name)
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)
})
}
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/runner/kubernetes/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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))
}

func KubernetesValidatorFunc() expr.Option {
return expr.Function("k8s", resourceValidator, new(func(interface{}, ...string) ResourceValidator))
}
6 changes: 6 additions & 0 deletions pkg/runner/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}

var requestBody io.Reader
if testcase.Request.Body != "" {
requestBody = bytes.NewBufferString(testcase.Request.Body)
Expand Down Expand Up @@ -247,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 {
Expand Down
2 changes: 2 additions & 0 deletions sample/kubernetes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ items:
- 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()

0 comments on commit 622abe1

Please sign in to comment.