-
Notifications
You must be signed in to change notification settings - Fork 197
ctl: add unit tests for all kmeshctl subcommands #1599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * Copyright The Kmesh 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 authz | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestNewCmd(t *testing.T) { | ||
| cmd := NewCmd() | ||
| if cmd.Use != "authz" { | ||
| t.Fatalf("Use = %q, want %q", cmd.Use, "authz") | ||
| } | ||
|
|
||
| got := map[string]bool{} | ||
| for _, sub := range cmd.Commands() { | ||
| got[sub.Name()] = true | ||
| } | ||
| for _, want := range []string{"enable", "disable", "status"} { | ||
| if !got[want] { | ||
| t.Errorf("subcommand %q not registered", want) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestNewEnableCmd(t *testing.T) { | ||
| cmd := NewEnableCmd() | ||
| if cmd.Use != "enable [podNames...]" { | ||
| t.Fatalf("Use = %q, want %q", cmd.Use, "enable [podNames...]") | ||
| } | ||
| } | ||
|
|
||
| func TestNewDisableCmd(t *testing.T) { | ||
| cmd := NewDisableCmd() | ||
| if cmd.Use != "disable [podNames...]" { | ||
| t.Fatalf("Use = %q, want %q", cmd.Use, "disable [podNames...]") | ||
| } | ||
| } | ||
|
|
||
| func TestNewStatusCmd(t *testing.T) { | ||
| cmd := NewStatusCmd() | ||
| if cmd.Use != "status [podNames...]" { | ||
| t.Fatalf("Use = %q, want %q", cmd.Use, "status [podNames...]") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /* | ||
| * Copyright The Kmesh 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 common | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestGetRootCommand(t *testing.T) { | ||
| rootCmd := GetRootCommand() | ||
|
|
||
| if rootCmd.Use != "kmeshctl" { | ||
| t.Fatalf("Use = %q, want %q", rootCmd.Use, "kmeshctl") | ||
| } | ||
| if !rootCmd.SilenceUsage { | ||
| t.Fatal("SilenceUsage should be true") | ||
| } | ||
| if !rootCmd.CompletionOptions.DisableDefaultCmd { | ||
| t.Fatal("default completion command should be disabled") | ||
| } | ||
|
|
||
| want := map[string]bool{ | ||
| "log": false, "dump": false, "waypoint": false, "version": false, | ||
| "monitoring": false, "authz": false, "secret": false, | ||
| } | ||
|
Comment on lines
+36
to
+39
|
||
| for _, cmd := range rootCmd.Commands() { | ||
| if _, ok := want[cmd.Name()]; ok { | ||
| want[cmd.Name()] = true | ||
| } | ||
| } | ||
| for name, found := range want { | ||
| if !found { | ||
| t.Errorf("subcommand %q not registered", name) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| /* | ||
| * Copyright The Kmesh 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 dump | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "io" | ||
| "os" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "google.golang.org/protobuf/encoding/protojson" | ||
|
|
||
| adminv2 "kmesh.net/kmesh/api/v2/admin" | ||
| cluster "kmesh.net/kmesh/api/v2/cluster" | ||
| ) | ||
|
|
||
| func TestUint32ToIPStr(t *testing.T) { | ||
| tests := []struct { | ||
| ip uint32 | ||
| want string | ||
| }{ | ||
| {0x0100007F, "127.0.0.1"}, | ||
| {0x00000000, "0.0.0.0"}, | ||
| {0xFFFFFFFF, "255.255.255.255"}, | ||
| {0x0101A8C0, "192.168.1.1"}, | ||
| {0x0100000A, "10.0.0.1"}, | ||
| } | ||
| for _, tt := range tests { | ||
| if got := uint32ToIPStr(tt.ip); got != tt.want { | ||
| t.Errorf("uint32ToIPStr(%#x) = %v, want %v", tt.ip, got, tt.want) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func captureStdout(t *testing.T, fn func()) string { | ||
| t.Helper() | ||
| old := os.Stdout | ||
| r, w, err := os.Pipe() | ||
| if err != nil { | ||
| t.Fatalf("failed to create pipe: %v", err) | ||
| } | ||
| defer func() { | ||
| os.Stdout = old | ||
| _ = w.Close() | ||
| _ = r.Close() | ||
| }() | ||
|
|
||
| os.Stdout = w | ||
| fn() | ||
| _ = w.Close() | ||
|
|
||
| var buf bytes.Buffer | ||
| if _, err := io.Copy(&buf, r); err != nil { | ||
| t.Fatalf("failed to read captured stdout: %v", err) | ||
| } | ||
| return buf.String() | ||
| } | ||
|
|
||
| func TestPrintDualEngineTable(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input workloadDump | ||
| wantEmpty bool | ||
| wantSubstr []string | ||
| }{ | ||
| { | ||
| name: "full dump", | ||
| input: workloadDump{ | ||
| Workloads: []workloadEntry{ | ||
| {Name: "nginx", Namespace: "default", Addresses: []string{"10.0.0.1"}, Protocol: "TCP", Status: "Healthy"}, | ||
| }, | ||
| Services: []serviceEntry{ | ||
| {Name: "my-svc", Namespace: "default", Hostname: "my-svc.default.svc.cluster.local", Addresses: []string{"10.96.0.1"}}, | ||
| }, | ||
| Policies: []policyEntry{ | ||
| {Name: "allow-all", Namespace: "default", Scope: "namespace", Action: "ALLOW"}, | ||
| }, | ||
| }, | ||
| wantSubstr: []string{"nginx", "my-svc", "allow-all", "ALLOW", "TCP", "Healthy"}, | ||
| }, | ||
| { | ||
| name: "empty dump", | ||
| input: workloadDump{}, | ||
| wantEmpty: true, | ||
| }, | ||
| { | ||
| name: "workloads only with multiple addresses", | ||
| input: workloadDump{ | ||
| Workloads: []workloadEntry{ | ||
| {Name: "app1", Namespace: "test", Addresses: []string{"10.0.0.2", "10.0.0.3"}, Protocol: "HTTP", Status: "Running"}, | ||
| }, | ||
| }, | ||
| wantSubstr: []string{"app1", "10.0.0.2,10.0.0.3"}, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| body, _ := json.Marshal(tt.input) | ||
| out := captureStdout(t, func() { printDualEngineTable(body) }) | ||
|
|
||
| if tt.wantEmpty && strings.TrimSpace(out) != "" { | ||
| t.Errorf("expected empty output, got: %s", out) | ||
| } | ||
| for _, s := range tt.wantSubstr { | ||
| if !strings.Contains(out, s) { | ||
| t.Errorf("output missing %q", s) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestPrintDualEngineTable_InvalidJSON(t *testing.T) { | ||
| out := captureStdout(t, func() { printDualEngineTable([]byte("not json")) }) | ||
| if !strings.Contains(out, "not json") { | ||
| t.Error("expected raw fallback output on invalid JSON") | ||
| } | ||
| } | ||
|
|
||
| func TestNewCmd(t *testing.T) { | ||
| cmd := NewCmd() | ||
| if cmd.Use != "dump" { | ||
| t.Fatalf("Use = %q, want %q", cmd.Use, "dump") | ||
| } | ||
| f := cmd.Flags().Lookup("output") | ||
| if f == nil { | ||
| t.Fatal("--output flag not defined") | ||
| } | ||
| if f.Shorthand != "o" { | ||
| t.Errorf("--output shorthand = %q, want %q", f.Shorthand, "o") | ||
| } | ||
| if f.DefValue != "table" { | ||
| t.Errorf("--output default = %q, want %q", f.DefValue, "table") | ||
| } | ||
| } | ||
|
|
||
| func TestPrintKernelNativeTable(t *testing.T) { | ||
| dump := &adminv2.ConfigDump{ | ||
| StaticResources: &adminv2.ConfigResources{ | ||
| ClusterConfigs: []*cluster.Cluster{ | ||
| {Name: "outbound|80|default|svc.cluster.local"}, | ||
| }, | ||
| }, | ||
| } | ||
| body, err := protojson.Marshal(dump) | ||
| if err != nil { | ||
| t.Fatalf("failed to marshal ConfigDump: %v", err) | ||
| } | ||
|
|
||
| out := captureStdout(t, func() { printKernelNativeTable(body) }) | ||
| if !strings.Contains(out, "NAME") { | ||
| t.Error("expected NAME header in output") | ||
| } | ||
| if !strings.Contains(out, "outbound|80|default|svc.cluster.local") { | ||
| t.Error("expected cluster name in output") | ||
| } | ||
| } | ||
|
|
||
| func TestPrintKernelNativeTable_EmptyDump(t *testing.T) { | ||
| dump := &adminv2.ConfigDump{} | ||
| body, _ := protojson.Marshal(dump) | ||
| out := captureStdout(t, func() { printKernelNativeTable(body) }) | ||
| if strings.TrimSpace(out) != "" { | ||
| t.Errorf("expected empty output for empty dump, got: %s", out) | ||
| } | ||
| } | ||
|
|
||
| func TestPrintKernelNativeTable_InvalidJSON(t *testing.T) { | ||
| out := captureStdout(t, func() { printKernelNativeTable([]byte("{bad")) }) | ||
| if !strings.Contains(out, "{bad") { | ||
| t.Error("expected raw fallback output on invalid JSON") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While these tests are good for verifying the command structure, they don't test the execution logic within the
Runfunctions of the commands (e.g., forenable,disable,status). This leaves the core functionality of theauthzsubcommand untested.Consider refactoring the
authzpackage to separate the command logic from the Kubernetes client and port-forwarding setup. This would allow you to write unit tests for the logic using anhttptestserver, similar to how it's done inctl/log/log_test.go. This would make the tests truly comprehensive.For example, you could add tests that execute the command and verify that the correct HTTP request is made to the mock server.