Skip to content

Commit 622abe1

Browse files
committed
feat: support to verify fields
1 parent 702ece2 commit 622abe1

15 files changed

+116
-58
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ The following fields are templated with [sprig](http://masterminds.github.io/spr
6363
* Request Body
6464
* Request Header
6565
66+
## Verify against Kubernetes
67+
68+
It could verify any kinds of Kubernetes resources. Please set the environment variables before using it:
69+
70+
* `KUBERNETES_SERVER`
71+
* `KUBERNETES_TOKEN`
72+
73+
See also the [example](sample/kubernetes.yaml).
74+
6675
## TODO
6776
6877
* Reduce the size of context

cmd/jsonschema_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
func TestJSONSchemaCmd(t *testing.T) {
14-
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
14+
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, cmd.NewFakeGRPCServer())
1515

1616
buf := new(bytes.Buffer)
1717
c.SetOut(buf)

cmd/root.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@ import (
66
"github.com/linuxsuren/api-testing/pkg/version"
77
fakeruntime "github.com/linuxsuren/go-fake-runtime"
88
"github.com/spf13/cobra"
9-
"google.golang.org/grpc"
109
)
1110

1211
// NewRootCmd creates the root command
13-
func NewRootCmd(execer fakeruntime.Execer) (c *cobra.Command) {
12+
func NewRootCmd(execer fakeruntime.Execer, gRPCServer gRPCServer) (c *cobra.Command) {
1413
c = &cobra.Command{
1514
Use: "atest",
1615
Short: "API testing tool",
1716
}
1817
c.SetOut(os.Stdout)
1918
c.Version = version.GetVersion()
20-
gRPCServer := grpc.NewServer()
2119
c.AddCommand(createInitCommand(execer),
2220
createRunCommand(), createSampleCmd(),
2321
createServerCmd(gRPCServer), createJSONSchemaCmd(),

cmd/root_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestCreateRunCommand(t *testing.T) {
5151
assert.NotNil(t, server)
5252
assert.Equal(t, "server", server.Use)
5353

54-
root := NewRootCmd(exec.FakeExecer{})
54+
root := NewRootCmd(exec.FakeExecer{}, NewFakeGRPCServer())
5555
root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"})
5656
err := root.Execute()
5757
assert.Nil(t, err)

cmd/run_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestRunCommand(t *testing.T) {
104104
}
105105

106106
func TestRootCmd(t *testing.T) {
107-
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
107+
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
108108
assert.NotNil(t, c)
109109
assert.Equal(t, "atest", c.Use)
110110
}

cmd/sample_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
func TestSampleCmd(t *testing.T) {
14-
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
14+
c := cmd.NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, cmd.NewFakeGRPCServer())
1515

1616
buf := new(bytes.Buffer)
1717
c.SetOut(buf)

cmd/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ type gRPCServer interface {
5959
type fakeGRPCServer struct {
6060
}
6161

62+
// NewFakeGRPCServer creates a fake gRPC server
63+
func NewFakeGRPCServer() gRPCServer {
64+
return &fakeGRPCServer{}
65+
}
66+
6267
// Serve is a fake method
6368
func (s *fakeGRPCServer) Serve(net.Listener) error {
6469
return nil

cmd/server_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,21 @@ func TestPrintProto(t *testing.T) {
2727
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
2828
assert.NotNil(t, err)
2929
},
30+
}, {
31+
name: "random port",
32+
args: []string{"server", "-p=0"},
33+
verify: func(t *testing.T, buf *bytes.Buffer, err error) {
34+
assert.Nil(t, err)
35+
},
3036
}}
3137
for _, tt := range tests {
3238
t.Run(tt.name, func(t *testing.T) {
3339
buf := new(bytes.Buffer)
34-
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
40+
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, &fakeGRPCServer{})
3541
root.SetOut(buf)
3642
root.SetArgs(tt.args)
3743
err := root.Execute()
3844
tt.verify(t, buf, err)
3945
})
4046
}
41-
42-
server := createServerCmd(&fakeGRPCServer{})
43-
err := server.Execute()
44-
assert.Nil(t, err)
4547
}

cmd/service_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import (
1010
)
1111

1212
func TestService(t *testing.T) {
13-
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
13+
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
1414
root.SetArgs([]string{"service", "fake"})
1515
err := root.Execute()
1616
assert.NotNil(t, err)
1717

18-
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"})
18+
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}, NewFakeGRPCServer())
1919
notLinux.SetArgs([]string{"service", "--action", "install"})
2020
err = notLinux.Execute()
2121
assert.NotNil(t, err)
@@ -26,7 +26,7 @@ func TestService(t *testing.T) {
2626
os.RemoveAll(tmpFile.Name())
2727
}()
2828

29-
targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"})
29+
targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
3030
targetScript.SetArgs([]string{"service", "--action", "install", "--script-path", tmpFile.Name()})
3131
err = targetScript.Execute()
3232
assert.Nil(t, err)
@@ -58,7 +58,7 @@ func TestService(t *testing.T) {
5858
for _, tt := range tests {
5959
t.Run(tt.name, func(t *testing.T) {
6060
buf := new(bytes.Buffer)
61-
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput})
61+
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}, NewFakeGRPCServer())
6262
normalRoot.SetOut(buf)
6363
normalRoot.SetArgs([]string{"service", "--action", tt.action})
6464
err = normalRoot.Execute()

main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55

66
"github.com/linuxsuren/api-testing/cmd"
77
exec "github.com/linuxsuren/go-fake-runtime"
8+
"google.golang.org/grpc"
89
)
910

1011
func main() {
11-
c := cmd.NewRootCmd(exec.DefaultExecer{})
12+
gRPCServer := grpc.NewServer()
13+
c := cmd.NewRootCmd(exec.DefaultExecer{}, gRPCServer)
1214
if err := c.Execute(); err != nil {
1315
os.Exit(1)
1416
}

pkg/runner/kubernetes/client.go

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@ import (
1010
"os"
1111
"strings"
1212

13-
"github.com/antonmedv/expr"
13+
unstructured "github.com/linuxsuren/unstructured/pkg"
1414
)
1515

1616
// Reader represents a reader interface
1717
type Reader interface {
18-
GetPod(namespace, name string) map[string]interface{}
19-
GetDeploy(namespace, name string) map[string]interface{}
20-
GetResource(group, kind, version, namespace, name string) map[string]interface{}
18+
GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error)
2119
}
2220

2321
type defualtReader struct {
@@ -33,19 +31,7 @@ func NewDefaultReader(server, token string) Reader {
3331
}
3432
}
3533

36-
// GetPod gets a pod by namespace and name
37-
func (r *defualtReader) GetPod(namespace, name string) (result map[string]interface{}) {
38-
api := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s", r.server, namespace, name)
39-
return r.request(api)
40-
}
41-
42-
// GetDeploy gets a pod by namespace and name
43-
func (r *defualtReader) GetDeploy(namespace, name string) (result map[string]interface{}) {
44-
api := fmt.Sprintf("%s/api/v1/namespaces/%s/deployments/%s", r.server, namespace, name)
45-
return r.request(api)
46-
}
47-
48-
func (r *defualtReader) GetResource(group, kind, version, namespace, name string) (result map[string]interface{}) {
34+
func (r *defualtReader) GetResource(group, kind, version, namespace, name string) (map[string]interface{}, error) {
4935
api := fmt.Sprintf("%s/api/%s/%s/namespaces/%s/%s/%s", r.server, group, version, namespace, kind, name)
5036
api = strings.ReplaceAll(api, "api//", "api/")
5137
if !strings.Contains(api, "api/v1") {
@@ -54,19 +40,19 @@ func (r *defualtReader) GetResource(group, kind, version, namespace, name string
5440
return r.request(api)
5541
}
5642

57-
func (r *defualtReader) request(api string) (result map[string]interface{}) {
43+
func (r *defualtReader) request(api string) (result map[string]interface{}, err error) {
5844
client := GetClient()
59-
60-
req, err := http.NewRequest(http.MethodGet, api, nil)
61-
if err != nil {
62-
return
63-
}
64-
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
65-
if resp, err := client.Do(req); err == nil && resp.StatusCode == http.StatusOK {
66-
if data, err := io.ReadAll(resp.Body); err == nil {
67-
result = make(map[string]interface{})
68-
69-
_ = json.Unmarshal(data, &result)
45+
var req *http.Request
46+
if req, err = http.NewRequest(http.MethodGet, api, nil); err == nil {
47+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
48+
var resp *http.Response
49+
if resp, err = client.Do(req); err == nil && resp.StatusCode == http.StatusOK {
50+
var data []byte
51+
if data, err = io.ReadAll(resp.Body); err == nil {
52+
result = make(map[string]interface{})
53+
54+
err = json.Unmarshal(data, &result)
55+
}
7056
}
7157
}
7258
return
@@ -88,16 +74,34 @@ func GetClient() *http.Client {
8874

8975
type ResourceValidator interface {
9076
Exist() bool
77+
ExpectField(value interface{}, fields ...string) bool
9178
}
9279

9380
type defaultResourceValidator struct {
9481
data map[string]interface{}
82+
err error
9583
}
9684

9785
func (v *defaultResourceValidator) Exist() bool {
86+
if v.err != nil {
87+
fmt.Println(v.err)
88+
return false
89+
}
9890
return v.data != nil && len(v.data) > 0
9991
}
10092

93+
func (v *defaultResourceValidator) ExpectField(value interface{}, fields ...string) (result bool) {
94+
val, ok, err := unstructured.NestedField(v.data, fields...)
95+
if !ok || err != nil {
96+
fmt.Printf("cannot find '%v',error: %v\n", fields, err)
97+
return
98+
}
99+
if result = fmt.Sprintf("%v", val) == fmt.Sprintf("%v", value); !result {
100+
fmt.Printf("expect: '%v', actual: '%v'\n", value, val)
101+
}
102+
return
103+
}
104+
101105
func podValidator(params ...interface{}) (validator interface{}, err error) {
102106
return resourceValidator(append([]interface{}{"pods"}, params...)...)
103107
}
@@ -143,17 +147,10 @@ func resourceValidator(params ...interface{}) (validator interface{}, err error)
143147
return
144148
}
145149
reader := NewDefaultReader(server, token)
150+
data, err := reader.GetResource(group, kind, version, params[1].(string), params[2].(string))
146151
validator = &defaultResourceValidator{
147-
data: reader.GetResource(group, kind, version, params[1].(string), params[2].(string)),
152+
data: data,
153+
err: err,
148154
}
149155
return
150156
}
151-
152-
// PodValidatorFunc returns a expr for checking pod existing
153-
func PodValidatorFunc() expr.Option {
154-
return expr.Function("pod", podValidator, new(func(...string) ResourceValidator))
155-
}
156-
157-
func KubernetesValidatorFunc() expr.Option {
158-
return expr.Function("k8s", resourceValidator, new(func(interface{}, ...string) ResourceValidator))
159-
}

pkg/runner/kubernetes/client_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ import (
1212
func TestGetPod(t *testing.T) {
1313
tests := []struct {
1414
name string
15+
group string
16+
version string
17+
kind string
1518
namespacedName namespacedName
1619
prepare func()
1720
expect map[string]interface{}
1821
}{{
19-
name: "normal",
22+
name: "normal",
23+
kind: "pods",
24+
version: "v1",
2025
namespacedName: namespacedName{
2126
namespace: "ns",
2227
name: "fake",
@@ -31,14 +36,34 @@ func TestGetPod(t *testing.T) {
3136
expect: map[string]interface{}{
3237
"kind": "pod",
3338
},
39+
}, {
40+
name: "deployments",
41+
kind: "deployments",
42+
version: "v1",
43+
group: "apps",
44+
namespacedName: namespacedName{
45+
namespace: "ns",
46+
name: "fake",
47+
},
48+
prepare: func() {
49+
gock.New("http://foo").
50+
Get("/apis/apps/v1/namespaces/ns/deployments/fake").
51+
Reply(http.StatusOK).
52+
JSON(`{"kind":"deployment"}`)
53+
gock.InterceptClient(kubernetes.GetClient())
54+
},
55+
expect: map[string]interface{}{
56+
"kind": "deployment",
57+
},
3458
}}
3559
for _, tt := range tests {
3660
t.Run(tt.name, func(t *testing.T) {
3761
defer gock.Clean()
3862
tt.prepare()
3963
reader := kubernetes.NewDefaultReader("http://foo", "")
40-
result := reader.GetPod(tt.namespacedName.namespace, tt.namespacedName.name)
64+
result, err := reader.GetResource(tt.group, tt.kind, tt.version, tt.namespacedName.namespace, tt.namespacedName.name)
4165
assert.Equal(t, tt.expect, result)
66+
assert.Nil(t, err)
4267
})
4368
}
4469
}

pkg/runner/kubernetes/verify.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package kubernetes
2+
3+
import "github.com/antonmedv/expr"
4+
5+
// PodValidatorFunc returns a expr for checking pod existing
6+
func PodValidatorFunc() expr.Option {
7+
return expr.Function("pod", podValidator, new(func(...string) ResourceValidator))
8+
}
9+
10+
func KubernetesValidatorFunc() expr.Option {
11+
return expr.Function("k8s", resourceValidator, new(func(interface{}, ...string) ResourceValidator))
12+
}

pkg/runner/simple.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
200200
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
201201
},
202202
}
203+
203204
var requestBody io.Reader
204205
if testcase.Request.Body != "" {
205206
requestBody = bytes.NewBufferString(testcase.Request.Body)
@@ -247,6 +248,11 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
247248

248249
r.log.Info("start to send request to %s\n", testcase.Request.API)
249250

251+
// TODO only do this for unit testing, should remove it once we have a better way
252+
if strings.HasPrefix(testcase.Request.API, "http://") {
253+
client = *http.DefaultClient
254+
}
255+
250256
// send the HTTP request
251257
var resp *http.Response
252258
if resp, err = client.Do(request); err != nil {

sample/kubernetes.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ items:
1313
- pod("kube-system", "kube-ovn-cni-55bz9").Exist()
1414
- k8s("pods", "kube-system", "kube-ovn-cni-55bz9").Exist()
1515
- k8s("deployments", "kube-system", "coredns").Exist()
16+
- k8s("deployments", "kube-system", "coredns").ExpectField(2, "spec", "replicas")
17+
- k8s("deployments", "kube-system", "coredns").ExpectField("kube-dns", "metadata", "labels", "k8s-app")
1618
- k8s("daemonsets", "kube-system", "kube-ovn-cni").Exist()
1719
- k8s({"kind":"virtualmachines","group":"kubevirt.io"}, "vm-test", "vm-win10-dkkhl").Exist()

0 commit comments

Comments
 (0)