github.com/spinnaker/spin@v1.30.0/cmd/pipeline-template/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_template
    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 TestPipelineTemplateSave_createjson(t *testing.T) {
    33  	saveBuffer := new(bytes.Buffer)
    34  	ts := testGatePipelineTemplateCreateSuccess(saveBuffer)
    35  	defer ts.Close()
    36  
    37  	tempFile := tempPipelineTemplateFile(testPipelineTemplateJsonStr)
    38  	if tempFile == nil {
    39  		t.Fatal("Could not create temp pipeline template file.")
    40  	}
    41  	defer os.Remove(tempFile.Name())
    42  
    43  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
    44  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
    45  
    46  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
    47  	rootCmd.SetArgs(args)
    48  	err := rootCmd.Execute()
    49  	if err != nil {
    50  		t.Fatalf("Command failed with: %s", err)
    51  	}
    52  
    53  	expected := strings.TrimSpace(testPipelineTemplateJsonStr)
    54  	recieved := saveBuffer.Bytes()
    55  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
    56  }
    57  
    58  func TestPipelineTemplateSave_createyaml(t *testing.T) {
    59  	saveBuffer := new(bytes.Buffer)
    60  	ts := testGatePipelineTemplateCreateSuccess(saveBuffer)
    61  	defer ts.Close()
    62  
    63  	tempFile := tempPipelineTemplateFile(testPipelineTemplateYamlStr)
    64  	if tempFile == nil {
    65  		t.Fatal("Could not create temp pipeline template file.")
    66  	}
    67  	defer os.Remove(tempFile.Name())
    68  
    69  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
    70  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
    71  
    72  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
    73  	rootCmd.SetArgs(args)
    74  	err := rootCmd.Execute()
    75  	if err != nil {
    76  		t.Fatalf("Command failed with: %s", err)
    77  	}
    78  
    79  	expected := strings.TrimSpace(testPipelineTemplateJsonStr)
    80  	recieved := saveBuffer.Bytes()
    81  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
    82  }
    83  
    84  func TestPipelineTemplateSave_createtag(t *testing.T) {
    85  	saveBuffer := new(bytes.Buffer)
    86  	ts := testGatePipelineTemplateCreateSuccess(saveBuffer)
    87  	defer ts.Close()
    88  
    89  	tempFile := tempPipelineTemplateFile(testPipelineTemplateJsonStr)
    90  	if tempFile == nil {
    91  		t.Fatal("Could not create temp pipeline template file.")
    92  	}
    93  	defer os.Remove(tempFile.Name())
    94  
    95  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
    96  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
    97  
    98  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--tag", "stable", "--gate-endpoint", ts.URL}
    99  	rootCmd.SetArgs(args)
   100  	err := rootCmd.Execute()
   101  	if err != nil {
   102  		t.Fatalf("Command failed with: %s", err)
   103  	}
   104  
   105  	expected := strings.TrimSpace(testPipelineTemplateJsonStr)
   106  	recieved := saveBuffer.Bytes()
   107  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
   108  }
   109  
   110  func TestPipelineTemplateSave_update(t *testing.T) {
   111  	saveBuffer := new(bytes.Buffer)
   112  	ts := testGatePipelineTemplateUpdateSuccess(saveBuffer)
   113  	defer ts.Close()
   114  
   115  	tempFile := tempPipelineTemplateFile(testPipelineTemplateJsonStr)
   116  	if tempFile == nil {
   117  		t.Fatal("Could not create temp pipeline template file.")
   118  	}
   119  	defer os.Remove(tempFile.Name())
   120  
   121  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   122  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   123  
   124  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
   125  	rootCmd.SetArgs(args)
   126  	err := rootCmd.Execute()
   127  	if err != nil {
   128  		t.Fatalf("Command failed with: %s", err)
   129  	}
   130  
   131  	expected := strings.TrimSpace(testPipelineTemplateJsonStr)
   132  	recieved := saveBuffer.Bytes()
   133  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
   134  }
   135  
   136  func TestPipelineTemplateSave_updatetagfromfile(t *testing.T) {
   137  	saveBuffer := new(bytes.Buffer)
   138  	method := new(string)
   139  	ts := testGatePipelineTemplateUpdateTagSuccess(saveBuffer, method, "stable")
   140  	defer ts.Close()
   141  
   142  	tempFile := tempPipelineTemplateFile(testPipelineTemplateWithTagJsonStr)
   143  	if tempFile == nil {
   144  		t.Fatal("Could not create temp pipeline template file.")
   145  	}
   146  	defer os.Remove(tempFile.Name())
   147  
   148  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   149  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   150  
   151  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
   152  	rootCmd.SetArgs(args)
   153  	err := rootCmd.Execute()
   154  	if err != nil {
   155  		t.Fatalf("Command failed with: %s", err)
   156  	}
   157  
   158  	expected := strings.TrimSpace(testPipelineTemplateWithTagJsonStr)
   159  	recieved := saveBuffer.Bytes()
   160  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
   161  
   162  	// Verify that the commad used the tag from the file
   163  	if *method != "update" {
   164  		t.Fatalf("Expected 'update' request, got %s", *method)
   165  	}
   166  }
   167  
   168  func TestPipelineTemplateSave_taginargumentsandfile(t *testing.T) {
   169  	saveBuffer := new(bytes.Buffer)
   170  	method := new(string)
   171  	ts := testGatePipelineTemplateUpdateTagSuccess(saveBuffer, method, "test")
   172  	defer ts.Close()
   173  
   174  	tempFile := tempPipelineTemplateFile(testPipelineTemplateWithTagJsonStr)
   175  	if tempFile == nil {
   176  		t.Fatal("Could not create temp pipeline template file.")
   177  	}
   178  	defer os.Remove(tempFile.Name())
   179  
   180  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   181  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   182  
   183  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--tag", "test", "--gate-endpoint", ts.URL}
   184  	rootCmd.SetArgs(args)
   185  	err := rootCmd.Execute()
   186  	if err != nil {
   187  		t.Fatalf("Command failed with: %s", err)
   188  	}
   189  
   190  	expected := strings.TrimSpace(testPipelineTemplateWithTagJsonStr)
   191  	recieved := saveBuffer.Bytes()
   192  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
   193  
   194  	// Tag in arguments should take precedence over tag in file
   195  	if *method != "update" {
   196  		t.Fatalf("Expected 'update' request, got %s", *method)
   197  	}
   198  }
   199  
   200  func TestPipelineTemplateSave_updatetag(t *testing.T) {
   201  	saveBuffer := new(bytes.Buffer)
   202  	ts := testGatePipelineTemplateUpdateSuccess(saveBuffer)
   203  	defer ts.Close()
   204  
   205  	tempFile := tempPipelineTemplateFile(testPipelineTemplateJsonStr)
   206  	if tempFile == nil {
   207  		t.Fatal("Could not create temp pipeline template file.")
   208  	}
   209  	defer os.Remove(tempFile.Name())
   210  
   211  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   212  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   213  
   214  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--tag", "stable", "--gate-endpoint", ts.URL}
   215  	rootCmd.SetArgs(args)
   216  	err := rootCmd.Execute()
   217  	if err != nil {
   218  		t.Fatalf("Command failed with: %s", err)
   219  	}
   220  
   221  	expected := strings.TrimSpace(testPipelineTemplateJsonStr)
   222  	recieved := saveBuffer.Bytes()
   223  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
   224  }
   225  
   226  func TestPipelineTemplateSave_stdin(t *testing.T) {
   227  	saveBuffer := new(bytes.Buffer)
   228  	ts := testGatePipelineTemplateUpdateSuccess(saveBuffer)
   229  	defer ts.Close()
   230  
   231  	tempFile := tempPipelineTemplateFile(testPipelineTemplateJsonStr)
   232  	if tempFile == nil {
   233  		t.Fatal("Could not create temp pipeline template file.")
   234  	}
   235  	defer os.Remove(tempFile.Name())
   236  
   237  	// Prepare Stdin for test reading.
   238  	tempFile.Seek(0, 0)
   239  	oldStdin := os.Stdin
   240  	defer func() { os.Stdin = oldStdin }()
   241  	os.Stdin = tempFile
   242  
   243  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   244  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   245  
   246  	args := []string{"pipeline-template", "save", "--gate-endpoint", ts.URL}
   247  	rootCmd.SetArgs(args)
   248  	err := rootCmd.Execute()
   249  	if err != nil {
   250  		t.Fatalf("Command failed with: %s", err)
   251  	}
   252  
   253  	expected := strings.TrimSpace(testPipelineTemplateJsonStr)
   254  	recieved := saveBuffer.Bytes()
   255  	util.TestPrettyJsonDiff(t, "save request body", expected, recieved)
   256  }
   257  
   258  func TestPipelineTemplateSave_fail(t *testing.T) {
   259  	ts := testGateFail()
   260  	defer ts.Close()
   261  
   262  	tempFile := tempPipelineTemplateFile(testPipelineTemplateJsonStr)
   263  	if tempFile == nil {
   264  		t.Fatal("Could not create temp pipeline template file.")
   265  	}
   266  	defer os.Remove(tempFile.Name())
   267  
   268  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   269  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   270  
   271  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
   272  
   273  	rootCmd.SetArgs(args)
   274  	err := rootCmd.Execute()
   275  	if err == nil {
   276  		t.Fatalf("Command failed with: %s", err)
   277  	}
   278  }
   279  
   280  func TestPipelineTemplateSave_flags(t *testing.T) {
   281  	saveBuffer := new(bytes.Buffer)
   282  	ts := testGatePipelineTemplateUpdateSuccess(saveBuffer)
   283  	defer ts.Close()
   284  
   285  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   286  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   287  
   288  	args := []string{"pipeline-template", "save", "--gate-endpoint", ts.URL} // Missing pipeline spec file and stdin.
   289  	rootCmd.SetArgs(args)
   290  	err := rootCmd.Execute()
   291  	if err == nil {
   292  		t.Fatalf("Command failed with: %s", err)
   293  	}
   294  
   295  	expected := ""
   296  	recieved := strings.TrimSpace(saveBuffer.String())
   297  	if expected != recieved {
   298  		t.Fatalf("Unexpected save request body:\n%s", recieved)
   299  	}
   300  }
   301  
   302  func TestPipelineTemplateSave_missingid(t *testing.T) {
   303  	saveBuffer := new(bytes.Buffer)
   304  	ts := testGatePipelineTemplateUpdateSuccess(saveBuffer)
   305  	defer ts.Close()
   306  
   307  	tempFile := tempPipelineTemplateFile(missingIdJsonStr)
   308  	if tempFile == nil {
   309  		t.Fatal("Could not create temp pipeline file.")
   310  	}
   311  	defer os.Remove(tempFile.Name())
   312  
   313  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   314  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   315  
   316  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
   317  	rootCmd.SetArgs(args)
   318  	err := rootCmd.Execute()
   319  	if err == nil {
   320  		t.Fatalf("Command failed with: %s", err)
   321  	}
   322  
   323  	expected := ""
   324  	recieved := strings.TrimSpace(saveBuffer.String())
   325  	if expected != recieved {
   326  		t.Fatalf("Unexpected save request body:\n%s", recieved)
   327  	}
   328  }
   329  
   330  func TestPipelineTemplateSave_missingschema(t *testing.T) {
   331  	saveBuffer := new(bytes.Buffer)
   332  	ts := testGatePipelineTemplateUpdateSuccess(saveBuffer)
   333  	defer ts.Close()
   334  
   335  	tempFile := tempPipelineTemplateFile(missingSchemaJsonStr)
   336  	if tempFile == nil {
   337  		t.Fatal("Could not create temp pipeline file.")
   338  	}
   339  	defer os.Remove(tempFile.Name())
   340  
   341  	rootCmd, rootOpts := cmd.NewCmdRoot(ioutil.Discard, ioutil.Discard)
   342  	rootCmd.AddCommand(NewPipelineTemplateCmd(rootOpts))
   343  
   344  	args := []string{"pipeline-template", "save", "--file", tempFile.Name(), "--gate-endpoint", ts.URL}
   345  	rootCmd.SetArgs(args)
   346  	err := rootCmd.Execute()
   347  	if err == nil {
   348  		t.Fatalf("Command failed with: %s", err)
   349  	}
   350  
   351  	expected := ""
   352  	recieved := strings.TrimSpace(saveBuffer.String())
   353  	if expected != recieved {
   354  		t.Fatalf("Unexpected save request body:\n%s", recieved)
   355  	}
   356  }
   357  
   358  func tempPipelineTemplateFile(pipelineContent string) *os.File {
   359  	tempFile, _ := ioutil.TempFile("" /* /tmp dir. */, "pipeline-spec")
   360  	bytes, err := tempFile.Write([]byte(pipelineContent))
   361  	if err != nil || bytes == 0 {
   362  		fmt.Println("Could not write temp file.")
   363  		return nil
   364  	}
   365  	return tempFile
   366  }
   367  
   368  // testGatePipelineTemplateUpdateTagSuccess spins up a local http server that we will configure the GateClient
   369  // to direct requests to.
   370  // Responds with OK to indicate a pipeline template with an expected tag exists.
   371  // Responds with 404 NotFound to indicate a pipeline template with an expected tag doesn't exist.
   372  // Accepts POST calls for create and update requests.
   373  // Writes used method and request body to buffer for testing.
   374  func testGatePipelineTemplateUpdateTagSuccess(buffer io.Writer, method *string, expectedTag string) *httptest.Server {
   375  	mux := util.TestGateMuxWithVersionHandler()
   376  	mux.Handle(
   377  		"/v2/pipelineTemplates/update/testSpelTemplate",
   378  		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   379  			*method = "update"
   380  			util.NewTestBufferHandlerFunc(http.MethodPost, buffer, http.StatusOK, "").ServeHTTP(w, r)
   381  		}),
   382  	)
   383  	mux.Handle(
   384  		"/v2/pipelineTemplates/create",
   385  		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   386  			*method = "create"
   387  			util.NewTestBufferHandlerFunc(http.MethodPost, buffer, http.StatusAccepted, "").ServeHTTP(w, r)
   388  		}),
   389  	)
   390  	// Return that we found an MPT if a tag from the request equals to expectedTag.
   391  	mux.Handle("/v2/pipelineTemplates/testSpelTemplate", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   392  		if r.URL.Query().Get("tag") == expectedTag {
   393  			w.WriteHeader(http.StatusOK)
   394  		} else {
   395  			w.WriteHeader(http.StatusNotFound)
   396  		}
   397  	}))
   398  	return httptest.NewServer(mux)
   399  }
   400  
   401  // testGatePipelineTemplateUpdateSuccess spins up a local http server that we will configure the GateClient
   402  // to direct requests to. Responds with OK to indicate a pipeline template exists,
   403  // and Accepts POST calls.
   404  // Writes request body to buffer for testing.
   405  func testGatePipelineTemplateUpdateSuccess(buffer io.Writer) *httptest.Server {
   406  	mux := util.TestGateMuxWithVersionHandler()
   407  	mux.Handle(
   408  		"/v2/pipelineTemplates/update/testSpelTemplate",
   409  		util.NewTestBufferHandlerFunc(http.MethodPost, buffer, http.StatusOK, ""),
   410  	)
   411  	// Return that we found an MPT to signal that we should update.
   412  	mux.Handle("/v2/pipelineTemplates/testSpelTemplate", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   413  		w.WriteHeader(http.StatusOK)
   414  	}))
   415  	return httptest.NewServer(mux)
   416  }
   417  
   418  // testGatePipelineTemplateCreateSuccess spins up a local http server that we will configure the GateClient
   419  // to direct requests to. Responds with 404 NotFound to indicate a pipeline template doesn't exist,
   420  // and Accepts POST calls.
   421  // Writes request body to buffer for testing.
   422  func testGatePipelineTemplateCreateSuccess(buffer io.Writer) *httptest.Server {
   423  	mux := util.TestGateMuxWithVersionHandler()
   424  	mux.Handle(
   425  		"/v2/pipelineTemplates/create",
   426  		util.NewTestBufferHandlerFunc(http.MethodPost, buffer, http.StatusAccepted, ""),
   427  	)
   428  	// Return that there are no existing MPTs.
   429  	mux.Handle("/v2/pipelineTemplates/testSpelTemplate", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   430  		w.WriteHeader(http.StatusNotFound)
   431  	}))
   432  	return httptest.NewServer(mux)
   433  }
   434  
   435  const missingSchemaJsonStr = `
   436  {
   437   "id": "testSpelTemplate",
   438   "lastModifiedBy": "anonymous",
   439   "metadata": {
   440    "description": "A generic application bake and tag pipeline.",
   441    "name": "Default Bake and Tag",
   442    "owner": "example@example.com",
   443    "scopes": [
   444     "global"
   445    ]
   446   },
   447   "pipeline": {
   448    "description": "",
   449    "keepWaitingPipelines": false,
   450    "lastModifiedBy": "anonymous",
   451    "limitConcurrent": true,
   452    "notifications": [],
   453    "parameterConfig": [],
   454    "stages": [
   455     {
   456      "name": "My Wait Stage",
   457      "refId": "wait1",
   458      "requisiteStageRefIds": [],
   459      "type": "wait",
   460      "waitTime": "${ templateVariables.waitTime }"
   461     }
   462    ],
   463    "triggers": [
   464     {
   465      "attributeConstraints": {},
   466      "enabled": true,
   467      "payloadConstraints": {},
   468      "pubsubSystem": "google",
   469      "source": "jake",
   470      "subscription": "super-why",
   471      "subscriptionName": "super-why",
   472      "type": "pubsub"
   473     }
   474    ],
   475    "updateTs": "1543509523663"
   476   },
   477   "protect": false,
   478   "updateTs": "1544475186050",
   479   "variables": [
   480    {
   481     "defaultValue": 42,
   482     "description": "The time a wait stage shall pauseth",
   483     "name": "waitTime",
   484     "type": "int"
   485    }
   486   ]
   487  }
   488  `
   489  
   490  const missingIdJsonStr = `
   491  {
   492   "lastModifiedBy": "anonymous",
   493   "metadata": {
   494    "description": "A generic application bake and tag pipeline.",
   495    "name": "Default Bake and Tag",
   496    "owner": "example@example.com",
   497    "scopes": [
   498     "global"
   499    ]
   500   },
   501   "pipeline": {
   502    "description": "",
   503    "keepWaitingPipelines": false,
   504    "lastModifiedBy": "anonymous",
   505    "limitConcurrent": true,
   506    "notifications": [],
   507    "parameterConfig": [],
   508    "stages": [
   509     {
   510      "name": "My Wait Stage",
   511      "refId": "wait1",
   512      "requisiteStageRefIds": [],
   513      "type": "wait",
   514      "waitTime": "${ templateVariables.waitTime }"
   515     }
   516    ],
   517    "triggers": [
   518     {
   519      "attributeConstraints": {},
   520      "enabled": true,
   521      "payloadConstraints": {},
   522      "pubsubSystem": "google",
   523      "source": "jake",
   524      "subscription": "super-why",
   525      "subscriptionName": "super-why",
   526      "type": "pubsub"
   527     }
   528    ],
   529    "updateTs": "1543509523663"
   530   },
   531   "protect": false,
   532   "schema": "v2",
   533   "updateTs": "1544475186050",
   534   "variables": [
   535    {
   536     "defaultValue": 42,
   537     "description": "The time a wait stage shall pauseth",
   538     "name": "waitTime",
   539     "type": "int"
   540    }
   541   ]
   542  }
   543  `
   544  
   545  const testPipelineTemplateJsonStr = `
   546  {
   547   "id": "testSpelTemplate",
   548   "lastModifiedBy": "anonymous",
   549   "metadata": {
   550    "description": "A generic application bake and tag pipeline.",
   551    "name": "Default Bake and Tag",
   552    "owner": "example@example.com",
   553    "scopes": [
   554     "global"
   555    ]
   556   },
   557   "pipeline": {
   558    "description": "",
   559    "keepWaitingPipelines": false,
   560    "lastModifiedBy": "anonymous",
   561    "limitConcurrent": true,
   562    "notifications": [],
   563    "parameterConfig": [],
   564    "stages": [
   565     {
   566      "name": "My Wait Stage",
   567      "refId": "wait1",
   568      "requisiteStageRefIds": [],
   569      "type": "wait",
   570      "waitTime": "${ templateVariables.waitTime }"
   571     }
   572    ],
   573    "triggers": [
   574     {
   575      "attributeConstraints": {},
   576      "enabled": true,
   577      "payloadConstraints": {},
   578      "pubsubSystem": "google",
   579      "source": "jake",
   580      "subscription": "super-why",
   581      "subscriptionName": "super-why",
   582      "type": "pubsub"
   583     }
   584    ],
   585    "updateTs": "1543509523663"
   586   },
   587   "protect": false,
   588   "schema": "v2",
   589   "updateTs": "1544475186050",
   590   "variables": [
   591    {
   592     "defaultValue": 42,
   593     "description": "The time a wait stage shall pauseth",
   594     "name": "waitTime",
   595     "type": "int"
   596    }
   597   ]
   598  }
   599  `
   600  
   601  const testPipelineTemplateWithTagJsonStr = `
   602  {
   603   "id": "testSpelTemplate",
   604   "lastModifiedBy": "anonymous",
   605   "metadata": {
   606    "description": "A generic application bake and tag pipeline.",
   607    "name": "Default Bake and Tag",
   608    "owner": "example@example.com",
   609    "scopes": [
   610     "global"
   611    ]
   612   },
   613   "pipeline": {
   614    "description": "",
   615    "keepWaitingPipelines": false,
   616    "lastModifiedBy": "anonymous",
   617    "limitConcurrent": true,
   618    "notifications": [],
   619    "parameterConfig": [],
   620    "stages": [
   621     {
   622      "name": "My Wait Stage",
   623      "refId": "wait1",
   624      "requisiteStageRefIds": [],
   625      "type": "wait",
   626      "waitTime": "${ templateVariables.waitTime }"
   627     }
   628    ],
   629    "triggers": [
   630     {
   631      "attributeConstraints": {},
   632      "enabled": true,
   633      "payloadConstraints": {},
   634      "pubsubSystem": "google",
   635      "source": "jake",
   636      "subscription": "super-why",
   637      "subscriptionName": "super-why",
   638      "type": "pubsub"
   639     }
   640    ],
   641    "updateTs": "1543509523663"
   642   },
   643   "protect": false,
   644   "schema": "v2",
   645   "tag": "stable",
   646   "updateTs": "1544475186050",
   647   "variables": [
   648    {
   649     "defaultValue": 42,
   650     "description": "The time a wait stage shall pauseth",
   651     "name": "waitTime",
   652     "type": "int"
   653    }
   654   ]
   655  }
   656  `
   657  
   658  const testPipelineTemplateYamlStr = `
   659  id: testSpelTemplate
   660  lastModifiedBy: anonymous
   661  metadata:
   662    description: A generic application bake and tag pipeline.
   663    name: Default Bake and Tag
   664    owner: example@example.com
   665    scopes:
   666    - global
   667  pipeline:
   668    description: ''
   669    keepWaitingPipelines: false
   670    lastModifiedBy: anonymous
   671    limitConcurrent: true
   672    notifications: []
   673    parameterConfig: []
   674    stages:
   675    - name: My Wait Stage
   676      refId: wait1
   677      requisiteStageRefIds: []
   678      type: wait
   679      waitTime: "${ templateVariables.waitTime }"
   680    triggers:
   681    - attributeConstraints: {}
   682      enabled: true
   683      payloadConstraints: {}
   684      pubsubSystem: google
   685      source: jake
   686      subscription: super-why
   687      subscriptionName: super-why
   688      type: pubsub
   689    updateTs: '1543509523663'
   690  protect: false
   691  schema: v2
   692  updateTs: '1544475186050'
   693  variables:
   694  - defaultValue: 42
   695    description: The time a wait stage shall pauseth
   696    name: waitTime
   697    type: int
   698  `