github.com/spinnaker/spin@v1.30.0/cmd/pipeline/save_test.go (about) 1 // Copyright (c) 2018, Google, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package pipeline 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "net/http" 23 "net/http/httptest" 24 "os" 25 "strings" 26 "testing" 27 28 "github.com/spinnaker/spin/cmd" 29 "github.com/spinnaker/spin/util" 30 ) 31 32 func TestPipelineSave_json(t *testing.T) { 33 saveBuffer := new(bytes.Buffer) 34 ts := testGatePipelineSaveSuccess(saveBuffer) 35 defer ts.Close() 36 37 tempFile := tempPipelineFile(testPipelineJsonStr) 38 if tempFile == nil { 39 t.Fatal("Could not create temp pipeline file.") 40 } 41 defer os.Remove(tempFile.Name()) 42 43 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 44 pipelineCmd, _ := NewPipelineCmd(rootOpts) 45 rootCmd.AddCommand(pipelineCmd) 46 47 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 48 rootCmd.SetArgs(args) 49 err := rootCmd.Execute() 50 if err != nil { 51 t.Fatalf("Command failed with: %s", err) 52 } 53 54 expected := strings.TrimSpace(testPipelineJsonStr) 55 recieved := saveBuffer.Bytes() 56 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 57 } 58 59 func TestPipelineSave_yaml(t *testing.T) { 60 saveBuffer := new(bytes.Buffer) 61 ts := testGatePipelineSaveSuccess(saveBuffer) 62 defer ts.Close() 63 64 tempFile := tempPipelineFile(testPipelineYamlStr) 65 if tempFile == nil { 66 t.Fatal("Could not create temp pipeline file.") 67 } 68 defer os.Remove(tempFile.Name()) 69 70 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 71 pipelineCmd, _ := NewPipelineCmd(rootOpts) 72 rootCmd.AddCommand(pipelineCmd) 73 74 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 75 rootCmd.SetArgs(args) 76 err := rootCmd.Execute() 77 if err != nil { 78 t.Fatalf("Command failed with: %s", err) 79 } 80 81 expected := strings.TrimSpace(testPipelineJsonStr) 82 recieved := saveBuffer.Bytes() 83 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 84 } 85 86 func TestPipelineSave_stdin(t *testing.T) { 87 saveBuffer := new(bytes.Buffer) 88 ts := testGatePipelineSaveSuccess(saveBuffer) 89 defer ts.Close() 90 91 tempFile := tempPipelineFile(testPipelineJsonStr) 92 if tempFile == nil { 93 t.Fatal("Could not create temp pipeline file.") 94 } 95 defer os.Remove(tempFile.Name()) 96 97 // Prepare Stdin for test reading. 98 tempFile.Seek(0, 0) 99 oldStdin := os.Stdin 100 defer func() { os.Stdin = oldStdin }() 101 os.Stdin = tempFile 102 103 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 104 pipelineCmd, _ := NewPipelineCmd(rootOpts) 105 rootCmd.AddCommand(pipelineCmd) 106 107 args := []string{"pipeline", "save", "--gate-endpoint", ts.URL} 108 rootCmd.SetArgs(args) 109 err := rootCmd.Execute() 110 if err != nil { 111 t.Fatalf("Command failed with: %s", err) 112 } 113 114 expected := strings.TrimSpace(testPipelineJsonStr) 115 recieved := saveBuffer.Bytes() 116 util.TestPrettyJsonDiff(t, "save request body", expected, recieved) 117 } 118 119 func TestPipelineSave_fail(t *testing.T) { 120 ts := testGateFail() 121 defer ts.Close() 122 123 tempFile := tempPipelineFile(testPipelineJsonStr) 124 if tempFile == nil { 125 t.Fatal("Could not create temp pipeline file.") 126 } 127 defer os.Remove(tempFile.Name()) 128 129 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 130 pipelineCmd, _ := NewPipelineCmd(rootOpts) 131 rootCmd.AddCommand(pipelineCmd) 132 133 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 134 rootCmd.SetArgs(args) 135 err := rootCmd.Execute() 136 if err == nil { 137 t.Fatalf("Command failed with: %s", err) 138 } 139 } 140 141 func TestPipelineSave_accessdenied(t *testing.T) { 142 ts := testGateReadOnly() 143 defer ts.Close() 144 145 tempFile := tempPipelineFile(testPipelineJsonStr) 146 if tempFile == nil { 147 t.Fatal("Could not create temp pipeline file.") 148 } 149 defer os.Remove(tempFile.Name()) 150 151 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 152 pipelineCmd, _ := NewPipelineCmd(rootOpts) 153 rootCmd.AddCommand(pipelineCmd) 154 155 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 156 rootCmd.SetArgs(args) 157 err := rootCmd.Execute() 158 if err == nil { 159 t.Fatalf("Command failed with: %s", err) 160 } 161 } 162 163 func TestPipelineSave_flags(t *testing.T) { 164 ts := testGateSuccess() 165 defer ts.Close() 166 167 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 168 pipelineCmd, _ := NewPipelineCmd(rootOpts) 169 rootCmd.AddCommand(pipelineCmd) 170 171 args := []string{"pipeline", "save", "--gate-endpoint", ts.URL} // Missing pipeline spec file. 172 rootCmd.SetArgs(args) 173 err := rootCmd.Execute() 174 if err == nil { 175 t.Fatalf("Command failed with: %s", err) 176 } 177 } 178 179 func TestPipelineSave_missingname(t *testing.T) { 180 ts := testGateSuccess() 181 defer ts.Close() 182 183 tempFile := tempPipelineFile(missingNameJsonStr) 184 if tempFile == nil { 185 t.Fatal("Could not create temp pipeline file.") 186 } 187 defer os.Remove(tempFile.Name()) 188 189 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 190 pipelineCmd, _ := NewPipelineCmd(rootOpts) 191 rootCmd.AddCommand(pipelineCmd) 192 193 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 194 rootCmd.SetArgs(args) 195 err := rootCmd.Execute() 196 if err == nil { 197 t.Fatalf("Command failed with: %s", err) 198 } 199 } 200 201 func TestPipelineSave_missingid(t *testing.T) { 202 ts := testGateSuccess() 203 defer ts.Close() 204 205 tempFile := tempPipelineFile(missingIdJsonStr) 206 if tempFile == nil { 207 t.Fatal("Could not create temp pipeline file.") 208 } 209 defer os.Remove(tempFile.Name()) 210 211 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 212 pipelineCmd, _ := NewPipelineCmd(rootOpts) 213 rootCmd.AddCommand(pipelineCmd) 214 215 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 216 rootCmd.SetArgs(args) 217 err := rootCmd.Execute() 218 if err != nil { 219 t.Fatalf("Command failed with: %s", err) 220 } 221 } 222 223 func TestPipelineSave_missingapp(t *testing.T) { 224 ts := testGateSuccess() 225 defer ts.Close() 226 227 tempFile := tempPipelineFile(missingAppJsonStr) 228 if tempFile == nil { 229 t.Fatal("Could not create temp pipeline file.") 230 } 231 defer os.Remove(tempFile.Name()) 232 233 rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard) 234 pipelineCmd, _ := NewPipelineCmd(rootOpts) 235 rootCmd.AddCommand(pipelineCmd) 236 237 args := []string{"pipeline", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL} 238 rootCmd.SetArgs(args) 239 err := rootCmd.Execute() 240 if err == nil { 241 t.Fatalf("Command failed with: %s", err) 242 } 243 } 244 245 func tempPipelineFile(pipelineContent string) *os.File { 246 tempFile, _ := ioutil.TempFile("" /* /tmp dir. */, "pipeline-spec") 247 bytes, err := tempFile.Write([]byte(pipelineContent)) 248 if err != nil || bytes == 0 { 249 fmt.Println("Could not write temp file.") 250 return nil 251 } 252 return tempFile 253 } 254 255 // testGatePipelineSaveSuccess spins up a local http server that we will configure the 256 // GateClient to direct requests to. Responds with a 200 OK. 257 // Writes pipeline body to buffer for testing. 258 func testGatePipelineSaveSuccess(buffer io.Writer) *httptest.Server { 259 mux := util.TestGateMuxWithVersionHandler() 260 mux.Handle( 261 "/applications/app/pipelineConfigs/pipeline1", 262 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 263 // Confirm the pipeline exists. 264 fmt.Fprintln(w, "") 265 }), 266 ) 267 mux.Handle( 268 "/pipelines", 269 util.NewTestBufferHandlerFunc(http.MethodPost, buffer, http.StatusOK, ""), 270 ) 271 return httptest.NewServer(mux) 272 } 273 274 // testGateSuccess spins up a local http server that we will configure the GateClient 275 // to direct requests to. Responds with a 200 OK. 276 func testGateSuccess() *httptest.Server { 277 mux := util.TestGateMuxWithVersionHandler() 278 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 279 fmt.Fprintln(w, "") // Just write an empty 200 success on save. 280 })) 281 return httptest.NewServer(mux) 282 } 283 284 // testGateReadOnly spins up a local http server that we will configure the GateClient 285 // to direct requests to. Responds with a 200 OK for READ-type requests (GET), 400 otherwise. 286 func testGateReadOnly() *httptest.Server { 287 mux := util.TestGateMuxWithVersionHandler() 288 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 289 switch method := r.Method; method { 290 case "GET": 291 fmt.Fprintln(w, "") // Just write an empty 200 success on GET. 292 default: 293 // Return "400 Access is denied" (Bad Request) 294 http.Error(w, "Access is denied", http.StatusBadRequest) 295 } 296 })) 297 return httptest.NewServer(mux) 298 } 299 300 // testGateFail spins up a local http server that we will configure the GateClient 301 // to direct requests to. Responds with a 500 InternalServerError. 302 func testGateFail() *httptest.Server { 303 mux := util.TestGateMuxWithVersionHandler() 304 mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 305 // TODO(jacobkiefer): Mock more robust errors once implemented upstream. 306 http.Error(w, "Internal Server Error", http.StatusInternalServerError) 307 })) 308 return httptest.NewServer(mux) 309 } 310 311 const missingNameJsonStr = ` 312 { 313 "id": "pipeline1", 314 "application": "app", 315 "keepWaitingPipelines": false, 316 "lastModifiedBy": "anonymous", 317 "limitConcurrent": true, 318 "stages": [ 319 { 320 "name": "Wait", 321 "refId": "1", 322 "requisiteStageRefIds": [], 323 "type": "wait", 324 "waitTime": 30 325 } 326 ], 327 "triggers": [], 328 "updateTs": "1520879791608" 329 } 330 ` 331 332 const missingIdJsonStr = ` 333 { 334 "name": "pipeline1", 335 "application": "app", 336 "keepWaitingPipelines": false, 337 "lastModifiedBy": "anonymous", 338 "limitConcurrent": true, 339 "stages": [ 340 { 341 "name": "Wait", 342 "refId": "1", 343 "requisiteStageRefIds": [], 344 "type": "wait", 345 "waitTime": 30 346 } 347 ], 348 "triggers": [], 349 "updateTs": "1520879791608" 350 } 351 ` 352 353 const missingAppJsonStr = ` 354 { 355 "name": "pipeline1", 356 "id": "pipeline1", 357 "keepWaitingPipelines": false, 358 "lastModifiedBy": "anonymous", 359 "limitConcurrent": true, 360 "stages": [ 361 { 362 "name": "Wait", 363 "refId": "1", 364 "requisiteStageRefIds": [], 365 "type": "wait", 366 "waitTime": 30 367 } 368 ], 369 "triggers": [], 370 "updateTs": "1520879791608" 371 } 372 ` 373 374 const testPipelineJsonStr = ` 375 { 376 "application": "app", 377 "id": "pipeline1", 378 "keepWaitingPipelines": false, 379 "lastModifiedBy": "anonymous", 380 "limitConcurrent": true, 381 "name": "pipeline1", 382 "parameterConfig": [ 383 { 384 "default": "bar", 385 "description": "A foo.", 386 "name": "foo", 387 "required": true 388 } 389 ], 390 "stages": [ 391 { 392 "comments": "${ parameters.derp }", 393 "name": "Wait", 394 "refId": "1", 395 "requisiteStageRefIds": [], 396 "type": "wait", 397 "waitTime": 30 398 } 399 ], 400 "triggers": [], 401 "updateTs": "1520879791608" 402 } 403 ` 404 405 const testPipelineYamlStr = ` 406 name: pipeline1 407 id: pipeline1 408 application: app 409 keepWaitingPipelines: false 410 lastModifiedBy: anonymous 411 limitConcurrent: true 412 parameterConfig: 413 - default: bar 414 description: A foo. 415 name: foo 416 required: true 417 stages: 418 - comments: ${ parameters.derp } 419 name: Wait 420 refId: "1" 421 requisiteStageRefIds: [] 422 type: wait 423 waitTime: 30 424 triggers: [] 425 updateTs: "1520879791608" 426 `