...

Source file src/k8s.io/kubectl/pkg/cmd/edit/edit_test.go

Documentation: k8s.io/kubectl/pkg/cmd/edit

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package edit
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"io"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/spf13/cobra"
    32  
    33  	yaml "gopkg.in/yaml.v2"
    34  
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/util/sets"
    37  	"k8s.io/cli-runtime/pkg/genericiooptions"
    38  	"k8s.io/cli-runtime/pkg/resource"
    39  	"k8s.io/client-go/rest/fake"
    40  	"k8s.io/kubectl/pkg/cmd/apply"
    41  	"k8s.io/kubectl/pkg/cmd/create"
    42  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    43  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    44  )
    45  
    46  type EditTestCase struct {
    47  	Description string `yaml:"description"`
    48  	// create or edit
    49  	Mode             string   `yaml:"mode"`
    50  	Args             []string `yaml:"args"`
    51  	Filename         string   `yaml:"filename"`
    52  	Output           string   `yaml:"outputFormat"`
    53  	OutputPatch      string   `yaml:"outputPatch"`
    54  	SaveConfig       string   `yaml:"saveConfig"`
    55  	Subresource      string   `yaml:"subresource"`
    56  	Namespace        string   `yaml:"namespace"`
    57  	ExpectedStdout   []string `yaml:"expectedStdout"`
    58  	ExpectedStderr   []string `yaml:"expectedStderr"`
    59  	ExpectedExitCode int      `yaml:"expectedExitCode"`
    60  
    61  	Steps []EditStep `yaml:"steps"`
    62  }
    63  
    64  type EditStep struct {
    65  	// edit or request
    66  	StepType string `yaml:"type"`
    67  
    68  	// only applies to request
    69  	RequestMethod      string `yaml:"expectedMethod,omitempty"`
    70  	RequestPath        string `yaml:"expectedPath,omitempty"`
    71  	RequestContentType string `yaml:"expectedContentType,omitempty"`
    72  	Input              string `yaml:"expectedInput"`
    73  
    74  	// only applies to request
    75  	ResponseStatusCode int `yaml:"resultingStatusCode,omitempty"`
    76  
    77  	Output string `yaml:"resultingOutput"`
    78  }
    79  
    80  func TestEdit(t *testing.T) {
    81  	var (
    82  		name     string
    83  		testcase EditTestCase
    84  		i        int
    85  		err      error
    86  	)
    87  
    88  	const updateEnvVar = "UPDATE_EDIT_FIXTURE_DATA"
    89  	updateInputFixtures := os.Getenv(updateEnvVar) == "true"
    90  
    91  	reqResp := func(req *http.Request) (*http.Response, error) {
    92  		defer func() { i++ }()
    93  		if i > len(testcase.Steps)-1 {
    94  			t.Fatalf("%s, step %d: more requests than steps, got %s %s", name, i, req.Method, req.URL.Path)
    95  		}
    96  		step := testcase.Steps[i]
    97  
    98  		body := []byte{}
    99  		if req.Body != nil {
   100  			body, err = io.ReadAll(req.Body)
   101  			if err != nil {
   102  				t.Fatalf("%s, step %d: %v", name, i, err)
   103  			}
   104  		}
   105  
   106  		inputFile := filepath.Join("testdata", "testcase-"+name, step.Input)
   107  		expectedInput, err := os.ReadFile(inputFile)
   108  		if err != nil {
   109  			t.Fatalf("%s, step %d: %v", name, i, err)
   110  		}
   111  
   112  		outputFile := filepath.Join("testdata", "testcase-"+name, step.Output)
   113  		resultingOutput, err := os.ReadFile(outputFile)
   114  		if err != nil {
   115  			t.Fatalf("%s, step %d: %v", name, i, err)
   116  		}
   117  
   118  		if req.Method == "POST" && req.URL.Path == "/callback" {
   119  			if step.StepType != "edit" {
   120  				t.Fatalf("%s, step %d: expected edit step, got %s %s", name, i, req.Method, req.URL.Path)
   121  			}
   122  			if !bytes.Equal(body, expectedInput) {
   123  				if updateInputFixtures {
   124  					// Convenience to allow recapturing the input and persisting it here
   125  					os.WriteFile(inputFile, body, os.FileMode(0644))
   126  				} else {
   127  					t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, cmp.Diff(string(body), string(expectedInput)))
   128  					t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
   129  				}
   130  			}
   131  			return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(resultingOutput))}, nil
   132  		}
   133  		if step.StepType != "request" {
   134  			t.Fatalf("%s, step %d: expected request step, got %s %s", name, i, req.Method, req.URL.Path)
   135  		}
   136  		body = tryIndent(body)
   137  		expectedInput = tryIndent(expectedInput)
   138  		if req.Method != step.RequestMethod || req.URL.Path != step.RequestPath || req.Header.Get("Content-Type") != step.RequestContentType {
   139  			t.Fatalf(
   140  				"%s, step %d: expected \n%s %s (content-type=%s)\ngot\n%s %s (content-type=%s)", name, i,
   141  				step.RequestMethod, step.RequestPath, step.RequestContentType,
   142  				req.Method, req.URL.Path, req.Header.Get("Content-Type"),
   143  			)
   144  		}
   145  		if !bytes.Equal(body, expectedInput) {
   146  			if updateInputFixtures {
   147  				// Convenience to allow recapturing the input and persisting it here
   148  				os.WriteFile(inputFile, body, os.FileMode(0644))
   149  			} else {
   150  				t.Errorf("%s, step %d: diff in edit content:\n%s", name, i, cmp.Diff(string(body), string(expectedInput)))
   151  				t.Logf("If the change in input is expected, rerun tests with %s=true to update input fixtures", updateEnvVar)
   152  			}
   153  		}
   154  		return &http.Response{StatusCode: step.ResponseStatusCode, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader(resultingOutput))}, nil
   155  
   156  	}
   157  
   158  	handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   159  		resp, _ := reqResp(req)
   160  		for k, vs := range resp.Header {
   161  			w.Header().Del(k)
   162  			for _, v := range vs {
   163  				w.Header().Add(k, v)
   164  			}
   165  		}
   166  		w.WriteHeader(resp.StatusCode)
   167  		io.Copy(w, resp.Body)
   168  	})
   169  
   170  	server := httptest.NewServer(handler)
   171  	defer server.Close()
   172  
   173  	t.Setenv("KUBE_EDITOR", "testdata/test_editor.sh")
   174  	t.Setenv("KUBE_EDITOR_CALLBACK", server.URL+"/callback")
   175  
   176  	testcases := sets.NewString()
   177  	filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error {
   178  		if err != nil {
   179  			return err
   180  		}
   181  		if path == "testdata" {
   182  			return nil
   183  		}
   184  		name := filepath.Base(path)
   185  		if info.IsDir() {
   186  			if strings.HasPrefix(name, "testcase-") {
   187  				testcases.Insert(strings.TrimPrefix(name, "testcase-"))
   188  			}
   189  			return filepath.SkipDir
   190  		}
   191  		return nil
   192  	})
   193  	// sanity check that we found the right folder
   194  	if !testcases.Has("create-list") {
   195  		t.Fatalf("Error locating edit testcases")
   196  	}
   197  
   198  	for _, testcaseName := range testcases.List() {
   199  		t.Run(testcaseName, func(t *testing.T) {
   200  			i = 0
   201  			name = testcaseName
   202  			testcase = EditTestCase{}
   203  			testcaseDir := filepath.Join("testdata", "testcase-"+name)
   204  			testcaseData, err := os.ReadFile(filepath.Join(testcaseDir, "test.yaml"))
   205  			if err != nil {
   206  				t.Fatalf("%s: %v", name, err)
   207  			}
   208  			if err := yaml.Unmarshal(testcaseData, &testcase); err != nil {
   209  				t.Fatalf("%s: %v", name, err)
   210  			}
   211  
   212  			tf := cmdtesting.NewTestFactory()
   213  			defer tf.Cleanup()
   214  
   215  			tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) {
   216  				versionedAPIPath := ""
   217  				if gv.Group == "" {
   218  					versionedAPIPath = "/api/" + gv.Version
   219  				} else {
   220  					versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version
   221  				}
   222  				return &fake.RESTClient{
   223  					VersionedAPIPath:     versionedAPIPath,
   224  					NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
   225  					Client:               fake.CreateHTTPClient(reqResp),
   226  				}, nil
   227  			}
   228  			tf.WithNamespace(testcase.Namespace)
   229  			tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   230  			ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
   231  
   232  			var cmd *cobra.Command
   233  			switch testcase.Mode {
   234  			case "edit":
   235  				cmd = NewCmdEdit(tf, ioStreams)
   236  			case "create":
   237  				cmd = create.NewCmdCreate(tf, ioStreams)
   238  				cmd.Flags().Set("edit", "true")
   239  			case "edit-last-applied":
   240  				cmd = apply.NewCmdApplyEditLastApplied(tf, ioStreams)
   241  			default:
   242  				t.Fatalf("%s: unexpected mode %s", name, testcase.Mode)
   243  			}
   244  			if len(testcase.Filename) > 0 {
   245  				cmd.Flags().Set("filename", filepath.Join(testcaseDir, testcase.Filename))
   246  			}
   247  			if len(testcase.Output) > 0 {
   248  				cmd.Flags().Set("output", testcase.Output)
   249  			}
   250  			if len(testcase.OutputPatch) > 0 {
   251  				cmd.Flags().Set("output-patch", testcase.OutputPatch)
   252  			}
   253  			if len(testcase.SaveConfig) > 0 {
   254  				cmd.Flags().Set("save-config", testcase.SaveConfig)
   255  			}
   256  			if len(testcase.Subresource) > 0 {
   257  				cmd.Flags().Set("subresource", testcase.Subresource)
   258  			}
   259  
   260  			cmdutil.BehaviorOnFatal(func(str string, code int) {
   261  				errBuf.WriteString(str)
   262  				if testcase.ExpectedExitCode != code {
   263  					t.Errorf("%s: expected exit code %d, got %d: %s", name, testcase.ExpectedExitCode, code, str)
   264  				}
   265  			})
   266  
   267  			cmd.Run(cmd, testcase.Args)
   268  
   269  			stdout := buf.String()
   270  			stderr := errBuf.String()
   271  
   272  			for _, s := range testcase.ExpectedStdout {
   273  				if !strings.Contains(stdout, s) {
   274  					t.Errorf("%s: expected to see '%s' in stdout\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
   275  				}
   276  			}
   277  			for _, s := range testcase.ExpectedStderr {
   278  				if !strings.Contains(stderr, s) {
   279  					t.Errorf("%s: expected to see '%s' in stderr\n\nstdout:\n%s\n\nstderr:\n%s", name, s, stdout, stderr)
   280  				}
   281  			}
   282  			if i < len(testcase.Steps) {
   283  				t.Errorf("%s: saw %d steps, testcase included %d additional steps that were not exercised", name, i, len(testcase.Steps)-i)
   284  			}
   285  		})
   286  	}
   287  }
   288  
   289  func tryIndent(data []byte) []byte {
   290  	indented := &bytes.Buffer{}
   291  	if err := json.Indent(indented, data, "", "\t"); err == nil {
   292  		return indented.Bytes()
   293  	}
   294  	return data
   295  }
   296  

View as plain text