Skip to content

Commit 884fc5d

Browse files
authored
chore: improve pkg kbagent coverage (#10386)
1 parent 85d441f commit 884fc5d

14 files changed

Lines changed: 1638 additions & 0 deletions

pkg/kbagent/client/client_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
Copyright (C) 2022-2026 ApeCloud Co., Ltd
3+
4+
This file is part of KubeBlocks project
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package client
21+
22+
import (
23+
"context"
24+
"errors"
25+
"io"
26+
"testing"
27+
28+
"github.com/go-logr/logr"
29+
"github.com/golang/mock/gomock"
30+
corev1 "k8s.io/api/core/v1"
31+
"k8s.io/client-go/rest"
32+
33+
"github.com/apecloud/kubeblocks/pkg/kbagent/proto"
34+
)
35+
36+
type stubClient struct{}
37+
38+
func (stubClient) Close() error {
39+
return nil
40+
}
41+
42+
func (stubClient) Action(context.Context, proto.ActionRequest) (proto.ActionResponse, error) {
43+
return proto.ActionResponse{Message: "ok"}, nil
44+
}
45+
46+
func TestMockClientLifecycle(t *testing.T) {
47+
t.Cleanup(UnsetMockClient)
48+
49+
mock := stubClient{}
50+
SetMockClient(mock, nil)
51+
if GetMockClient() != mock {
52+
t.Fatalf("GetMockClient() did not return mock client")
53+
}
54+
got, err := NewClient(func() (string, int32, error) {
55+
t.Fatal("endpoint should not be called when mock client is set")
56+
return "", 0, nil
57+
})
58+
if err != nil || got != mock {
59+
t.Fatalf("NewClient() = %v, %v, want mock nil-error", got, err)
60+
}
61+
62+
mockErr := errors.New("mock")
63+
SetMockClient(nil, mockErr)
64+
got, err = NewClient(func() (string, int32, error) {
65+
t.Fatal("endpoint should not be called when mock error is set")
66+
return "", 0, nil
67+
})
68+
if got != nil || !errors.Is(err, mockErr) {
69+
t.Fatalf("NewClient() = %v, %v, want nil mockErr", got, err)
70+
}
71+
72+
UnsetMockClient()
73+
if GetMockClient() != nil {
74+
t.Fatalf("mock client should be cleared")
75+
}
76+
}
77+
78+
func TestNewClientEndpointBranches(t *testing.T) {
79+
endpointErr := errors.New("endpoint")
80+
if got, err := NewClient(func() (string, int32, error) { return "", 0, endpointErr }); got != nil || !errors.Is(err, endpointErr) {
81+
t.Fatalf("NewClient endpoint error = %v, %v", got, err)
82+
}
83+
84+
if got, err := NewClient(func() (string, int32, error) { return "", 0, nil }); got != nil || err != nil {
85+
t.Fatalf("NewClient empty endpoint = %v, %v, want nil nil", got, err)
86+
}
87+
88+
got, err := NewClient(func() (string, int32, error) { return "127.0.0.1", 3501, nil })
89+
if err != nil {
90+
t.Fatalf("NewClient returned error: %v", err)
91+
}
92+
if _, ok := got.(*httpClient); !ok {
93+
t.Fatalf("NewClient returned %T, want *httpClient", got)
94+
}
95+
if err := got.Close(); err != nil {
96+
t.Fatalf("Close() error = %v", err)
97+
}
98+
}
99+
100+
func TestGeneratedMockClient(t *testing.T) {
101+
ctrl := gomock.NewController(t)
102+
mock := NewMockClient(ctrl)
103+
mock.EXPECT().
104+
Action(gomock.Any(), proto.ActionRequest{Action: "backup"}).
105+
Return(proto.ActionResponse{Message: "ok"}, nil)
106+
107+
resp, err := mock.Action(context.Background(), proto.ActionRequest{Action: "backup"})
108+
if err != nil {
109+
t.Fatalf("mock Action() error = %v", err)
110+
}
111+
if resp.Message != "ok" {
112+
t.Fatalf("mock Action() response = %#v", resp)
113+
}
114+
if err := mock.Close(); err != nil {
115+
t.Fatalf("mock Close() error = %v", err)
116+
}
117+
}
118+
119+
func TestNewPortForwardClientStableBranches(t *testing.T) {
120+
t.Cleanup(UnsetMockClient)
121+
122+
mock := stubClient{}
123+
SetMockClient(mock, nil)
124+
got, err := NewPortForwardClient(&corev1.Pod{}, func() (string, int32, error) {
125+
t.Fatal("endpoint should not be called when mock client is set")
126+
return "", 0, nil
127+
})
128+
if err != nil || got != mock {
129+
t.Fatalf("NewPortForwardClient mock = %v, %v", got, err)
130+
}
131+
132+
UnsetMockClient()
133+
endpointErr := errors.New("endpoint")
134+
got, err = NewPortForwardClient(&corev1.Pod{}, func() (string, int32, error) {
135+
return "", 0, endpointErr
136+
})
137+
if got != nil || !errors.Is(err, endpointErr) {
138+
t.Fatalf("NewPortForwardClient endpoint error = %v, %v", got, err)
139+
}
140+
}
141+
142+
func TestPortForwardClientStableErrors(t *testing.T) {
143+
pf := &portForwardClient{
144+
pod: &corev1.Pod{},
145+
port: "3501",
146+
config: &rest.Config{},
147+
logger: logr.Discard(),
148+
}
149+
if err := pf.Close(); err != nil {
150+
t.Fatalf("Close() error = %v", err)
151+
}
152+
_, _ = pf.createDialer("POST", nil, &rest.Config{})
153+
readyCh := make(chan struct{})
154+
stopCh := make(chan struct{})
155+
forwarder, err := pf.newPortForwarder(readyCh, stopCh, io.Discard)
156+
close(stopCh)
157+
if err != nil {
158+
t.Fatalf("newPortForwarder() error = %v", err)
159+
}
160+
if forwarder == nil {
161+
t.Fatalf("newPortForwarder() returned nil")
162+
}
163+
if resp, err := pf.Action(context.Background(), proto.ActionRequest{Action: "backup"}); err == nil || resp.Message != "" {
164+
t.Fatalf("expected Action error for empty rest config, got %#v, %v", resp, err)
165+
}
166+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
Copyright (C) 2022-2026 ApeCloud Co., Ltd
3+
4+
This file is part of KubeBlocks project
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package client
21+
22+
import (
23+
"context"
24+
"errors"
25+
"fmt"
26+
"io"
27+
"net"
28+
"net/http"
29+
"net/http/httptest"
30+
"net/url"
31+
"strings"
32+
"testing"
33+
34+
"github.com/apecloud/kubeblocks/pkg/constant"
35+
"github.com/apecloud/kubeblocks/pkg/kbagent/proto"
36+
)
37+
38+
type errorReader struct{}
39+
40+
func (errorReader) Read([]byte) (int, error) {
41+
return 0, errors.New("read")
42+
}
43+
44+
func newHTTPClientForTest(t *testing.T, handler http.HandlerFunc) (*httpClient, func()) {
45+
t.Helper()
46+
server := httptest.NewServer(handler)
47+
u, err := url.Parse(server.URL)
48+
if err != nil {
49+
t.Fatalf("parse server URL: %v", err)
50+
}
51+
host, portString, err := net.SplitHostPort(u.Host)
52+
if err != nil {
53+
t.Fatalf("split host port: %v", err)
54+
}
55+
var port int32
56+
if _, err := fmt.Sscan(portString, &port); err != nil {
57+
t.Fatalf("parse port: %v", err)
58+
}
59+
return &httpClient{host: host, port: port, client: server.Client()}, server.Close
60+
}
61+
62+
func TestHTTPClientAction(t *testing.T) {
63+
cli, closeServer := newHTTPClientForTest(t, func(w http.ResponseWriter, r *http.Request) {
64+
if r.URL.Path != proto.ServiceAction.URI || r.Method != http.MethodPost {
65+
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
66+
}
67+
w.WriteHeader(http.StatusOK)
68+
_, _ = w.Write([]byte(`{"message":"done","output":"b2s="}`))
69+
})
70+
defer closeServer()
71+
72+
resp, err := cli.Action(context.Background(), proto.ActionRequest{Action: "backup"})
73+
if err != nil {
74+
t.Fatalf("Action() error = %v", err)
75+
}
76+
if resp.Message != "done" || string(resp.Output) != "ok" {
77+
t.Fatalf("unexpected response: %#v", resp)
78+
}
79+
80+
resp, err = cli.Action(context.WithValue(context.Background(), constant.DryRunContextKey, true), proto.ActionRequest{Action: "backup"})
81+
if err != nil || resp.Message != "" || resp.Error != "" || len(resp.Output) != 0 {
82+
t.Fatalf("dry-run Action() = %#v, %v", resp, err)
83+
}
84+
}
85+
86+
func TestHTTPClientRequestAndDecodeErrors(t *testing.T) {
87+
cli, closeServer := newHTTPClientForTest(t, func(w http.ResponseWriter, r *http.Request) {
88+
switch r.URL.Query().Get("case") {
89+
case "bad-status":
90+
w.WriteHeader(http.StatusAccepted)
91+
case "invalid-json":
92+
w.WriteHeader(http.StatusInternalServerError)
93+
_, _ = w.Write([]byte("{"))
94+
default:
95+
w.WriteHeader(http.StatusOK)
96+
_, _ = w.Write([]byte(`{"error":"failed"}`))
97+
}
98+
})
99+
defer closeServer()
100+
101+
if _, err := cli.request(context.Background(), " bad method", "http://bad-url", nil); err == nil {
102+
t.Fatalf("expected request construction error")
103+
}
104+
105+
body, err := cli.request(context.Background(), http.MethodPost, "http://"+net.JoinHostPort(cli.host, fmt.Sprint(cli.port))+proto.ServiceAction.URI, strings.NewReader("{}"))
106+
if err != nil {
107+
t.Fatalf("request() error = %v", err)
108+
}
109+
_ = body.Close()
110+
111+
_, err = cli.request(context.Background(), http.MethodPost, "http://"+net.JoinHostPort(cli.host, fmt.Sprint(cli.port))+proto.ServiceAction.URI+"?case=bad-status", nil)
112+
if err == nil {
113+
t.Fatalf("expected unexpected status error")
114+
}
115+
116+
body, err = cli.request(context.Background(), http.MethodPost, "http://"+net.JoinHostPort(cli.host, fmt.Sprint(cli.port))+proto.ServiceAction.URI+"?case=invalid-json", nil)
117+
if err != nil {
118+
t.Fatalf("request invalid-json error = %v", err)
119+
}
120+
defer body.Close()
121+
if _, err := decode(body, &proto.ActionResponse{}); err == nil {
122+
t.Fatalf("expected decode error")
123+
}
124+
125+
if _, err := decode(io.NopCloser(errorReader{}), &proto.ActionResponse{}); err == nil {
126+
t.Fatalf("expected read error")
127+
}
128+
}

pkg/kbagent/proto/errors_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright (C) 2022-2026 ApeCloud Co., Ltd
3+
4+
This file is part of KubeBlocks project
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU Affero General Public License for more details.
15+
16+
You should have received a copy of the GNU Affero General Public License
17+
along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package proto
21+
22+
import (
23+
"errors"
24+
"testing"
25+
)
26+
27+
func TestError2Type(t *testing.T) {
28+
tests := []struct {
29+
name string
30+
err error
31+
want string
32+
}{
33+
{name: "nil", want: ""},
34+
{name: "not defined", err: ErrNotDefined, want: "notDefined"},
35+
{name: "not implemented", err: ErrNotImplemented, want: "notImplemented"},
36+
{name: "precondition failed", err: ErrPreconditionFailed, want: "preconditionFailed"},
37+
{name: "bad request", err: ErrBadRequest, want: "badRequest"},
38+
{name: "in progress", err: ErrInProgress, want: "inProgress"},
39+
{name: "busy", err: ErrBusy, want: "busy"},
40+
{name: "timed out", err: ErrTimedOut, want: "timedOut"},
41+
{name: "failed", err: ErrFailed, want: "failed"},
42+
{name: "internal error", err: ErrInternalError, want: "internalError"},
43+
{name: "wrapped", err: errors.Join(ErrBusy), want: "busy"},
44+
{name: "unknown", err: errors.New("other"), want: "unknown"},
45+
}
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
if got := Error2Type(tt.err); got != tt.want {
49+
t.Fatalf("Error2Type() = %q, want %q", got, tt.want)
50+
}
51+
})
52+
}
53+
}
54+
55+
func TestType2Error(t *testing.T) {
56+
tests := []struct {
57+
errType string
58+
want error
59+
}{
60+
{errType: "", want: nil},
61+
{errType: "notDefined", want: ErrNotDefined},
62+
{errType: "notImplemented", want: ErrNotImplemented},
63+
{errType: "preconditionFailed", want: ErrPreconditionFailed},
64+
{errType: "badRequest", want: ErrBadRequest},
65+
{errType: "inProgress", want: ErrInProgress},
66+
{errType: "busy", want: ErrBusy},
67+
{errType: "timedOut", want: ErrTimedOut},
68+
{errType: "failed", want: ErrFailed},
69+
{errType: "internalError", want: ErrInternalError},
70+
{errType: "unknown-type", want: ErrUnknown},
71+
}
72+
for _, tt := range tests {
73+
t.Run(tt.errType, func(t *testing.T) {
74+
if got := Type2Error(tt.errType); !errors.Is(got, tt.want) {
75+
t.Fatalf("Type2Error() = %v, want %v", got, tt.want)
76+
}
77+
})
78+
}
79+
}

0 commit comments

Comments
 (0)