github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/api/config_test.go (about)

     1  package api_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"time"
    10  
    11  	"github.com/pf-qiu/concourse/v6/atc"
    12  	"github.com/pf-qiu/concourse/v6/atc/creds/noop"
    13  	"github.com/pf-qiu/concourse/v6/atc/db"
    14  	"github.com/pf-qiu/concourse/v6/atc/db/dbfakes"
    15  	. "github.com/pf-qiu/concourse/v6/atc/testhelpers"
    16  	"github.com/onsi/gomega/gbytes"
    17  	"github.com/tedsuo/rata"
    18  	"sigs.k8s.io/yaml"
    19  
    20  	// load dummy credential manager
    21  	_ "github.com/pf-qiu/concourse/v6/atc/creds/dummy"
    22  
    23  	. "github.com/onsi/ginkgo"
    24  	. "github.com/onsi/gomega"
    25  )
    26  
    27  var _ = Describe("Config API", func() {
    28  	var (
    29  		pipelineConfig   atc.Config
    30  		requestGenerator *rata.RequestGenerator
    31  	)
    32  
    33  	BeforeEach(func() {
    34  		requestGenerator = rata.NewRequestGenerator(server.URL, atc.Routes)
    35  
    36  		pipelineConfig = atc.Config{
    37  			Groups: atc.GroupConfigs{
    38  				{
    39  					Name:      "some-group",
    40  					Jobs:      []string{"some-job"},
    41  					Resources: []string{"some-resource"},
    42  				},
    43  			},
    44  
    45  			VarSources: atc.VarSourceConfigs{
    46  				{
    47  					Name: "some",
    48  					Type: "dummy",
    49  					Config: map[string]interface{}{
    50  						"vars": map[string]interface{}{},
    51  					},
    52  				},
    53  			},
    54  
    55  			Resources: atc.ResourceConfigs{
    56  				{
    57  					Name: "some-resource",
    58  					Type: "some-type",
    59  					Source: atc.Source{
    60  						"source-config": "some-value",
    61  					},
    62  				},
    63  			},
    64  
    65  			ResourceTypes: atc.ResourceTypes{
    66  				{
    67  					Name:   "custom-resource",
    68  					Type:   "custom-type",
    69  					Source: atc.Source{"custom": "source"},
    70  					Tags:   atc.Tags{"some-tag"},
    71  				},
    72  			},
    73  
    74  			Jobs: atc.JobConfigs{
    75  				{
    76  					Name:   "some-job",
    77  					Public: true,
    78  					Serial: true,
    79  					PlanSequence: []atc.Step{
    80  						{
    81  							Config: &atc.GetStep{
    82  								Name:     "some-input",
    83  								Resource: "some-resource",
    84  								Params:   atc.Params{"some-param": "some-value"},
    85  							},
    86  						},
    87  						{
    88  							Config: &atc.TaskStep{
    89  								Name:       "some-task",
    90  								Privileged: true,
    91  								Config: &atc.TaskConfig{
    92  									Platform:  "linux",
    93  									RootfsURI: "some-image",
    94  									Run: atc.TaskRunConfig{
    95  										Path: "/path/to/run",
    96  									},
    97  								},
    98  							},
    99  						},
   100  						{
   101  							Config: &atc.PutStep{
   102  								Name:     "some-output",
   103  								Resource: "some-resource",
   104  								Params:   atc.Params{"some-param": "some-value"},
   105  							},
   106  						},
   107  					},
   108  				},
   109  			},
   110  		}
   111  	})
   112  
   113  	Describe("GET /api/v1/teams/:team_name/pipelines/:name/config", func() {
   114  		var (
   115  			request  *http.Request
   116  			response *http.Response
   117  		)
   118  
   119  		BeforeEach(func() {
   120  			var err error
   121  			request, err = requestGenerator.CreateRequest(atc.GetConfig, rata.Params{
   122  				"team_name":     "a-team",
   123  				"pipeline_name": "something-else",
   124  			}, nil)
   125  			Expect(err).NotTo(HaveOccurred())
   126  		})
   127  
   128  		JustBeforeEach(func() {
   129  			var err error
   130  			response, err = client.Do(request)
   131  			Expect(err).NotTo(HaveOccurred())
   132  		})
   133  
   134  		Context("when authorized", func() {
   135  			BeforeEach(func() {
   136  				fakeAccess.IsAuthenticatedReturns(true)
   137  				fakeAccess.IsAuthorizedReturns(true)
   138  			})
   139  
   140  			Context("when the team is found", func() {
   141  				var fakeTeam *dbfakes.FakeTeam
   142  				BeforeEach(func() {
   143  					fakeTeam = new(dbfakes.FakeTeam)
   144  					fakeTeam.NameReturns("a-team")
   145  					dbTeamFactory.FindTeamReturns(fakeTeam, true, nil)
   146  				})
   147  
   148  				Context("when the pipeline is found", func() {
   149  					var fakePipeline *dbfakes.FakePipeline
   150  					BeforeEach(func() {
   151  						fakePipeline = new(dbfakes.FakePipeline)
   152  						fakePipeline.NameReturns("something-else")
   153  						fakePipeline.ConfigVersionReturns(1)
   154  						fakePipeline.GroupsReturns(atc.GroupConfigs{
   155  							{
   156  								Name:      "some-group",
   157  								Jobs:      []string{"some-job"},
   158  								Resources: []string{"some-resource"},
   159  							},
   160  						})
   161  						fakePipeline.VarSourcesReturns(atc.VarSourceConfigs{
   162  							{
   163  								Name: "some",
   164  								Type: "dummy",
   165  								Config: map[string]interface{}{
   166  									"vars": map[string]interface{}{},
   167  								},
   168  							},
   169  						})
   170  						fakeTeam.PipelineReturns(fakePipeline, true, nil)
   171  					})
   172  
   173  					Context("when instance vars ar specified", func() {
   174  						Context("when instance vars are malformed", func() {
   175  							BeforeEach(func() {
   176  								query := request.URL.Query()
   177  								query.Add("instance_vars", "{")
   178  								request.URL.RawQuery = query.Encode()
   179  							})
   180  
   181  							It("returns 400", func() {
   182  								Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   183  							})
   184  
   185  							It("returns Content-Type 'application/json'", func() {
   186  								expectedHeaderEntries := map[string]string{
   187  									"Content-Type": "application/json",
   188  								}
   189  								Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   190  							})
   191  
   192  							It("returns an error in the response body", func() {
   193  								Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
   194  										{
   195  											"errors": [
   196  												"instance_vars is malformed: unexpected end of JSON input"
   197  											]
   198  										}`))
   199  							})
   200  
   201  							It("doesn't find the pipeline", func() {
   202  								Expect(dbTeam.PipelineCallCount()).To(Equal(0))
   203  							})
   204  						})
   205  
   206  						Context("when instance vars is valid", func() {
   207  							BeforeEach(func() {
   208  								query := request.URL.Query()
   209  								query.Add("instance_vars", "{\"branch\":\"feature\"}")
   210  								request.URL.RawQuery = query.Encode()
   211  
   212  								fakePipeline.InstanceVarsReturns(atc.InstanceVars{"branch": "feature"})
   213  							})
   214  
   215  							It("finds the pipeline", func() {
   216  								Expect(fakeTeam.PipelineCallCount()).To(Equal(1))
   217  
   218  								ref := fakeTeam.PipelineArgsForCall(0)
   219  								Expect(ref).To(Equal(atc.PipelineRef{
   220  									Name:         "something-else",
   221  									InstanceVars: atc.InstanceVars{"branch": "feature"},
   222  								}))
   223  							})
   224  						})
   225  					})
   226  
   227  					Context("when the pipeline config is found", func() {
   228  						BeforeEach(func() {
   229  							fakePipeline.ConfigReturns(pipelineConfig, nil)
   230  						})
   231  
   232  						It("returns 200", func() {
   233  							Expect(response.StatusCode).To(Equal(http.StatusOK))
   234  						})
   235  
   236  						It("returns Content-Type 'application/json' and config version as X-Concourse-Config-Version", func() {
   237  							expectedHeaderEntries := map[string]string{
   238  								"Content-Type":          "application/json",
   239  								atc.ConfigVersionHeader: "1",
   240  							}
   241  							Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   242  						})
   243  
   244  						It("returns the config", func() {
   245  							var actualConfigResponse atc.ConfigResponse
   246  							err := json.NewDecoder(response.Body).Decode(&actualConfigResponse)
   247  							Expect(err).NotTo(HaveOccurred())
   248  
   249  							Expect(actualConfigResponse).To(Equal(atc.ConfigResponse{
   250  								Config: pipelineConfig,
   251  							}))
   252  						})
   253  
   254  						Context("when finding the config fails", func() {
   255  							BeforeEach(func() {
   256  								fakePipeline.ConfigReturns(atc.Config{}, errors.New("fail"))
   257  							})
   258  
   259  							It("returns 500", func() {
   260  								Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
   261  							})
   262  						})
   263  					})
   264  
   265  					Context("when the pipeline is archived", func() {
   266  						BeforeEach(func() {
   267  							fakePipeline.ArchivedReturns(true)
   268  						})
   269  						It("returns 404", func() {
   270  							Expect(response.StatusCode).To(Equal(http.StatusNotFound))
   271  						})
   272  					})
   273  				})
   274  
   275  				Context("when the pipeline is not found", func() {
   276  					BeforeEach(func() {
   277  						fakeTeam.PipelineReturns(nil, false, nil)
   278  					})
   279  
   280  					It("returns 404", func() {
   281  						Expect(response.StatusCode).To(Equal(http.StatusNotFound))
   282  					})
   283  				})
   284  
   285  				Context("when finding the pipeline fails", func() {
   286  					BeforeEach(func() {
   287  						fakeTeam.PipelineReturns(nil, false, errors.New("failed"))
   288  					})
   289  
   290  					It("returns 500", func() {
   291  						Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
   292  					})
   293  				})
   294  			})
   295  
   296  			Context("when the team is not found", func() {
   297  				BeforeEach(func() {
   298  					dbTeamFactory.FindTeamReturns(nil, false, nil)
   299  				})
   300  
   301  				It("returns 404", func() {
   302  					Expect(response.StatusCode).To(Equal(http.StatusNotFound))
   303  				})
   304  			})
   305  
   306  			Context("when finding the team fails", func() {
   307  				BeforeEach(func() {
   308  					dbTeamFactory.FindTeamReturns(nil, false, errors.New("failed"))
   309  				})
   310  
   311  				It("returns 500", func() {
   312  					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
   313  				})
   314  			})
   315  		})
   316  
   317  		Context("when not authenticated", func() {
   318  			BeforeEach(func() {
   319  				fakeAccess.IsAuthenticatedReturns(false)
   320  			})
   321  
   322  			It("returns 401", func() {
   323  				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
   324  			})
   325  		})
   326  	})
   327  
   328  	Describe("PUT /api/v1/teams/:team_name/pipelines/:name/config", func() {
   329  		var (
   330  			request  *http.Request
   331  			response *http.Response
   332  		)
   333  
   334  		BeforeEach(func() {
   335  			var err error
   336  			request, err = requestGenerator.CreateRequest(atc.SaveConfig, rata.Params{
   337  				"team_name":     "a-team",
   338  				"pipeline_name": "a-pipeline",
   339  			}, nil)
   340  			Expect(err).NotTo(HaveOccurred())
   341  		})
   342  
   343  		JustBeforeEach(func() {
   344  			var err error
   345  			response, err = client.Do(request)
   346  			Expect(err).NotTo(HaveOccurred())
   347  		})
   348  
   349  		Context("when authorized", func() {
   350  			BeforeEach(func() {
   351  				fakeAccess.IsAuthenticatedReturns(true)
   352  				fakeAccess.IsAuthorizedReturns(true)
   353  			})
   354  
   355  			Context("when an identifier is invalid", func() {
   356  
   357  				BeforeEach(func() {
   358  					var err error
   359  					request, err = requestGenerator.CreateRequest(atc.SaveConfig, rata.Params{
   360  						"team_name":     "_team",
   361  						"pipeline_name": "_pipeline",
   362  					}, nil)
   363  					Expect(err).NotTo(HaveOccurred())
   364  
   365  					request.Header.Set("Content-Type", "application/json")
   366  
   367  					payload, err := json.Marshal(pipelineConfig)
   368  					Expect(err).NotTo(HaveOccurred())
   369  
   370  					request.Body = gbytes.BufferWithBytes(payload)
   371  				})
   372  
   373  				It("returns warnings in the response body", func() {
   374  					Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
   375  							{
   376  								"warnings": [
   377  									{
   378  										"type": "invalid_identifier",
   379  										"message": "pipeline: '_pipeline' is not a valid identifier: must start with a lowercase letter"
   380  									},
   381  									{
   382  										"type": "invalid_identifier",
   383  										"message": "team: '_team' is not a valid identifier: must start with a lowercase letter"
   384  									}
   385  								]
   386  							}`))
   387  				})
   388  			})
   389  
   390  			Context("when a config version is specified", func() {
   391  				BeforeEach(func() {
   392  					request.Header.Set(atc.ConfigVersionHeader, "42")
   393  				})
   394  
   395  				Context("when the config is malformed", func() {
   396  					Context("JSON", func() {
   397  						BeforeEach(func() {
   398  							request.Header.Set("Content-Type", "application/json")
   399  							request.Body = gbytes.BufferWithBytes([]byte(`{`))
   400  						})
   401  
   402  						It("returns 400", func() {
   403  							Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   404  						})
   405  
   406  						It("returns Content-Type 'application/json'", func() {
   407  							expectedHeaderEntries := map[string]string{
   408  								"Content-Type": "application/json",
   409  							}
   410  							Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   411  						})
   412  
   413  						It("returns error JSON", func() {
   414  							Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
   415  								{
   416  									"errors": [
   417  										"malformed config: error converting YAML to JSON: yaml: line 1: did not find expected node content"
   418  									]
   419  								}`))
   420  						})
   421  
   422  						It("does not save anything", func() {
   423  							Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
   424  						})
   425  					})
   426  
   427  					Context("YAML", func() {
   428  						BeforeEach(func() {
   429  							request.Header.Set("Content-Type", "application/x-yaml")
   430  							request.Body = gbytes.BufferWithBytes([]byte(`{`))
   431  						})
   432  
   433  						It("returns 400", func() {
   434  							Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   435  						})
   436  
   437  						It("returns Content-Type 'application/json'", func() {
   438  							expectedHeaderEntries := map[string]string{
   439  								"Content-Type": "application/json",
   440  							}
   441  							Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   442  						})
   443  
   444  						It("returns error JSON", func() {
   445  							Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
   446  								{
   447  									"errors": [
   448  										"malformed config: error converting YAML to JSON: yaml: line 1: did not find expected node content"
   449  									]
   450  								}`))
   451  						})
   452  
   453  						It("does not save anything", func() {
   454  							Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
   455  						})
   456  					})
   457  				})
   458  
   459  				Context("when the config is valid", func() {
   460  					Context("JSON", func() {
   461  						BeforeEach(func() {
   462  							request.Header.Set("Content-Type", "application/json")
   463  
   464  							payload, err := json.Marshal(pipelineConfig)
   465  							Expect(err).NotTo(HaveOccurred())
   466  
   467  							request.Body = gbytes.BufferWithBytes(payload)
   468  						})
   469  
   470  						It("returns 200", func() {
   471  							Expect(response.StatusCode).To(Equal(http.StatusOK))
   472  						})
   473  
   474  						It("notifies the scanner to run", func() {
   475  							Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(1))
   476  						})
   477  
   478  						It("returns Content-Type 'application/json'", func() {
   479  							expectedHeaderEntries := map[string]string{
   480  								"Content-Type": "application/json",
   481  							}
   482  							Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   483  						})
   484  
   485  						It("saves it initially paused", func() {
   486  							Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
   487  
   488  							ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0)
   489  							Expect(ref.Name).To(Equal("a-pipeline"))
   490  							Expect(savedConfig).To(Equal(pipelineConfig))
   491  							Expect(id).To(Equal(db.ConfigVersion(42)))
   492  							Expect(initiallyPaused).To(BeTrue())
   493  						})
   494  
   495  						Context("and saving it fails", func() {
   496  							BeforeEach(func() {
   497  								dbTeam.SavePipelineReturns(nil, false, errors.New("oh no!"))
   498  							})
   499  
   500  							It("returns 500", func() {
   501  								Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
   502  							})
   503  
   504  							It("returns the error in the response body", func() {
   505  								Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("failed to save config: oh no!")))
   506  							})
   507  						})
   508  
   509  						Context("when it's the first time the pipeline has been created", func() {
   510  							BeforeEach(func() {
   511  								returnedPipeline := new(dbfakes.FakePipeline)
   512  								dbTeam.SavePipelineReturns(returnedPipeline, true, nil)
   513  							})
   514  
   515  							It("returns 201", func() {
   516  								Expect(response.StatusCode).To(Equal(http.StatusCreated))
   517  							})
   518  
   519  							It("does not notify the scanner to run", func() {
   520  								Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(0))
   521  							})
   522  						})
   523  
   524  						Context("when the config is invalid", func() {
   525  							BeforeEach(func() {
   526  								pipelineConfig.Groups[0].Resources = []string{"missing-resource"}
   527  								payload, err := json.Marshal(pipelineConfig)
   528  								Expect(err).NotTo(HaveOccurred())
   529  								request.Body = gbytes.BufferWithBytes(payload)
   530  							})
   531  
   532  							It("returns 400", func() {
   533  								Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   534  							})
   535  
   536  							It("returns Content-Type 'application/json'", func() {
   537  								expectedHeaderEntries := map[string]string{
   538  									"Content-Type": "application/json",
   539  								}
   540  								Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   541  							})
   542  
   543  							It("returns error JSON", func() {
   544  								Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
   545  								{
   546  									"errors": [
   547  										"invalid groups:\n\tgroup 'some-group' has unknown resource 'missing-resource'\n"
   548  									]
   549  								}`))
   550  							})
   551  
   552  							It("does not save it", func() {
   553  								Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
   554  							})
   555  						})
   556  					})
   557  
   558  					Context("YAML", func() {
   559  						BeforeEach(func() {
   560  							request.Header.Set("Content-Type", "application/x-yaml")
   561  
   562  							payload, err := yaml.Marshal(pipelineConfig)
   563  							Expect(err).NotTo(HaveOccurred())
   564  
   565  							request.Body = gbytes.BufferWithBytes(payload)
   566  						})
   567  
   568  						It("returns 200", func() {
   569  							Expect(response.StatusCode).To(Equal(http.StatusOK))
   570  						})
   571  
   572  						It("notifies the scanner to run", func() {
   573  							Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(1))
   574  						})
   575  
   576  						It("returns Content-Type 'application/json'", func() {
   577  							expectedHeaderEntries := map[string]string{
   578  								"Content-Type": "application/json",
   579  							}
   580  							Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   581  						})
   582  
   583  						It("saves it initially paused", func() {
   584  							Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
   585  
   586  							ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0)
   587  							Expect(ref.Name).To(Equal("a-pipeline"))
   588  							Expect(savedConfig).To(Equal(pipelineConfig))
   589  							Expect(id).To(Equal(db.ConfigVersion(42)))
   590  							Expect(initiallyPaused).To(BeTrue())
   591  						})
   592  
   593  						Context("when the payload contains suspicious types", func() {
   594  							BeforeEach(func() {
   595  								payload := `---
   596  resources:
   597  - name: some-resource
   598    type: some-type
   599    check_every: 10s
   600    check_timeout: 1m
   601  jobs:
   602  - name: some-job
   603    plan:
   604    - get: some-resource
   605    - task: some-task
   606      config:
   607        platform: linux
   608        run:
   609          path: ls
   610        params:
   611          FOO: true
   612          BAR: 1
   613          BAZ: 1.9`
   614  
   615  								request.Header.Set("Content-Type", "application/x-yaml")
   616  								request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   617  							})
   618  
   619  							It("returns 200", func() {
   620  								Expect(response.StatusCode).To(Equal(http.StatusOK))
   621  							})
   622  
   623  							It("returns Content-Type 'application/json'", func() {
   624  								expectedHeaderEntries := map[string]string{
   625  									"Content-Type": "application/json",
   626  								}
   627  								Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
   628  							})
   629  
   630  							It("saves it", func() {
   631  								Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
   632  
   633  								ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0)
   634  								Expect(ref.Name).To(Equal("a-pipeline"))
   635  								Expect(savedConfig).To(Equal(atc.Config{
   636  									Resources: []atc.ResourceConfig{
   637  										{
   638  											Name:         "some-resource",
   639  											Type:         "some-type",
   640  											Source:       nil,
   641  											CheckEvery:   "10s",
   642  											CheckTimeout: "1m",
   643  										},
   644  									},
   645  									Jobs: atc.JobConfigs{
   646  										{
   647  											Name: "some-job",
   648  											PlanSequence: []atc.Step{
   649  												{
   650  													Config: &atc.GetStep{
   651  														Name: "some-resource",
   652  													},
   653  												},
   654  												{
   655  													Config: &atc.TaskStep{
   656  														Name: "some-task",
   657  														Config: &atc.TaskConfig{
   658  															Platform: "linux",
   659  
   660  															Run: atc.TaskRunConfig{
   661  																Path: "ls",
   662  															},
   663  
   664  															Params: atc.TaskEnv{
   665  																"FOO": "true",
   666  																"BAR": "1",
   667  																"BAZ": "1.9",
   668  															},
   669  														},
   670  													},
   671  												},
   672  											},
   673  										},
   674  									},
   675  								}))
   676  								Expect(id).To(Equal(db.ConfigVersion(42)))
   677  								Expect(initiallyPaused).To(BeTrue())
   678  							})
   679  						})
   680  
   681  						Describe("test validate cred params when the check_creds param is set in request", func() {
   682  							var (
   683  								payload string
   684  							)
   685  
   686  							BeforeEach(func() {
   687  								query := request.URL.Query()
   688  								query.Add(atc.SaveConfigCheckCreds, "")
   689  								request.URL.RawQuery = query.Encode()
   690  							})
   691  
   692  							ExpectCredsValidationPass := func() {
   693  								Context("when the param exists in creds manager", func() {
   694  									BeforeEach(func() {
   695  										fakeSecretManager.GetReturns("this-string-value-doesn't-matter", nil, true, nil)
   696  									})
   697  
   698  									It("passes validation", func() {
   699  										Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
   700  									})
   701  
   702  									It("returns 200 ok", func() {
   703  										Expect(response.StatusCode).To(Equal(http.StatusOK))
   704  									})
   705  								})
   706  							}
   707  
   708  							ExpectCredsValidationFail := func() {
   709  								Context("when the param does not exist in creds manager", func() {
   710  									BeforeEach(func() {
   711  										fakeSecretManager.GetReturns(nil, nil, false, nil)
   712  									})
   713  
   714  									It("fail validation", func() {
   715  										Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
   716  									})
   717  
   718  									It("returns 400", func() {
   719  										Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   720  									})
   721  
   722  								})
   723  							}
   724  							Context("when there is param in resource type config", func() {
   725  								BeforeEach(func() {
   726  									payload = `---
   727  resource_types:
   728  - name: some-type
   729    type: some-base-resource-type
   730    source:
   731      FOO: ((BAR))
   732  
   733  jobs:
   734  - name: some-job
   735    plan:
   736    - task: some-task
   737      file: some/task/config.yaml`
   738  
   739  									request.Header.Set("Content-Type", "application/x-yaml")
   740  									request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   741  								})
   742  
   743  								ExpectCredsValidationPass()
   744  								ExpectCredsValidationFail()
   745  							})
   746  
   747  							Context("when there is param in resource source config", func() {
   748  								BeforeEach(func() {
   749  									payload = `---
   750  resources:
   751  - name: some-resource
   752    type: some-type
   753    source:
   754      FOO: ((BAR))
   755  jobs:
   756  - name: some-job
   757    plan:
   758    - get: some-resource`
   759  
   760  									request.Header.Set("Content-Type", "application/x-yaml")
   761  									request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   762  								})
   763  
   764  								ExpectCredsValidationPass()
   765  								ExpectCredsValidationFail()
   766  							})
   767  
   768  							Context("when there is param in resource webhook token", func() {
   769  								BeforeEach(func() {
   770  									payload = `---
   771  resources:
   772  - name: some-resource
   773    type: some-type
   774    webhook_token: ((BAR))
   775  jobs:
   776  - name: some-job
   777    plan:
   778    - get: some-resource`
   779  
   780  									request.Header.Set("Content-Type", "application/x-yaml")
   781  									request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   782  								})
   783  
   784  								ExpectCredsValidationPass()
   785  								ExpectCredsValidationFail()
   786  							})
   787  
   788  							Context("when it contains task that uses external config file and params in task params", func() {
   789  								BeforeEach(func() {
   790  									payload = `---
   791  resources:
   792  - name: some-resource
   793    type: some-type
   794    check_every: 10s
   795  jobs:
   796  - name: some-job
   797    plan:
   798    - get: some-resource
   799    - task: some-task
   800      file: some-resource/config.yml
   801      params:
   802        FOO: ((BAR))`
   803  
   804  									request.Header.Set("Content-Type", "application/x-yaml")
   805  									request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   806  								})
   807  
   808  								ExpectCredsValidationPass()
   809  								ExpectCredsValidationFail()
   810  							})
   811  
   812  							Context("when it contains task that uses external config file and params in task vars", func() {
   813  								BeforeEach(func() {
   814  									payload = `---
   815  resources:
   816  - name: some-resource
   817    type: some-type
   818    check_every: 10s
   819  jobs:
   820  - name: some-job
   821    plan:
   822    - get: some-resource
   823    - task: some-task
   824      file: some-resource/config.yml
   825      vars:
   826        FOO: ((BAR))`
   827  
   828  									request.Header.Set("Content-Type", "application/x-yaml")
   829  									request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   830  								})
   831  
   832  								ExpectCredsValidationPass()
   833  								ExpectCredsValidationFail()
   834  							})
   835  
   836  							Context("when it contains nested task that uses external config file and params in task vars", func() {
   837  								BeforeEach(func() {
   838  									payload = `---
   839  resources:
   840  - name: some-resource
   841    type: some-type
   842    check_every: 10s
   843  jobs:
   844  - name: some-job
   845    plan:
   846    - get: some-resource
   847    - do:
   848      - task: some-task
   849        file: some-resource/config.yml
   850        vars:
   851          FOO: ((BAR))`
   852  
   853  									request.Header.Set("Content-Type", "application/x-yaml")
   854  									request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   855  								})
   856  
   857  								ExpectCredsValidationPass()
   858  								ExpectCredsValidationFail()
   859  							})
   860  						})
   861  
   862  						Context("when it contains credentials to be interpolated", func() {
   863  							var (
   864  								payloadAsConfig atc.Config
   865  								payload         string
   866  							)
   867  
   868  							BeforeEach(func() {
   869  								payload = `---
   870  resources:
   871  - name: some-resource
   872    type: some-type
   873    check_every: 10s
   874  jobs:
   875  - name: some-job
   876    plan:
   877    - get: some-resource
   878    - task: some-task
   879      config:
   880        platform: linux
   881        run:
   882          path: ls
   883        params:
   884          FOO: ((BAR))`
   885  								payloadAsConfig = atc.Config{Resources: []atc.ResourceConfig{
   886  									{
   887  										Name:       "some-resource",
   888  										Type:       "some-type",
   889  										Source:     nil,
   890  										CheckEvery: "10s",
   891  									},
   892  								},
   893  									Jobs: atc.JobConfigs{
   894  										{
   895  											Name: "some-job",
   896  											PlanSequence: []atc.Step{
   897  												{
   898  													Config: &atc.GetStep{
   899  														Name: "some-resource",
   900  													},
   901  												},
   902  												{
   903  													Config: &atc.TaskStep{
   904  														Name: "some-task",
   905  														Config: &atc.TaskConfig{
   906  															Platform: "linux",
   907  
   908  															Run: atc.TaskRunConfig{
   909  																Path: "ls",
   910  															},
   911  
   912  															Params: atc.TaskEnv{
   913  																"FOO": "((BAR))",
   914  															},
   915  														},
   916  													},
   917  												},
   918  											},
   919  										},
   920  									},
   921  								}
   922  
   923  								request.Header.Set("Content-Type", "application/x-yaml")
   924  								request.Body = ioutil.NopCloser(bytes.NewBufferString(payload))
   925  							})
   926  
   927  							Context("when the check_creds param is set", func() {
   928  								BeforeEach(func() {
   929  									query := request.URL.Query()
   930  									query.Add(atc.SaveConfigCheckCreds, "")
   931  									request.URL.RawQuery = query.Encode()
   932  								})
   933  
   934  								Context("when the credential exists in the credential manager", func() {
   935  									BeforeEach(func() {
   936  										fakeSecretManager.GetReturns("this-string-value-doesn't-matter", nil, true, nil)
   937  									})
   938  
   939  									It("passes validation and saves it un-interpolated", func() {
   940  										Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
   941  
   942  										ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0)
   943  										Expect(ref.Name).To(Equal("a-pipeline"))
   944  										Expect(savedConfig).To(Equal(payloadAsConfig))
   945  										Expect(id).To(Equal(db.ConfigVersion(42)))
   946  										Expect(initiallyPaused).To(BeTrue())
   947  									})
   948  
   949  									It("returns 200", func() {
   950  										Expect(response.StatusCode).To(Equal(http.StatusOK))
   951  									})
   952  								})
   953  
   954  								Context("when the credential does not exist in the credential manager", func() {
   955  									BeforeEach(func() {
   956  										fakeSecretManager.GetReturns(nil, nil, false, nil) // nil value, nil expiration, not found, no error
   957  									})
   958  
   959  									It("returns 400", func() {
   960  										Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   961  									})
   962  
   963  									It("returns the credential name that was missing", func() {
   964  										Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"errors":["credential validation failed\n\n1 error occurred:\n\t* failed to interpolate task config: undefined vars: BAR\n\n"]}`))
   965  									})
   966  								})
   967  
   968  								Context("when a credentials manager is not used", func() {
   969  									BeforeEach(func() {
   970  										fakeSecretManager.GetStub = func(secretPath string) (interface{}, *time.Time, bool, error) {
   971  											return noop.Noop{}.Get(secretPath)
   972  										}
   973  									})
   974  
   975  									It("returns 400", func() {
   976  										Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
   977  									})
   978  
   979  									It("returns the credential name that was missing", func() {
   980  										Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"errors":["credential validation failed\n\n1 error occurred:\n\t* failed to interpolate task config: undefined vars: BAR\n\n"]}`))
   981  									})
   982  								})
   983  							})
   984  
   985  						})
   986  
   987  						Context("when it's the first time the pipeline has been created", func() {
   988  							BeforeEach(func() {
   989  								returnedPipeline := new(dbfakes.FakePipeline)
   990  								dbTeam.SavePipelineReturns(returnedPipeline, true, nil)
   991  							})
   992  
   993  							It("returns 201", func() {
   994  								Expect(response.StatusCode).To(Equal(http.StatusCreated))
   995  							})
   996  
   997  							It("does not notify the scanner to run", func() {
   998  								Expect(dbTeamFactory.NotifyResourceScannerCallCount()).To(Equal(0))
   999  							})
  1000  						})
  1001  
  1002  						Context("and saving it fails", func() {
  1003  							BeforeEach(func() {
  1004  								dbTeam.SavePipelineReturns(nil, false, errors.New("oh no!"))
  1005  							})
  1006  
  1007  							It("returns 500", func() {
  1008  								Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
  1009  							})
  1010  
  1011  							It("returns the error in the response body", func() {
  1012  								Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("failed to save config: oh no!")))
  1013  							})
  1014  						})
  1015  
  1016  						Context("when the config is invalid", func() {
  1017  							BeforeEach(func() {
  1018  								pipelineConfig.Groups[0].Resources = []string{"missing-resource"}
  1019  								payload, err := json.Marshal(pipelineConfig)
  1020  								Expect(err).NotTo(HaveOccurred())
  1021  								request.Body = gbytes.BufferWithBytes(payload)
  1022  							})
  1023  
  1024  							It("returns 400", func() {
  1025  								Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
  1026  							})
  1027  
  1028  							It("returns Content-Type 'application/json'", func() {
  1029  								expectedHeaderEntries := map[string]string{
  1030  									"Content-Type": "application/json",
  1031  								}
  1032  								Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
  1033  							})
  1034  
  1035  							It("returns error JSON", func() {
  1036  								Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
  1037  								{
  1038  									"errors": [
  1039  										"invalid groups:\n\tgroup 'some-group' has unknown resource 'missing-resource'\n"
  1040  									]
  1041  								}`))
  1042  							})
  1043  
  1044  							It("does not save it", func() {
  1045  								Expect(dbTeam.SavePipelineCallCount()).To(BeZero())
  1046  							})
  1047  						})
  1048  
  1049  						Context("when instance vars are specified", func() {
  1050  							Context("when instance vars are malformed", func() {
  1051  								BeforeEach(func() {
  1052  									query := request.URL.Query()
  1053  									query.Add("instance_vars", "{")
  1054  									request.URL.RawQuery = query.Encode()
  1055  								})
  1056  
  1057  								It("returns 400", func() {
  1058  									Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
  1059  								})
  1060  
  1061  								It("returns Content-Type 'application/json'", func() {
  1062  									expectedHeaderEntries := map[string]string{
  1063  										"Content-Type": "application/json",
  1064  									}
  1065  									Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
  1066  								})
  1067  
  1068  								It("returns an error in the response body", func() {
  1069  									Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
  1070  										{
  1071  											"errors": [
  1072  												"instance_vars is malformed: unexpected end of JSON input"
  1073  											]
  1074  										}`))
  1075  								})
  1076  
  1077  								It("does not save anything", func() {
  1078  									Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
  1079  								})
  1080  							})
  1081  
  1082  							Context("when instance vars is valid", func() {
  1083  								BeforeEach(func() {
  1084  									query := request.URL.Query()
  1085  									query.Add("instance_vars", "{\"branch\":\"feature\"}")
  1086  									request.URL.RawQuery = query.Encode()
  1087  								})
  1088  
  1089  								It("saves an instanced pipeline", func() {
  1090  									Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
  1091  
  1092  									ref, _, _, _ := dbTeam.SavePipelineArgsForCall(0)
  1093  									Expect(ref).To(Equal(atc.PipelineRef{
  1094  										Name:         "a-pipeline",
  1095  										InstanceVars: atc.InstanceVars{"branch": "feature"},
  1096  									}))
  1097  								})
  1098  							})
  1099  						})
  1100  					})
  1101  
  1102  					Context("there is a problem fetching the team", func() {
  1103  						BeforeEach(func() {
  1104  							request.Header.Set("Content-Type", "application/json")
  1105  
  1106  							payload, err := json.Marshal(pipelineConfig)
  1107  							Expect(err).NotTo(HaveOccurred())
  1108  
  1109  							request.Body = gbytes.BufferWithBytes(payload)
  1110  						})
  1111  
  1112  						Context("when the team is not found", func() {
  1113  							BeforeEach(func() {
  1114  								dbTeamFactory.FindTeamReturns(nil, false, nil)
  1115  							})
  1116  
  1117  							It("returns 404", func() {
  1118  								Expect(response.StatusCode).To(Equal(http.StatusNotFound))
  1119  							})
  1120  						})
  1121  
  1122  						Context("when finding the team fails", func() {
  1123  							BeforeEach(func() {
  1124  								dbTeamFactory.FindTeamReturns(nil, false, errors.New("failed"))
  1125  							})
  1126  
  1127  							It("returns 500", func() {
  1128  								Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
  1129  							})
  1130  						})
  1131  					})
  1132  
  1133  				})
  1134  
  1135  				Context("when the Content-Type is unsupported", func() {
  1136  					BeforeEach(func() {
  1137  						request.Header.Set("Content-Type", "application/x-toml")
  1138  
  1139  						payload, err := yaml.Marshal(pipelineConfig)
  1140  						Expect(err).NotTo(HaveOccurred())
  1141  
  1142  						request.Body = gbytes.BufferWithBytes(payload)
  1143  					})
  1144  
  1145  					It("returns Unsupported Media Type", func() {
  1146  						Expect(response.StatusCode).To(Equal(http.StatusUnsupportedMediaType))
  1147  					})
  1148  
  1149  					It("does not save it", func() {
  1150  						Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
  1151  					})
  1152  				})
  1153  
  1154  				Context("when the config contains extra keys at the toplevel", func() {
  1155  					BeforeEach(func() {
  1156  						request.Header.Set("Content-Type", "application/json")
  1157  
  1158  						remoraPayload, err := json.Marshal(map[string]interface{}{
  1159  							"extra": "noooooo",
  1160  
  1161  							"meta": map[string]interface{}{
  1162  								"whoa": "lol",
  1163  							},
  1164  
  1165  							"jobs": []map[string]interface{}{
  1166  								{
  1167  									"name":   "some-job",
  1168  									"public": true,
  1169  									"plan":   []atc.Step{},
  1170  								},
  1171  							},
  1172  						})
  1173  						Expect(err).NotTo(HaveOccurred())
  1174  
  1175  						request.Body = gbytes.BufferWithBytes(remoraPayload)
  1176  					})
  1177  
  1178  					It("returns 200", func() {
  1179  						Expect(response.StatusCode).To(Equal(http.StatusOK))
  1180  					})
  1181  
  1182  					It("returns Content-Type 'application/json'", func() {
  1183  						expectedHeaderEntries := map[string]string{
  1184  							"Content-Type": "application/json",
  1185  						}
  1186  						Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
  1187  					})
  1188  
  1189  					It("saves it", func() {
  1190  						Expect(dbTeam.SavePipelineCallCount()).To(Equal(1))
  1191  
  1192  						ref, savedConfig, id, initiallyPaused := dbTeam.SavePipelineArgsForCall(0)
  1193  						Expect(ref.Name).To(Equal("a-pipeline"))
  1194  						Expect(savedConfig).To(Equal(atc.Config{
  1195  							Jobs: atc.JobConfigs{
  1196  								{
  1197  									Name:         "some-job",
  1198  									Public:       true,
  1199  									PlanSequence: []atc.Step{},
  1200  								},
  1201  							},
  1202  						}))
  1203  						Expect(id).To(Equal(db.ConfigVersion(42)))
  1204  						Expect(initiallyPaused).To(BeTrue())
  1205  					})
  1206  				})
  1207  
  1208  				Context("when the config contains extra keys nested under a valid key", func() {
  1209  					BeforeEach(func() {
  1210  						request.Header.Set("Content-Type", "application/json")
  1211  
  1212  						remoraPayload, err := json.Marshal(map[string]interface{}{
  1213  							"extra": "noooooo",
  1214  
  1215  							"jobs": []map[string]interface{}{
  1216  								{
  1217  									"name":  "some-job",
  1218  									"pubic": true,
  1219  									"plan":  []atc.Step{},
  1220  								},
  1221  							},
  1222  						})
  1223  						Expect(err).NotTo(HaveOccurred())
  1224  
  1225  						request.Body = gbytes.BufferWithBytes(remoraPayload)
  1226  					})
  1227  
  1228  					It("returns 400", func() {
  1229  						Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
  1230  					})
  1231  
  1232  					It("returns Content-Type 'application/json'", func() {
  1233  						expectedHeaderEntries := map[string]string{
  1234  							"Content-Type": "application/json",
  1235  						}
  1236  						Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
  1237  					})
  1238  
  1239  					It("returns an error in the response body", func() {
  1240  						Expect(ioutil.ReadAll(response.Body)).To(ContainSubstring(`malformed config: error unmarshaling JSON: while decoding JSON: json: unknown field \"pubic\"`))
  1241  					})
  1242  
  1243  					It("does not save it", func() {
  1244  						Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
  1245  					})
  1246  				})
  1247  			})
  1248  
  1249  			Context("when a config version is malformed", func() {
  1250  				BeforeEach(func() {
  1251  					request.Header.Set(atc.ConfigVersionHeader, "forty-two")
  1252  				})
  1253  
  1254  				It("returns 400", func() {
  1255  					Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
  1256  				})
  1257  
  1258  				It("returns Content-Type 'application/json'", func() {
  1259  					expectedHeaderEntries := map[string]string{
  1260  						"Content-Type": "application/json",
  1261  					}
  1262  					Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
  1263  				})
  1264  
  1265  				It("returns an error in the response body", func() {
  1266  					Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`
  1267  							{
  1268  								"errors": [
  1269  									"config version is malformed: expected integer"
  1270  								]
  1271  							}`))
  1272  				})
  1273  
  1274  				It("does not save it", func() {
  1275  					Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
  1276  				})
  1277  			})
  1278  		})
  1279  
  1280  		Context("when not authenticated", func() {
  1281  			BeforeEach(func() {
  1282  				fakeAccess.IsAuthenticatedReturns(false)
  1283  			})
  1284  
  1285  			It("returns 401", func() {
  1286  				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
  1287  			})
  1288  
  1289  			It("does not save the config", func() {
  1290  				Expect(dbTeam.SavePipelineCallCount()).To(Equal(0))
  1291  			})
  1292  		})
  1293  	})
  1294  })