Skip to content

Commit

Permalink
pkg/karmadactl: unit test describe
Browse files Browse the repository at this point in the history
In this commit, we unit test describe command on karmadactl
by creating a fake rest client builder and make sure the described
k8s object returned successfully when the command submitted.

Signed-off-by: Mohamed Awnallah <[email protected]>
  • Loading branch information
mohamedawnallah committed Nov 20, 2024
1 parent ff92a84 commit 0d28b73
Show file tree
Hide file tree
Showing 2 changed files with 301 additions and 1 deletion.
3 changes: 2 additions & 1 deletion pkg/karmadactl/describe/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package describe

import (
"errors"
"fmt"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -146,7 +147,7 @@ func (o *CommandDescribeOptions) Validate() error {
return err
}
if o.OperationScope == options.Members && len(o.Cluster) == 0 {
return fmt.Errorf("must specify a member cluster")
return errors.New("must specify a member cluster")
}
return nil
}
Expand Down
299 changes: 299 additions & 0 deletions pkg/karmadactl/describe/describe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/*
Copyright 2024 The Karmada 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 describe

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
jsonk8serializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource"
fakerest "k8s.io/client-go/rest/fake"
kubectldescribe "k8s.io/kubectl/pkg/cmd/describe"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
describecmd "k8s.io/kubectl/pkg/describe"

"github.com/karmada-io/karmada/pkg/karmadactl/options"
)

var deployment = &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-deployment",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "nginx"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: "nginx",
ImagePullPolicy: "Always",
Name: "nginx",
},
},
},
},
},
}

// var deploymentResponse = map[string]interface{}{
// "apiVersion": "apps/v1",
// "kind": "Deployment",
// "metadata": map[string]interface{}{
// "annotations": map[string]interface{}{
// "propagationpolicy.karmada.io/name": "nginx-propagation",
// "propagationpolicy.karmada.io/namespace": "default",
// },
// "labels": map[string]interface{}{
// "app": "nginx",
// "propagationpolicy.karmada.io/permanent-id": "a06b8325-778f-4724-996b-9994a21394c5",
// },
// "name": "nginx",
// "namespace": "default",
// },
// "spec": map[string]interface{}{
// "selector": map[string]interface{}{
// "matchLabels": map[string]string{
// "app": "nginx",
// },
// },
// "template": map[string]interface{}{
// "metadata": map[string]interface{}{
// "creationTimestamp": nil,
// "labels": map[string]string{
// "app": "nginx",
// },
// },
// "spec": map[string]interface{}{
// "containers": []map[string]interface{}{
// {
// "image": "nginx",
// "imagePullPolicy": "Always",
// "name": "nginx",
// "resources": map[string]interface{}{},
// "terminationMessagePath": "/dev/termination-log",
// "terminationMessagePolicy": "File",
// },
// },
// "dnsPolicy": "ClusterFirst",
// "restartPolicy": "Always",
// "schedulerName": "default-scheduler",
// "securityContext": map[string]interface{}{},
// "terminationGracePeriodSeconds": 30,
// },
// },
// },
// "status": map[string]interface{}{
// "availableReplicas": 2,
// "observedGeneration": 2,
// "readyReplicas": 2,
// "replicas": 2,
// "updatedReplicas": 2,
// },
// }

type ResourceDescribe struct{}

func (rd ResourceDescribe) Describe(string, string, describecmd.DescriberSettings) (output string, err error) {
// Serialize the deployment response to JSON.
bodyBytes, err := json.Marshal(deployment)
if err != nil {
return "", fmt.Errorf("failed to serialize deployment response: %v", err)
}

// Return the serialized deployment as a string (pretty-printed JSON).
return string(bodyBytes), nil
}

func TestValidate(t *testing.T) {
tests := []struct {
name string
describeOpts *CommandDescribeOptions
wantErr bool
errMsg string
}{
{
name: "Validate_WithMemberOperationScopeAndWithoutCluster_MemberClusterOptMustBeSpecified",
describeOpts: &CommandDescribeOptions{OperationScope: options.Members},
wantErr: true,
errMsg: "must specify a member cluster",
},
{
name: "Validate__WithMemberOperationScopeAndWithCluster_Validated",
describeOpts: &CommandDescribeOptions{
OperationScope: options.Members,
Cluster: "test-cluster",
},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.describeOpts.Validate()
if err == nil && test.wantErr {
t.Fatal("expected an error, but got none")
}
if err != nil && !test.wantErr {
t.Errorf("unexpected error, got: %v", err)
}
if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) {
t.Errorf("expected error message %s to be in %s", test.errMsg, err.Error())
}
})
}
}

func TestRunDescribe(t *testing.T) {
tests := []struct {
name string
describeOpts *CommandDescribeOptions
testFactory *cmdtesting.TestFactory
output *bytes.Buffer
prep func(*CommandDescribeOptions, *cmdtesting.TestFactory, *bytes.Buffer) error
cleanup func(*cmdtesting.TestFactory) error
verify func(*bytes.Buffer) error
wantErr bool
}{
{
name: "Run_DescribeDeploymentInfo_DeploymentInfoDescribed",
describeOpts: &CommandDescribeOptions{
KubectlDescribeOptions: &kubectldescribe.DescribeOptions{
FilenameOptions: &resource.FilenameOptions{},
DescriberSettings: &describecmd.DescriberSettings{},
BuilderArgs: []string{"deployment.apps"},
CmdParent: "test-deployment",
Describer: func(*meta.RESTMapping) (describecmd.ResourceDescriber, error) {
return ResourceDescribe{}, nil
},
IOStreams: genericiooptions.IOStreams{},
},
},
testFactory: cmdtesting.NewTestFactory(),
output: &bytes.Buffer{},
prep: func(describeOpts *CommandDescribeOptions, testFactory *cmdtesting.TestFactory, output *bytes.Buffer) error {
client, err := generateFakeRestClient()
if err != nil {
return fmt.Errorf("failed to generate the fake rest client, got: %v", err)
}
testFactory.Client = client
describeOpts.KubectlDescribeOptions.IOStreams.Out = output
describeOpts.KubectlDescribeOptions.NewBuilder = func() *resource.Builder {
return testFactory.NewBuilder()
}
return nil
},
verify: func(output *bytes.Buffer) error {
var deploymentUnmarshalled appsv1.Deployment
err := json.Unmarshal(output.Bytes(), &deploymentUnmarshalled)
if err != nil {
return fmt.Errorf("error unmarshalling JSON: %v", err)
}
if deploymentUnmarshalled.Name != deployment.Name {
return fmt.Errorf("expected deployment name to be %s, but got %s", deployment.Name, deploymentUnmarshalled.Name)
}
if deploymentUnmarshalled.Namespace != deployment.Namespace {
return fmt.Errorf("expected deployment namespace to be %s, but got %s", deployment.Namespace, deploymentUnmarshalled.Namespace)
}
return nil
},
cleanup: func(testFactory *cmdtesting.TestFactory) error {
testFactory.Cleanup()
return nil
},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := test.prep(test.describeOpts, test.testFactory, test.output); err != nil {
t.Fatalf("failed to prep test environment, got error: %v", err)
}
defer func() {
if err := test.cleanup(test.testFactory); err != nil {
t.Errorf("failed to cleanup test environment, got error: %v", err)
}
}()
err := test.describeOpts.Run()
if err == nil && test.wantErr {
t.Fatal("expected an error, but got none")
}
if err != nil && !test.wantErr {
t.Errorf("unexpected error, got: %v", err)
}
if err := test.verify(test.output); err != nil {
t.Errorf("failed to verify describing deployment, got error: %v", err)
}
})
}
}

// generateFakeRestClient creates and returns a fake REST client for testing purposes.
func generateFakeRestClient() (cmdtesting.RESTClient, error) {
// Define the runtime scheme.
scheme := runtime.NewScheme()
if err := appsv1.AddToScheme(scheme); err != nil {
return nil, fmt.Errorf("failed to add scheme, got error: %v", err)
}

// Set up a JSON serializer.
options := jsonk8serializer.SerializerOptions{
Yaml: false,
Pretty: false,
Strict: true,
}
// Serialize the deployment response to JSON.
bodyBytes, err := json.Marshal(deployment)
if err != nil {
return nil, fmt.Errorf("failed to marshal deployment, got error: %v", err)
}

client := &fakerest.RESTClient{
NegotiatedSerializer: runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{
MediaType: "application/json",
MediaTypeType: "application",
MediaTypeSubType: "json",
Serializer: jsonk8serializer.NewSerializerWithOptions(jsonk8serializer.DefaultMetaFactory, scheme, scheme, options),
}),
Client: fakerest.CreateHTTPClient(func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(bodyBytes)),
Header: http.Header{"Content-Type": []string{"application/json"}},
}, nil
}),
}
return client, nil
}

0 comments on commit 0d28b73

Please sign in to comment.