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

     1  package configvalidate_test
     2  
     3  import (
     4  	"encoding/json"
     5  	"strings"
     6  
     7  	"github.com/pf-qiu/concourse/v6/atc"
     8  	"github.com/pf-qiu/concourse/v6/atc/configvalidate"
     9  
    10  	// load dummy credential manager
    11  	_ "github.com/pf-qiu/concourse/v6/atc/creds/dummy"
    12  
    13  	. "github.com/onsi/ginkgo"
    14  	. "github.com/onsi/gomega"
    15  )
    16  
    17  var _ = Describe("ValidateConfig", func() {
    18  	var (
    19  		config        atc.Config
    20  		warnings      []atc.ConfigWarning
    21  		errorMessages []string
    22  	)
    23  
    24  	BeforeEach(func() {
    25  		config = atc.Config{
    26  			Groups: atc.GroupConfigs{
    27  				{
    28  					Name:      "some-group",
    29  					Jobs:      []string{"some-job"},
    30  					Resources: []string{"some-resource"},
    31  				},
    32  				{
    33  					Name: "some-other-group",
    34  					Jobs: []string{"some-empty-*"},
    35  				},
    36  			},
    37  
    38  			VarSources: atc.VarSourceConfigs{},
    39  
    40  			Resources: atc.ResourceConfigs{
    41  				{
    42  					Name: "some-resource",
    43  					Type: "some-type",
    44  					Source: atc.Source{
    45  						"source-config": "some-value",
    46  					},
    47  				},
    48  			},
    49  
    50  			ResourceTypes: atc.ResourceTypes{
    51  				{
    52  					Name: "some-resource-type",
    53  					Type: "some-type",
    54  					Source: atc.Source{
    55  						"source-config": "some-value",
    56  					},
    57  				},
    58  			},
    59  
    60  			Jobs: atc.JobConfigs{
    61  				{
    62  					Name:   "some-job",
    63  					Public: true,
    64  					Serial: true,
    65  					PlanSequence: []atc.Step{
    66  						{
    67  							Config: &atc.GetStep{
    68  								Name:     "some-input",
    69  								Resource: "some-resource",
    70  								Params: atc.Params{
    71  									"some-param": "some-value",
    72  								},
    73  							},
    74  						},
    75  						{
    76  							Config: &atc.LoadVarStep{
    77  								Name: "some-var",
    78  								File: "some-input/some-file.json",
    79  							},
    80  						},
    81  						{
    82  							Config: &atc.TaskStep{
    83  								Name:       "some-task",
    84  								Privileged: true,
    85  								ConfigPath: "some/config/path.yml",
    86  							},
    87  						},
    88  						{
    89  							Config: &atc.PutStep{
    90  								Name: "some-resource",
    91  								Params: atc.Params{
    92  									"some-param": "some-value",
    93  								},
    94  							},
    95  						},
    96  						{
    97  							Config: &atc.SetPipelineStep{
    98  								Name: "some-pipeline",
    99  								File: "some-file",
   100  							},
   101  						},
   102  					},
   103  				},
   104  				{
   105  					Name: "some-empty-job",
   106  				},
   107  			},
   108  		}
   109  
   110  		atc.EnableAcrossStep = true
   111  	})
   112  
   113  	JustBeforeEach(func() {
   114  		warnings, errorMessages = configvalidate.Validate(config)
   115  	})
   116  
   117  	Context("when the config is valid", func() {
   118  		It("returns no error", func() {
   119  			Expect(errorMessages).To(HaveLen(0))
   120  		})
   121  	})
   122  
   123  	Describe("invalid identifiers", func() {
   124  
   125  		Context("when a group has an invalid identifier", func() {
   126  			BeforeEach(func() {
   127  				config.Groups = append(config.Groups, atc.GroupConfig{
   128  					Name: "_some-group",
   129  					Jobs: []string{"some-job"},
   130  				})
   131  			})
   132  
   133  			It("returns a warning", func() {
   134  				Expect(warnings).To(HaveLen(1))
   135  				Expect(warnings[0].Message).To(ContainSubstring("'_some-group' is not a valid identifier"))
   136  			})
   137  		})
   138  
   139  		Context("when a resource has an invalid identifier", func() {
   140  			BeforeEach(func() {
   141  				config.Resources = append(config.Resources, atc.ResourceConfig{
   142  					Name: "some_resource",
   143  					Type: "some-type",
   144  					Source: atc.Source{
   145  						"source-config": "some-value",
   146  					},
   147  				})
   148  			})
   149  
   150  			It("returns a warning", func() {
   151  				Expect(warnings).To(HaveLen(1))
   152  				Expect(warnings[0].Message).To(ContainSubstring("'some_resource' is not a valid identifier"))
   153  			})
   154  		})
   155  
   156  		Context("when a resource type has an invalid identifier", func() {
   157  			BeforeEach(func() {
   158  				config.ResourceTypes = append(config.ResourceTypes, atc.ResourceType{
   159  					Name: "_some-resource-type",
   160  					Type: "some-type",
   161  					Source: atc.Source{
   162  						"source-config": "some-value",
   163  					},
   164  				})
   165  			})
   166  
   167  			It("returns a warning", func() {
   168  				Expect(warnings).To(HaveLen(1))
   169  				Expect(warnings[0].Message).To(ContainSubstring("'_some-resource-type' is not a valid identifier"))
   170  			})
   171  		})
   172  
   173  		Context("when a var source has an invalid identifier", func() {
   174  			BeforeEach(func() {
   175  				config.VarSources = append(config.VarSources, atc.VarSourceConfig{
   176  					Name:   "_some-var-source",
   177  					Type:   "dummy",
   178  					Config: "",
   179  				})
   180  			})
   181  
   182  			It("returns a warning", func() {
   183  				Expect(warnings).To(HaveLen(1))
   184  				Expect(warnings[0].Message).To(ContainSubstring("'_some-var-source' is not a valid identifier"))
   185  			})
   186  		})
   187  
   188  		Context("when a job has an invalid identifier", func() {
   189  			BeforeEach(func() {
   190  				config.Jobs = append(config.Jobs, atc.JobConfig{
   191  					Name: "_some-job",
   192  				})
   193  			})
   194  
   195  			It("returns a warning", func() {
   196  				Expect(warnings).To(HaveLen(1))
   197  				Expect(warnings[0].Message).To(ContainSubstring("'_some-job' is not a valid identifier"))
   198  			})
   199  		})
   200  
   201  		Context("when a step has an invalid identifier", func() {
   202  			var job atc.JobConfig
   203  
   204  			BeforeEach(func() {
   205  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   206  					Config: &atc.GetStep{
   207  						Name: "_get-step",
   208  					},
   209  				})
   210  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   211  					Config: &atc.TaskStep{
   212  						Name: "_task-step",
   213  					},
   214  				})
   215  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   216  					Config: &atc.PutStep{
   217  						Name: "_put-step",
   218  					},
   219  				})
   220  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   221  					Config: &atc.SetPipelineStep{
   222  						Name: "_set-pipeline-step",
   223  					},
   224  				})
   225  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   226  					Config: &atc.LoadVarStep{
   227  						Name: "_load-var-step",
   228  					},
   229  				})
   230  
   231  				config.Jobs = append(config.Jobs, job)
   232  			})
   233  
   234  			It("returns a warning", func() {
   235  				Expect(warnings).To(HaveLen(5))
   236  				Expect(warnings[0].Message).To(ContainSubstring("'_get-step' is not a valid identifier"))
   237  				Expect(warnings[1].Message).To(ContainSubstring("'_task-step' is not a valid identifier"))
   238  				Expect(warnings[2].Message).To(ContainSubstring("'_put-step' is not a valid identifier"))
   239  				Expect(warnings[3].Message).To(ContainSubstring("'_set-pipeline-step' is not a valid identifier"))
   240  				Expect(warnings[4].Message).To(ContainSubstring("'_load-var-step' is not a valid identifier"))
   241  			})
   242  		})
   243  	})
   244  
   245  	Describe("invalid groups", func() {
   246  		Context("when the groups reference a bogus resource", func() {
   247  			BeforeEach(func() {
   248  				config.Groups = append(config.Groups, atc.GroupConfig{
   249  					Name:      "bogus",
   250  					Resources: []string{"bogus-resource"},
   251  				})
   252  			})
   253  
   254  			It("returns an error", func() {
   255  				Expect(errorMessages).To(HaveLen(1))
   256  				Expect(errorMessages[0]).To(ContainSubstring("invalid groups:"))
   257  				Expect(errorMessages[0]).To(ContainSubstring("unknown resource 'bogus-resource'"))
   258  			})
   259  		})
   260  
   261  		Context("when the groups reference a bogus job", func() {
   262  			BeforeEach(func() {
   263  				config.Groups = append(config.Groups, atc.GroupConfig{
   264  					Name: "bogus",
   265  					Jobs: []string{"bogus-*"},
   266  				})
   267  			})
   268  
   269  			It("returns an error", func() {
   270  				Expect(errorMessages).To(HaveLen(1))
   271  				Expect(errorMessages[0]).To(ContainSubstring("invalid groups:"))
   272  				Expect(errorMessages[0]).To(ContainSubstring("no jobs match 'bogus-*' for group 'bogus'"))
   273  			})
   274  		})
   275  
   276  		Context("when there are jobs excluded from groups", func() {
   277  			BeforeEach(func() {
   278  				config.Jobs = append(config.Jobs, atc.JobConfig{
   279  					Name: "stand-alone-job",
   280  				})
   281  				config.Jobs = append(config.Jobs, atc.JobConfig{
   282  					Name: "other-stand-alone-job",
   283  				})
   284  			})
   285  
   286  			It("returns an error", func() {
   287  				Expect(errorMessages).To(HaveLen(1))
   288  				Expect(errorMessages[0]).To(ContainSubstring("invalid groups:"))
   289  				Expect(errorMessages[0]).To(ContainSubstring("job 'stand-alone-job' belongs to no group"))
   290  				Expect(errorMessages[0]).To(ContainSubstring("job 'other-stand-alone-job' belongs to no group"))
   291  			})
   292  
   293  		})
   294  
   295  		Context("when the groups have two duplicate names", func() {
   296  			BeforeEach(func() {
   297  				config.Groups = append(config.Groups, atc.GroupConfig{
   298  					Name: "some-group",
   299  				})
   300  			})
   301  
   302  			It("returns an error", func() {
   303  				Expect(errorMessages).To(HaveLen(1))
   304  				Expect(errorMessages[0]).To(ContainSubstring("invalid groups:"))
   305  				Expect(errorMessages[0]).To(ContainSubstring("'some-group' appears 2 times. Duplicate names are not allowed."))
   306  			})
   307  		})
   308  
   309  		Context("when the groups have four duplicate names", func() {
   310  			BeforeEach(func() {
   311  				config.Groups = append(config.Groups, atc.GroupConfig{
   312  					Name: "some-group",
   313  				}, atc.GroupConfig{
   314  					Name: "some-group",
   315  				}, atc.GroupConfig{
   316  					Name: "some-group",
   317  				})
   318  			})
   319  
   320  			It("returns an error", func() {
   321  				Expect(errorMessages).To(HaveLen(1))
   322  				errorMessage := strings.Trim(errorMessages[0], "\n")
   323  				errorLines := strings.Split(errorMessage, "\n")
   324  				Expect(errorLines).To(HaveLen(2))
   325  				Expect(errorLines[0]).To(ContainSubstring("invalid groups:"))
   326  				Expect(errorLines[1]).To(ContainSubstring("group 'some-group' appears 4 times. Duplicate names are not allowed."))
   327  			})
   328  		})
   329  
   330  		Context("when a group has and invalid glob expression", func() {
   331  			BeforeEach(func() {
   332  				config.Groups = append(config.Groups, atc.GroupConfig{
   333  					Name: "a-group",
   334  					Jobs: []string{"some-bad-glob-[0-9"},
   335  				})
   336  			})
   337  
   338  			It("returns an error", func() {
   339  				Expect(errorMessages).To(HaveLen(1))
   340  				Expect(errorMessages[0]).To(ContainSubstring("invalid groups:"))
   341  				Expect(errorMessages[0]).To(ContainSubstring("invalid glob expression 'some-bad-glob-[0-9' for group 'a-group'"))
   342  			})
   343  		})
   344  	})
   345  
   346  	Describe("invalid var sources", func() {
   347  		Context("when a var source type is invalid", func() {
   348  			BeforeEach(func() {
   349  				config.VarSources = append(config.VarSources, atc.VarSourceConfig{
   350  					Name:   "some",
   351  					Type:   "some",
   352  					Config: "",
   353  				})
   354  			})
   355  
   356  			It("returns an error", func() {
   357  				Expect(errorMessages).To(HaveLen(1))
   358  				Expect(errorMessages[0]).To(ContainSubstring("unknown credential manager type: some"))
   359  			})
   360  		})
   361  
   362  		Context("when config is invalid", func() {
   363  			BeforeEach(func() {
   364  				config.VarSources = append(config.VarSources, atc.VarSourceConfig{
   365  					Name:   "some",
   366  					Type:   "dummy",
   367  					Config: "",
   368  				})
   369  			})
   370  
   371  			It("returns an error", func() {
   372  				Expect(errorMessages).To(HaveLen(1))
   373  				Expect(errorMessages[0]).To(ContainSubstring("failed to create credential manager some: invalid dummy credential manager config"))
   374  			})
   375  		})
   376  
   377  		Context("when duplicate var source names", func() {
   378  			BeforeEach(func() {
   379  				config.VarSources = append(config.VarSources,
   380  					atc.VarSourceConfig{
   381  						Name: "some",
   382  						Type: "dummy",
   383  						Config: map[string]interface{}{
   384  							"vars": map[string]interface{}{"k2": "v2"},
   385  						},
   386  					},
   387  					atc.VarSourceConfig{
   388  						Name: "some",
   389  						Type: "dummy",
   390  						Config: map[string]interface{}{
   391  							"vars": map[string]interface{}{"k2": "v2"},
   392  						},
   393  					},
   394  				)
   395  			})
   396  
   397  			It("returns an error", func() {
   398  				Expect(errorMessages).To(HaveLen(1))
   399  				Expect(errorMessages[0]).To(ContainSubstring("duplicate var_source name: some"))
   400  			})
   401  		})
   402  
   403  		Context("when var source's dependency cannot be resolved", func() {
   404  			BeforeEach(func() {
   405  				config.VarSources = append(config.VarSources,
   406  					atc.VarSourceConfig{
   407  						Name: "s1",
   408  						Type: "dummy",
   409  						Config: map[string]interface{}{
   410  							"vars": map[string]interface{}{"k": "v"},
   411  						},
   412  					},
   413  					atc.VarSourceConfig{
   414  						Name: "s2",
   415  						Type: "dummy",
   416  						Config: map[string]interface{}{
   417  							"vars": map[string]interface{}{"k": "((s1:k))"},
   418  						},
   419  					},
   420  					atc.VarSourceConfig{
   421  						Name: "s3",
   422  						Type: "dummy",
   423  						Config: map[string]interface{}{
   424  							"vars": map[string]interface{}{"k": "((none:k))"},
   425  						},
   426  					},
   427  				)
   428  			})
   429  
   430  			It("returns an error", func() {
   431  				Expect(errorMessages).To(HaveLen(1))
   432  				Expect(errorMessages[0]).To(ContainSubstring("could not resolve inter-dependent var sources: s3"))
   433  			})
   434  		})
   435  
   436  		Context("when var source names are circular", func() {
   437  			BeforeEach(func() {
   438  				config.VarSources = append(config.VarSources,
   439  					atc.VarSourceConfig{
   440  						Name: "s1",
   441  						Type: "dummy",
   442  						Config: map[string]interface{}{
   443  							"vars": map[string]interface{}{"k": "((s3:v))"},
   444  						},
   445  					},
   446  					atc.VarSourceConfig{
   447  						Name: "s2",
   448  						Type: "dummy",
   449  						Config: map[string]interface{}{
   450  							"vars": map[string]interface{}{"k": "((s1:k))"},
   451  						},
   452  					},
   453  					atc.VarSourceConfig{
   454  						Name: "s3",
   455  						Type: "dummy",
   456  						Config: map[string]interface{}{
   457  							"vars": map[string]interface{}{"k": "((s2:k))"},
   458  						},
   459  					},
   460  				)
   461  			})
   462  
   463  			It("returns an error", func() {
   464  				Expect(errorMessages).To(HaveLen(1))
   465  				Expect(errorMessages[0]).To(ContainSubstring("could not resolve inter-dependent var sources: s1, s2, s3"))
   466  			})
   467  		})
   468  	})
   469  
   470  	Describe("invalid resources", func() {
   471  		Context("when a resource has no name", func() {
   472  			BeforeEach(func() {
   473  				config.Resources = append(config.Resources, atc.ResourceConfig{
   474  					Name: "",
   475  				})
   476  			})
   477  
   478  			It("returns an error", func() {
   479  				Expect(errorMessages).To(HaveLen(1))
   480  				Expect(errorMessages[0]).To(ContainSubstring("invalid resources:"))
   481  				Expect(errorMessages[0]).To(ContainSubstring("resources[1] has no name"))
   482  			})
   483  		})
   484  
   485  		Context("when a resource has no type", func() {
   486  			BeforeEach(func() {
   487  				config.Resources = append(config.Resources, atc.ResourceConfig{
   488  					Name: "bogus-resource",
   489  					Type: "",
   490  				})
   491  			})
   492  
   493  			It("returns an error", func() {
   494  				Expect(errorMessages).To(HaveLen(1))
   495  				Expect(errorMessages[0]).To(ContainSubstring("invalid resources:"))
   496  				Expect(errorMessages[0]).To(ContainSubstring("resources.bogus-resource has no type"))
   497  			})
   498  		})
   499  
   500  		Context("when a resource has no name or type", func() {
   501  			BeforeEach(func() {
   502  				config.Resources = append(config.Resources, atc.ResourceConfig{
   503  					Name: "",
   504  					Type: "",
   505  				})
   506  			})
   507  
   508  			It("returns an error describing both errors", func() {
   509  				Expect(errorMessages).To(HaveLen(1))
   510  				Expect(errorMessages[0]).To(ContainSubstring("invalid resources:"))
   511  				Expect(errorMessages[0]).To(ContainSubstring("resources[1] has no name"))
   512  				Expect(errorMessages[0]).To(ContainSubstring("resources[1] has no type"))
   513  			})
   514  		})
   515  
   516  		Context("when two resources have the same name", func() {
   517  			BeforeEach(func() {
   518  				config.Resources = append(config.Resources, config.Resources...)
   519  			})
   520  
   521  			It("returns an error", func() {
   522  				Expect(errorMessages).To(HaveLen(1))
   523  				Expect(errorMessages[0]).To(ContainSubstring("invalid resources:"))
   524  				Expect(errorMessages[0]).To(ContainSubstring(
   525  					"resources[0] and resources[1] have the same name ('some-resource')",
   526  				))
   527  			})
   528  		})
   529  	})
   530  
   531  	Describe("unused resources", func() {
   532  		BeforeEach(func() {
   533  			config = atc.Config{
   534  				Resources: atc.ResourceConfigs{
   535  					{
   536  						Name: "unused-resource",
   537  						Type: "some-type",
   538  					},
   539  					{
   540  						Name: "get",
   541  						Type: "some-type",
   542  					},
   543  					{
   544  						Name: "get-alias",
   545  						Type: "some-type",
   546  					},
   547  					{
   548  						Name: "resource",
   549  						Type: "some-type",
   550  					},
   551  					{
   552  						Name: "put",
   553  						Type: "some-type",
   554  					},
   555  					{
   556  						Name: "put-alias",
   557  						Type: "some-type",
   558  					},
   559  					{
   560  						Name: "do",
   561  						Type: "some-type",
   562  					},
   563  					{
   564  						Name: "aggregate",
   565  						Type: "some-type",
   566  					},
   567  					{
   568  						Name: "parallel",
   569  						Type: "some-type",
   570  					},
   571  					{
   572  						Name: "abort",
   573  						Type: "some-type",
   574  					},
   575  					{
   576  						Name: "error",
   577  						Type: "some-type",
   578  					},
   579  					{
   580  						Name: "failure",
   581  						Type: "some-type",
   582  					},
   583  					{
   584  						Name: "ensure",
   585  						Type: "some-type",
   586  					},
   587  					{
   588  						Name: "success",
   589  						Type: "some-type",
   590  					},
   591  					{
   592  						Name: "try",
   593  						Type: "some-type",
   594  					},
   595  					{
   596  						Name: "another-job",
   597  						Type: "some-type",
   598  					},
   599  				},
   600  
   601  				Jobs: atc.JobConfigs{
   602  					{
   603  						Name: "some-job",
   604  						PlanSequence: []atc.Step{
   605  							{
   606  								Config: &atc.GetStep{
   607  									Name: "get",
   608  								},
   609  							},
   610  							{
   611  								Config: &atc.GetStep{
   612  									Name:     "get-alias",
   613  									Resource: "resource",
   614  								},
   615  							},
   616  							{
   617  								Config: &atc.PutStep{
   618  									Name: "put",
   619  								},
   620  							},
   621  							{
   622  								Config: &atc.PutStep{
   623  									Name:     "put-alias",
   624  									Resource: "resource",
   625  								},
   626  							},
   627  							{
   628  								Config: &atc.DoStep{
   629  									Steps: []atc.Step{
   630  										{
   631  											Config: &atc.GetStep{
   632  												Name: "do",
   633  											},
   634  										},
   635  									},
   636  								},
   637  							},
   638  							{
   639  								Config: &atc.AggregateStep{
   640  									Steps: []atc.Step{
   641  										{
   642  											Config: &atc.GetStep{
   643  												Name: "aggregate",
   644  											},
   645  										},
   646  									},
   647  								},
   648  							},
   649  							{
   650  								Config: &atc.InParallelStep{
   651  									Config: atc.InParallelConfig{
   652  										Steps: []atc.Step{
   653  											{
   654  												Config: &atc.GetStep{
   655  													Name: "parallel",
   656  												},
   657  											},
   658  										},
   659  										Limit:    1,
   660  										FailFast: true,
   661  									},
   662  								},
   663  							},
   664  							{
   665  								Config: &atc.OnAbortStep{
   666  									Step: &atc.TaskStep{
   667  										Name:       "some-task",
   668  										ConfigPath: "some/config/path.yml",
   669  									},
   670  									Hook: atc.Step{
   671  										Config: &atc.GetStep{
   672  											Name: "abort",
   673  										},
   674  									},
   675  								},
   676  							},
   677  							{
   678  								Config: &atc.OnErrorStep{
   679  									Step: &atc.TaskStep{
   680  										Name:       "some-task",
   681  										ConfigPath: "some/config/path.yml",
   682  									},
   683  									Hook: atc.Step{
   684  										Config: &atc.GetStep{
   685  											Name: "error",
   686  										},
   687  									},
   688  								},
   689  							},
   690  							{
   691  								Config: &atc.OnFailureStep{
   692  									Step: &atc.TaskStep{
   693  										Name:       "some-task",
   694  										ConfigPath: "some/config/path.yml",
   695  									},
   696  									Hook: atc.Step{
   697  										Config: &atc.GetStep{
   698  											Name: "failure",
   699  										},
   700  									},
   701  								},
   702  							},
   703  							{
   704  								Config: &atc.OnSuccessStep{
   705  									Step: &atc.TaskStep{
   706  										Name:       "some-task",
   707  										ConfigPath: "some/config/path.yml",
   708  									},
   709  									Hook: atc.Step{
   710  										Config: &atc.GetStep{
   711  											Name: "success",
   712  										},
   713  									},
   714  								},
   715  							},
   716  							{
   717  								Config: &atc.EnsureStep{
   718  									Step: &atc.TaskStep{
   719  										Name:       "some-task",
   720  										ConfigPath: "some/config/path.yml",
   721  									},
   722  									Hook: atc.Step{
   723  										Config: &atc.GetStep{
   724  											Name: "ensure",
   725  										},
   726  									},
   727  								},
   728  							},
   729  							{
   730  								Config: &atc.TryStep{
   731  									Step: atc.Step{
   732  										Config: &atc.GetStep{
   733  											Name: "try",
   734  										},
   735  									},
   736  								},
   737  							},
   738  							{
   739  								Config: &atc.TaskStep{
   740  									Name:       "some-task",
   741  									ConfigPath: "some/config/path.yml",
   742  								},
   743  							},
   744  						},
   745  					},
   746  					{
   747  						Name: "another-job",
   748  						PlanSequence: []atc.Step{
   749  							{
   750  								Config: &atc.GetStep{
   751  									Name: "another-job",
   752  								},
   753  							},
   754  							{
   755  								Config: &atc.TaskStep{
   756  									Name:       "some-task",
   757  									ConfigPath: "some/config/path.yml",
   758  								},
   759  							},
   760  						},
   761  					},
   762  				},
   763  			}
   764  		})
   765  
   766  		Context("when a resource is not used in any jobs", func() {
   767  			It("returns an error", func() {
   768  				Expect(errorMessages).To(HaveLen(1))
   769  				Expect(errorMessages[0]).To(ContainSubstring("resource 'unused-resource' is not used"))
   770  				Expect(errorMessages[0]).To(ContainSubstring("resource 'get-alias' is not used"))
   771  				Expect(errorMessages[0]).To(ContainSubstring("resource 'put-alias' is not used"))
   772  			})
   773  		})
   774  	})
   775  
   776  	Describe("invalid resource types", func() {
   777  		Context("when a resource type has no name", func() {
   778  			BeforeEach(func() {
   779  				config.ResourceTypes = append(config.ResourceTypes, atc.ResourceType{
   780  					Name: "",
   781  				})
   782  			})
   783  
   784  			It("returns an error", func() {
   785  				Expect(errorMessages).To(HaveLen(1))
   786  				Expect(errorMessages[0]).To(ContainSubstring("invalid resource types:"))
   787  				Expect(errorMessages[0]).To(ContainSubstring("resource_types[1] has no name"))
   788  			})
   789  		})
   790  
   791  		Context("when a resource has no type", func() {
   792  			BeforeEach(func() {
   793  				config.ResourceTypes = append(config.ResourceTypes, atc.ResourceType{
   794  					Name: "bogus-resource-type",
   795  					Type: "",
   796  				})
   797  			})
   798  
   799  			It("returns an error", func() {
   800  				Expect(errorMessages).To(HaveLen(1))
   801  				Expect(errorMessages[0]).To(ContainSubstring("invalid resource types:"))
   802  				Expect(errorMessages[0]).To(ContainSubstring("resource_types.bogus-resource-type has no type"))
   803  			})
   804  		})
   805  
   806  		Context("when a resource has no name or type", func() {
   807  			BeforeEach(func() {
   808  				config.ResourceTypes = append(config.ResourceTypes, atc.ResourceType{
   809  					Name: "",
   810  					Type: "",
   811  				})
   812  			})
   813  
   814  			It("returns an error describing both errors", func() {
   815  				Expect(errorMessages).To(HaveLen(1))
   816  				Expect(errorMessages[0]).To(ContainSubstring("invalid resource types:"))
   817  				Expect(errorMessages[0]).To(ContainSubstring("resource_types[1] has no name"))
   818  				Expect(errorMessages[0]).To(ContainSubstring("resource_types[1] has no type"))
   819  			})
   820  		})
   821  
   822  		Context("when two resource types have the same name", func() {
   823  			BeforeEach(func() {
   824  				config.ResourceTypes = append(config.ResourceTypes, config.ResourceTypes...)
   825  			})
   826  
   827  			It("returns an error", func() {
   828  				Expect(errorMessages).To(HaveLen(1))
   829  				Expect(errorMessages[0]).To(ContainSubstring("invalid resource types:"))
   830  				Expect(errorMessages[0]).To(ContainSubstring("resource_types[0] and resource_types[1] have the same name ('some-resource-type')"))
   831  			})
   832  		})
   833  	})
   834  
   835  	Describe("validating a job", func() {
   836  		var job atc.JobConfig
   837  
   838  		BeforeEach(func() {
   839  			job = atc.JobConfig{
   840  				Name: "some-other-job",
   841  			}
   842  			config.Groups = []atc.GroupConfig{}
   843  		})
   844  
   845  		Context("when a job has no name", func() {
   846  			BeforeEach(func() {
   847  				job.Name = ""
   848  				config.Jobs = append(config.Jobs, job)
   849  			})
   850  
   851  			It("returns an error", func() {
   852  				Expect(errorMessages).To(HaveLen(1))
   853  				Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
   854  				Expect(errorMessages[0]).To(ContainSubstring("jobs[2] has no name"))
   855  			})
   856  		})
   857  
   858  		Context("when a job has a negative build_logs_to_retain", func() {
   859  			BeforeEach(func() {
   860  				job.BuildLogsToRetain = -1
   861  				config.Jobs = append(config.Jobs, job)
   862  			})
   863  
   864  			It("returns an error", func() {
   865  				Expect(errorMessages).To(HaveLen(1))
   866  				Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
   867  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job has negative build_logs_to_retain: -1"))
   868  			})
   869  		})
   870  
   871  		Context("when a job has duplicate inputs", func() {
   872  			BeforeEach(func() {
   873  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   874  					Config: &atc.GetStep{
   875  						Name: "some-resource",
   876  					},
   877  				})
   878  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   879  					Config: &atc.GetStep{
   880  						Name: "some-resource",
   881  					},
   882  				})
   883  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   884  					Config: &atc.GetStep{
   885  						Name: "some-resource",
   886  					},
   887  				})
   888  
   889  				config.Jobs = append(config.Jobs, job)
   890  			})
   891  
   892  			It("returns an error for each step", func() {
   893  				Expect(errorMessages).To(HaveLen(1))
   894  				Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
   895  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[1].get(some-resource): repeated name"))
   896  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[2].get(some-resource): repeated name"))
   897  			})
   898  		})
   899  
   900  		Context("when a job has duplicate inputs with different resources", func() {
   901  			BeforeEach(func() {
   902  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   903  					Config: &atc.GetStep{
   904  						Name:     "some-resource",
   905  						Resource: "a",
   906  					},
   907  				})
   908  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   909  					Config: &atc.GetStep{
   910  						Name:     "some-resource",
   911  						Resource: "b",
   912  					},
   913  				})
   914  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   915  					Config: &atc.GetStep{
   916  						Name:     "some-resource",
   917  						Resource: "c",
   918  					},
   919  				})
   920  
   921  				config.Jobs = append(config.Jobs, job)
   922  			})
   923  
   924  			It("returns an error for each step", func() {
   925  				Expect(errorMessages).To(HaveLen(1))
   926  				Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
   927  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[1].get(some-resource): repeated name"))
   928  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[2].get(some-resource): repeated name"))
   929  			})
   930  		})
   931  
   932  		Context("when a job gets the same resource multiple times but with different names", func() {
   933  			BeforeEach(func() {
   934  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   935  					Config: &atc.GetStep{
   936  						Name:     "a",
   937  						Resource: "some-resource",
   938  					},
   939  				})
   940  				job.PlanSequence = append(job.PlanSequence, atc.Step{
   941  					Config: &atc.GetStep{
   942  						Name:     "b",
   943  						Resource: "some-resource",
   944  					},
   945  				})
   946  
   947  				config.Jobs = append(config.Jobs, job)
   948  			})
   949  
   950  			It("returns no errors", func() {
   951  				Expect(errorMessages).To(HaveLen(0))
   952  			})
   953  		})
   954  
   955  		Describe("plans", func() {
   956  			Context("when a task plan has neither a config or a path set", func() {
   957  				BeforeEach(func() {
   958  					job.PlanSequence = append(job.PlanSequence, atc.Step{
   959  						Config: &atc.TaskStep{
   960  							Name: "lol",
   961  						},
   962  					})
   963  
   964  					config.Jobs = append(config.Jobs, job)
   965  				})
   966  
   967  				It("returns an error", func() {
   968  					Expect(errorMessages).To(HaveLen(1))
   969  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
   970  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].task(lol): must specify either `file:` or `config:`"))
   971  				})
   972  			})
   973  
   974  			Context("when a task plan has config path and config specified", func() {
   975  				BeforeEach(func() {
   976  					job.PlanSequence = append(job.PlanSequence, atc.Step{
   977  						Config: &atc.TaskStep{
   978  							Name:       "lol",
   979  							ConfigPath: "task.yml",
   980  							Config: &atc.TaskConfig{
   981  								Params: atc.TaskEnv{
   982  									"param1": "value1",
   983  								},
   984  							},
   985  						},
   986  					})
   987  
   988  					config.Jobs = append(config.Jobs, job)
   989  				})
   990  
   991  				It("returns an error", func() {
   992  					Expect(errorMessages).To(HaveLen(1))
   993  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
   994  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].task(lol): must specify one of `file:` or `config:`, not both"))
   995  				})
   996  			})
   997  
   998  			Context("when a task plan is invalid", func() {
   999  				BeforeEach(func() {
  1000  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1001  						Config: &atc.TaskStep{
  1002  							Name: "some-resource",
  1003  							Config: &atc.TaskConfig{
  1004  								Params: atc.TaskEnv{
  1005  									"param1": "value1",
  1006  								},
  1007  							},
  1008  						},
  1009  					})
  1010  
  1011  					config.Jobs = append(config.Jobs, job)
  1012  				})
  1013  
  1014  				It("returns an error", func() {
  1015  					Expect(errorMessages).To(HaveLen(1))
  1016  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1017  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].task(some-resource).config: missing 'platform'"))
  1018  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].task(some-resource).config: missing path to executable to run"))
  1019  				})
  1020  			})
  1021  
  1022  			Context("when a put plan has refers to a resource that does exist", func() {
  1023  				BeforeEach(func() {
  1024  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1025  						Config: &atc.PutStep{
  1026  							Name: "some-resource",
  1027  						},
  1028  					})
  1029  
  1030  					config.Jobs = append(config.Jobs, job)
  1031  				})
  1032  
  1033  				It("does not return an error", func() {
  1034  					Expect(errorMessages).To(HaveLen(0))
  1035  				})
  1036  			})
  1037  
  1038  			Context("when a get plan has refers to a resource that does not exist", func() {
  1039  				BeforeEach(func() {
  1040  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1041  						Config: &atc.GetStep{
  1042  							Name: "some-nonexistent-resource",
  1043  						},
  1044  					})
  1045  
  1046  					config.Jobs = append(config.Jobs, job)
  1047  				})
  1048  
  1049  				It("returns an error", func() {
  1050  					Expect(errorMessages).To(HaveLen(1))
  1051  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1052  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].get(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1053  				})
  1054  			})
  1055  
  1056  			Context("when a put plan has refers to a resource that does not exist", func() {
  1057  				BeforeEach(func() {
  1058  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1059  						Config: &atc.PutStep{
  1060  							Name: "some-nonexistent-resource",
  1061  						},
  1062  					})
  1063  
  1064  					config.Jobs = append(config.Jobs, job)
  1065  				})
  1066  
  1067  				It("returns an error", func() {
  1068  					Expect(errorMessages).To(HaveLen(1))
  1069  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1070  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].put(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1071  				})
  1072  			})
  1073  
  1074  			Context("when a get plan has a custom name but refers to a resource that does exist", func() {
  1075  				BeforeEach(func() {
  1076  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1077  						Config: &atc.GetStep{
  1078  							Name:     "custom-name",
  1079  							Resource: "some-resource",
  1080  						},
  1081  					})
  1082  
  1083  					config.Jobs = append(config.Jobs, job)
  1084  				})
  1085  
  1086  				It("does not return an error", func() {
  1087  					Expect(errorMessages).To(HaveLen(0))
  1088  				})
  1089  			})
  1090  
  1091  			Context("when a get plan has a custom name but refers to a resource that does not exist", func() {
  1092  				BeforeEach(func() {
  1093  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1094  						Config: &atc.GetStep{
  1095  							Name:     "custom-name",
  1096  							Resource: "some-missing-resource",
  1097  						},
  1098  					})
  1099  
  1100  					config.Jobs = append(config.Jobs, job)
  1101  				})
  1102  
  1103  				It("returns an error", func() {
  1104  					Expect(errorMessages).To(HaveLen(1))
  1105  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1106  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].get(custom-name): unknown resource 'some-missing-resource'"))
  1107  				})
  1108  			})
  1109  
  1110  			Context("when a put plan has a custom name but refers to a resource that does exist", func() {
  1111  				BeforeEach(func() {
  1112  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1113  						Config: &atc.PutStep{
  1114  							Name:     "custom-name",
  1115  							Resource: "some-resource",
  1116  						},
  1117  					})
  1118  
  1119  					config.Jobs = append(config.Jobs, job)
  1120  				})
  1121  
  1122  				It("does not return an error", func() {
  1123  					Expect(errorMessages).To(HaveLen(0))
  1124  				})
  1125  			})
  1126  
  1127  			Context("when a put plan has a custom name but refers to a resource that does not exist", func() {
  1128  				BeforeEach(func() {
  1129  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1130  						Config: &atc.PutStep{
  1131  							Name:     "custom-name",
  1132  							Resource: "some-missing-resource",
  1133  						},
  1134  					})
  1135  
  1136  					config.Jobs = append(config.Jobs, job)
  1137  				})
  1138  
  1139  				It("does return an error", func() {
  1140  					Expect(errorMessages).To(HaveLen(1))
  1141  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].put(custom-name): unknown resource 'some-missing-resource'"))
  1142  				})
  1143  			})
  1144  
  1145  			Context("when a job success hook refers to a resource that does exist", func() {
  1146  				BeforeEach(func() {
  1147  					job.OnSuccess = &atc.Step{
  1148  						Config: &atc.GetStep{
  1149  							Name: "some-resource",
  1150  						},
  1151  					}
  1152  
  1153  					config.Jobs = append(config.Jobs, job)
  1154  				})
  1155  
  1156  				It("does not return an error", func() {
  1157  					Expect(errorMessages).To(HaveLen(0))
  1158  				})
  1159  			})
  1160  
  1161  			Context("when a job success hook refers to a resource that does not exist", func() {
  1162  				BeforeEach(func() {
  1163  					job.OnSuccess = &atc.Step{
  1164  						Config: &atc.GetStep{
  1165  							Name: "some-nonexistent-resource",
  1166  						},
  1167  					}
  1168  
  1169  					config.Jobs = append(config.Jobs, job)
  1170  				})
  1171  
  1172  				It("returns an error", func() {
  1173  					Expect(errorMessages).To(HaveLen(1))
  1174  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1175  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.on_success.get(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1176  				})
  1177  			})
  1178  
  1179  			Context("when a job failure hook refers to a resource that does exist", func() {
  1180  				BeforeEach(func() {
  1181  					job.OnFailure = &atc.Step{
  1182  						Config: &atc.GetStep{
  1183  							Name: "some-resource",
  1184  						},
  1185  					}
  1186  
  1187  					config.Jobs = append(config.Jobs, job)
  1188  				})
  1189  
  1190  				It("does not return an error", func() {
  1191  					Expect(errorMessages).To(HaveLen(0))
  1192  				})
  1193  			})
  1194  
  1195  			Context("when a job failure hook refers to a resource that does not exist", func() {
  1196  				BeforeEach(func() {
  1197  					job.OnFailure = &atc.Step{
  1198  						Config: &atc.GetStep{
  1199  							Name: "some-nonexistent-resource",
  1200  						},
  1201  					}
  1202  
  1203  					config.Jobs = append(config.Jobs, job)
  1204  				})
  1205  
  1206  				It("returns an error", func() {
  1207  					Expect(errorMessages).To(HaveLen(1))
  1208  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1209  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.on_failure.get(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1210  				})
  1211  			})
  1212  
  1213  			Context("when a job error hook refers to a resource that does exist", func() {
  1214  				BeforeEach(func() {
  1215  					job.OnError = &atc.Step{
  1216  						Config: &atc.GetStep{
  1217  							Name: "some-resource",
  1218  						},
  1219  					}
  1220  
  1221  					config.Jobs = append(config.Jobs, job)
  1222  				})
  1223  
  1224  				It("does not return an error", func() {
  1225  					Expect(errorMessages).To(HaveLen(0))
  1226  				})
  1227  			})
  1228  
  1229  			Context("when a job ensure hook refers to a resource that does not exist", func() {
  1230  				BeforeEach(func() {
  1231  					job.OnError = &atc.Step{
  1232  						Config: &atc.GetStep{
  1233  							Name: "some-nonexistent-resource",
  1234  						},
  1235  					}
  1236  
  1237  					config.Jobs = append(config.Jobs, job)
  1238  				})
  1239  
  1240  				It("returns an error", func() {
  1241  					Expect(errorMessages).To(HaveLen(1))
  1242  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1243  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.on_error.get(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1244  				})
  1245  			})
  1246  
  1247  			Context("when a job abort hook refers to a resource that does exist", func() {
  1248  				BeforeEach(func() {
  1249  					job.OnAbort = &atc.Step{
  1250  						Config: &atc.GetStep{
  1251  							Name: "some-resource",
  1252  						},
  1253  					}
  1254  
  1255  					config.Jobs = append(config.Jobs, job)
  1256  				})
  1257  
  1258  				It("does not return an error", func() {
  1259  					Expect(errorMessages).To(HaveLen(0))
  1260  				})
  1261  			})
  1262  
  1263  			Context("when a job abort hook refers to a resource that does not exist", func() {
  1264  				BeforeEach(func() {
  1265  					job.OnAbort = &atc.Step{
  1266  						Config: &atc.GetStep{
  1267  							Name: "some-nonexistent-resource",
  1268  						},
  1269  					}
  1270  
  1271  					config.Jobs = append(config.Jobs, job)
  1272  				})
  1273  
  1274  				It("returns an error", func() {
  1275  					Expect(errorMessages).To(HaveLen(1))
  1276  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1277  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.on_abort.get(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1278  				})
  1279  			})
  1280  
  1281  			Context("when a job ensure hook refers to a resource that does exist", func() {
  1282  				BeforeEach(func() {
  1283  					job.Ensure = &atc.Step{
  1284  						Config: &atc.GetStep{
  1285  							Name: "some-resource",
  1286  						},
  1287  					}
  1288  
  1289  					config.Jobs = append(config.Jobs, job)
  1290  				})
  1291  
  1292  				It("does not return an error", func() {
  1293  					Expect(errorMessages).To(HaveLen(0))
  1294  				})
  1295  			})
  1296  
  1297  			Context("when a job ensure hook refers to a resource that does not exist", func() {
  1298  				BeforeEach(func() {
  1299  					job.Ensure = &atc.Step{
  1300  						Config: &atc.GetStep{
  1301  							Name: "some-nonexistent-resource",
  1302  						},
  1303  					}
  1304  
  1305  					config.Jobs = append(config.Jobs, job)
  1306  				})
  1307  
  1308  				It("returns an error", func() {
  1309  					Expect(errorMessages).To(HaveLen(1))
  1310  					Expect(errorMessages[0]).To(ContainSubstring("invalid jobs:"))
  1311  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.ensure.get(some-nonexistent-resource): unknown resource 'some-nonexistent-resource'"))
  1312  				})
  1313  			})
  1314  
  1315  			Context("when a get plan refers to a 'put' resource that exists in another job's hook", func() {
  1316  				var (
  1317  					job1 atc.JobConfig
  1318  					job2 atc.JobConfig
  1319  				)
  1320  				BeforeEach(func() {
  1321  					job1 = atc.JobConfig{
  1322  						Name: "job-one",
  1323  					}
  1324  					job2 = atc.JobConfig{
  1325  						Name: "job-two",
  1326  					}
  1327  
  1328  					job1.PlanSequence = append(job1.PlanSequence, atc.Step{
  1329  						Config: &atc.OnSuccessStep{
  1330  							Step: &atc.TaskStep{
  1331  								Name:       "job-one",
  1332  								ConfigPath: "job-one-config-path",
  1333  							},
  1334  							Hook: atc.Step{
  1335  								Config: &atc.PutStep{
  1336  									Name: "some-resource",
  1337  								},
  1338  							},
  1339  						},
  1340  					})
  1341  
  1342  					job2.PlanSequence = append(job2.PlanSequence, atc.Step{
  1343  						Config: &atc.GetStep{
  1344  							Name:   "some-resource",
  1345  							Passed: []string{"job-one"},
  1346  						},
  1347  					})
  1348  					config.Jobs = append(config.Jobs, job1, job2)
  1349  				})
  1350  
  1351  				It("does not return an error", func() {
  1352  					Expect(errorMessages).To(HaveLen(0))
  1353  				})
  1354  			})
  1355  
  1356  			Context("when a get plan refers to a 'get' resource that exists in another job's hook", func() {
  1357  				var (
  1358  					job1 atc.JobConfig
  1359  					job2 atc.JobConfig
  1360  				)
  1361  				BeforeEach(func() {
  1362  					job1 = atc.JobConfig{
  1363  						Name: "job-one",
  1364  					}
  1365  					job2 = atc.JobConfig{
  1366  						Name: "job-two",
  1367  					}
  1368  
  1369  					job1.PlanSequence = append(job1.PlanSequence, atc.Step{
  1370  						Config: &atc.OnSuccessStep{
  1371  							Step: &atc.TaskStep{
  1372  								Name:       "job-one",
  1373  								ConfigPath: "job-one-config-path",
  1374  							},
  1375  							Hook: atc.Step{
  1376  								Config: &atc.GetStep{
  1377  									Name: "some-resource",
  1378  								},
  1379  							},
  1380  						},
  1381  					})
  1382  
  1383  					job2.PlanSequence = append(job2.PlanSequence, atc.Step{
  1384  						Config: &atc.GetStep{
  1385  							Name:   "some-resource",
  1386  							Passed: []string{"job-one"},
  1387  						},
  1388  					})
  1389  					config.Jobs = append(config.Jobs, job1, job2)
  1390  				})
  1391  
  1392  				It("does not return an error", func() {
  1393  					Expect(errorMessages).To(HaveLen(0))
  1394  				})
  1395  			})
  1396  
  1397  			Context("when a get plan refers to a 'put' resource that exists in another job's try-step", func() {
  1398  				var (
  1399  					job1 atc.JobConfig
  1400  					job2 atc.JobConfig
  1401  				)
  1402  				BeforeEach(func() {
  1403  					job1 = atc.JobConfig{
  1404  						Name: "job-one",
  1405  					}
  1406  					job2 = atc.JobConfig{
  1407  						Name: "job-two",
  1408  					}
  1409  
  1410  					job1.PlanSequence = append(job1.PlanSequence, atc.Step{
  1411  						Config: &atc.TryStep{
  1412  							Step: atc.Step{
  1413  								Config: &atc.PutStep{
  1414  									Name: "some-resource",
  1415  								},
  1416  							},
  1417  						},
  1418  					})
  1419  
  1420  					job2.PlanSequence = append(job2.PlanSequence, atc.Step{
  1421  						Config: &atc.GetStep{
  1422  							Name:   "some-resource",
  1423  							Passed: []string{"job-one"},
  1424  						},
  1425  					})
  1426  					config.Jobs = append(config.Jobs, job1, job2)
  1427  
  1428  				})
  1429  
  1430  				It("does not return an error", func() {
  1431  					Expect(errorMessages).To(HaveLen(0))
  1432  				})
  1433  			})
  1434  
  1435  			Context("when a get plan refers to a 'get' resource that exists in another job's try-step", func() {
  1436  				var (
  1437  					job1 atc.JobConfig
  1438  					job2 atc.JobConfig
  1439  				)
  1440  				BeforeEach(func() {
  1441  					job1 = atc.JobConfig{
  1442  						Name: "job-one",
  1443  					}
  1444  					job2 = atc.JobConfig{
  1445  						Name: "job-two",
  1446  					}
  1447  
  1448  					job1.PlanSequence = append(job1.PlanSequence, atc.Step{
  1449  						Config: &atc.TryStep{
  1450  							Step: atc.Step{
  1451  								Config: &atc.GetStep{
  1452  									Name: "some-resource",
  1453  								},
  1454  							},
  1455  						},
  1456  					})
  1457  
  1458  					job2.PlanSequence = append(job2.PlanSequence, atc.Step{
  1459  						Config: &atc.GetStep{
  1460  							Name:   "some-resource",
  1461  							Passed: []string{"job-one"},
  1462  						},
  1463  					})
  1464  					config.Jobs = append(config.Jobs, job1, job2)
  1465  
  1466  				})
  1467  
  1468  				It("does not return an error", func() {
  1469  					Expect(errorMessages).To(HaveLen(0))
  1470  				})
  1471  			})
  1472  
  1473  			Context("when a plan has an invalid step within an abort", func() {
  1474  				BeforeEach(func() {
  1475  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1476  						Config: &atc.OnAbortStep{
  1477  							Step: &atc.GetStep{
  1478  								Name: "some-resource",
  1479  							},
  1480  							Hook: atc.Step{
  1481  								Config: &atc.PutStep{
  1482  									Name:     "custom-name",
  1483  									Resource: "some-missing-resource",
  1484  								},
  1485  							},
  1486  						},
  1487  					})
  1488  
  1489  					config.Jobs = append(config.Jobs, job)
  1490  				})
  1491  
  1492  				It("throws a validation error", func() {
  1493  					Expect(errorMessages).To(HaveLen(1))
  1494  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].on_abort.put(custom-name): unknown resource 'some-missing-resource'"))
  1495  				})
  1496  			})
  1497  
  1498  			Context("when a plan has an invalid step within an error", func() {
  1499  				BeforeEach(func() {
  1500  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1501  						Config: &atc.OnErrorStep{
  1502  							Step: &atc.GetStep{
  1503  								Name: "some-resource",
  1504  							},
  1505  							Hook: atc.Step{
  1506  								Config: &atc.PutStep{
  1507  									Name:     "custom-name",
  1508  									Resource: "some-missing-resource",
  1509  								},
  1510  							},
  1511  						},
  1512  					})
  1513  
  1514  					config.Jobs = append(config.Jobs, job)
  1515  				})
  1516  
  1517  				It("throws a validation error", func() {
  1518  					Expect(errorMessages).To(HaveLen(1))
  1519  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].on_error.put(custom-name): unknown resource 'some-missing-resource'"))
  1520  				})
  1521  			})
  1522  
  1523  			Context("when a plan has an invalid step within an ensure", func() {
  1524  				BeforeEach(func() {
  1525  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1526  						Config: &atc.EnsureStep{
  1527  							Step: &atc.GetStep{
  1528  								Name: "some-resource",
  1529  							},
  1530  							Hook: atc.Step{
  1531  								Config: &atc.PutStep{
  1532  									Name:     "custom-name",
  1533  									Resource: "some-missing-resource",
  1534  								},
  1535  							},
  1536  						},
  1537  					})
  1538  
  1539  					config.Jobs = append(config.Jobs, job)
  1540  				})
  1541  
  1542  				It("throws a validation error", func() {
  1543  					Expect(errorMessages).To(HaveLen(1))
  1544  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].ensure.put(custom-name): unknown resource 'some-missing-resource'"))
  1545  				})
  1546  			})
  1547  
  1548  			Context("when a plan has an invalid step within a success", func() {
  1549  				BeforeEach(func() {
  1550  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1551  						Config: &atc.OnSuccessStep{
  1552  							Step: &atc.GetStep{
  1553  								Name: "some-resource",
  1554  							},
  1555  							Hook: atc.Step{
  1556  								Config: &atc.PutStep{
  1557  									Name:     "custom-name",
  1558  									Resource: "some-missing-resource",
  1559  								},
  1560  							},
  1561  						},
  1562  					})
  1563  
  1564  					config.Jobs = append(config.Jobs, job)
  1565  				})
  1566  
  1567  				It("throws a validation error", func() {
  1568  					Expect(errorMessages).To(HaveLen(1))
  1569  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].on_success.put(custom-name): unknown resource 'some-missing-resource'"))
  1570  				})
  1571  			})
  1572  
  1573  			Context("when a plan has an invalid step within a failure", func() {
  1574  				BeforeEach(func() {
  1575  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1576  						Config: &atc.OnFailureStep{
  1577  							Step: &atc.GetStep{
  1578  								Name: "some-resource",
  1579  							},
  1580  							Hook: atc.Step{
  1581  								Config: &atc.PutStep{
  1582  									Name:     "custom-name",
  1583  									Resource: "some-missing-resource",
  1584  								},
  1585  							},
  1586  						},
  1587  					})
  1588  
  1589  					config.Jobs = append(config.Jobs, job)
  1590  				})
  1591  
  1592  				It("throws a validation error", func() {
  1593  					Expect(errorMessages).To(HaveLen(1))
  1594  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].on_failure.put(custom-name): unknown resource 'some-missing-resource'"))
  1595  				})
  1596  			})
  1597  
  1598  			Context("when a plan has an invalid step within a try", func() {
  1599  				BeforeEach(func() {
  1600  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1601  						Config: &atc.TryStep{
  1602  							Step: atc.Step{
  1603  								Config: &atc.PutStep{
  1604  									Name:     "custom-name",
  1605  									Resource: "some-missing-resource",
  1606  								},
  1607  							},
  1608  						},
  1609  					})
  1610  
  1611  					config.Jobs = append(config.Jobs, job)
  1612  				})
  1613  
  1614  				It("throws a validation error", func() {
  1615  					Expect(errorMessages).To(HaveLen(1))
  1616  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].try.put(custom-name): unknown resource 'some-missing-resource'"))
  1617  				})
  1618  			})
  1619  
  1620  			Context("when a plan has an invalid timeout in a step", func() {
  1621  				BeforeEach(func() {
  1622  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1623  						Config: &atc.TimeoutStep{
  1624  							Step: &atc.GetStep{
  1625  								Name: "some-resource",
  1626  							},
  1627  							Duration: "nope",
  1628  						},
  1629  					})
  1630  
  1631  					config.Jobs = append(config.Jobs, job)
  1632  				})
  1633  
  1634  				It("throws a validation error", func() {
  1635  					Expect(errorMessages).To(HaveLen(1))
  1636  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].timeout: invalid duration 'nope'"))
  1637  				})
  1638  			})
  1639  
  1640  			Context("when a retry plan has a negative attempts number", func() {
  1641  				BeforeEach(func() {
  1642  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1643  						Config: &atc.RetryStep{
  1644  							Step: &atc.PutStep{
  1645  								Name: "some-resource",
  1646  							},
  1647  							Attempts: 0,
  1648  						},
  1649  					})
  1650  
  1651  					config.Jobs = append(config.Jobs, job)
  1652  				})
  1653  
  1654  				It("does return an error", func() {
  1655  					Expect(errorMessages).To(HaveLen(1))
  1656  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].attempts: must be greater than 0"))
  1657  				})
  1658  			})
  1659  
  1660  			Context("when a set_pipeline step has no file configured", func() {
  1661  				BeforeEach(func() {
  1662  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1663  						Config: &atc.SetPipelineStep{
  1664  							Name: "other-pipeline",
  1665  						},
  1666  					})
  1667  
  1668  					config.Jobs = append(config.Jobs, job)
  1669  				})
  1670  
  1671  				It("does return an error", func() {
  1672  					Expect(errorMessages).To(HaveLen(1))
  1673  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].set_pipeline(other-pipeline): no file specified"))
  1674  				})
  1675  			})
  1676  
  1677  			Context("when a job's input's passed constraints reference a bogus job", func() {
  1678  				BeforeEach(func() {
  1679  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1680  						Config: &atc.GetStep{
  1681  							Name:   "lol",
  1682  							Passed: []string{"bogus-job"},
  1683  						},
  1684  					})
  1685  
  1686  					config.Jobs = append(config.Jobs, job)
  1687  				})
  1688  
  1689  				It("returns an error", func() {
  1690  					Expect(errorMessages).To(HaveLen(1))
  1691  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].get(lol).passed: unknown job 'bogus-job'"))
  1692  				})
  1693  			})
  1694  
  1695  			Context("when a job's input's passed constraints references a valid job that has the resource as an output", func() {
  1696  				BeforeEach(func() {
  1697  					config.Jobs[0].PlanSequence = append(config.Jobs[0].PlanSequence, atc.Step{
  1698  						Config: &atc.PutStep{
  1699  							Name:     "custom-name",
  1700  							Resource: "some-resource",
  1701  						},
  1702  					})
  1703  
  1704  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1705  						Config: &atc.GetStep{
  1706  							Name:   "some-resource",
  1707  							Passed: []string{"some-job"},
  1708  						},
  1709  					})
  1710  
  1711  					config.Jobs = append(config.Jobs, job)
  1712  				})
  1713  
  1714  				It("does not return an error", func() {
  1715  					Expect(errorMessages).To(HaveLen(0))
  1716  				})
  1717  			})
  1718  
  1719  			Context("when a job's input's passed constraints references a valid job that has the resource as an input", func() {
  1720  				BeforeEach(func() {
  1721  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1722  						Config: &atc.GetStep{
  1723  							Name:   "some-resource",
  1724  							Passed: []string{"some-job"},
  1725  						},
  1726  					})
  1727  
  1728  					config.Jobs = append(config.Jobs, job)
  1729  				})
  1730  
  1731  				It("does not return an error", func() {
  1732  					Expect(errorMessages).To(HaveLen(0))
  1733  				})
  1734  			})
  1735  
  1736  			Context("when a job's input's passed constraints references a valid job that has the resource (with a custom name) as an input", func() {
  1737  				BeforeEach(func() {
  1738  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1739  						Config: &atc.GetStep{
  1740  							Name:     "custom-name",
  1741  							Resource: "some-resource",
  1742  							Passed:   []string{"some-job"},
  1743  						},
  1744  					})
  1745  
  1746  					config.Jobs = append(config.Jobs, job)
  1747  				})
  1748  
  1749  				It("does not return an error", func() {
  1750  					Expect(errorMessages).To(HaveLen(0))
  1751  				})
  1752  			})
  1753  
  1754  			Context("when a job's input's passed constraints references a valid job that does not have the resource as an input or output", func() {
  1755  				BeforeEach(func() {
  1756  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1757  						Config: &atc.GetStep{
  1758  							Name:   "some-resource",
  1759  							Passed: []string{"some-empty-job"},
  1760  						},
  1761  					})
  1762  
  1763  					config.Jobs = append(config.Jobs, job)
  1764  				})
  1765  
  1766  				It("returns an error", func() {
  1767  					Expect(errorMessages).To(HaveLen(1))
  1768  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].get(some-resource).passed: job 'some-empty-job' does not interact with resource 'some-resource'"))
  1769  				})
  1770  			})
  1771  
  1772  			Context("when a load_var has not defined 'File'", func() {
  1773  				BeforeEach(func() {
  1774  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1775  						Config: &atc.LoadVarStep{
  1776  							Name: "a-var",
  1777  						},
  1778  					})
  1779  
  1780  					config.Jobs = append(config.Jobs, job)
  1781  				})
  1782  
  1783  				It("returns an error", func() {
  1784  					Expect(errorMessages).To(HaveLen(1))
  1785  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].load_var(a-var): no file specified"))
  1786  				})
  1787  			})
  1788  
  1789  			Context("when two load_var steps have same name", func() {
  1790  				BeforeEach(func() {
  1791  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1792  						Config: &atc.LoadVarStep{
  1793  							Name: "a-var",
  1794  							File: "file1",
  1795  						},
  1796  					}, atc.Step{
  1797  						Config: &atc.LoadVarStep{
  1798  							Name: "a-var",
  1799  							File: "file1",
  1800  						},
  1801  					})
  1802  
  1803  					config.Jobs = append(config.Jobs, job)
  1804  				})
  1805  
  1806  				It("returns an error", func() {
  1807  					Expect(errorMessages).To(HaveLen(1))
  1808  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[1].load_var(a-var): repeated var name"))
  1809  				})
  1810  			})
  1811  
  1812  			Context("when a step has unknown fields", func() {
  1813  				BeforeEach(func() {
  1814  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1815  						Config: &atc.TaskStep{
  1816  							Name:       "task",
  1817  							ConfigPath: "some-file",
  1818  						},
  1819  						UnknownFields: map[string]*json.RawMessage{"bogus": nil},
  1820  					})
  1821  
  1822  					config.Jobs = append(config.Jobs, job)
  1823  				})
  1824  
  1825  				It("returns an error", func() {
  1826  					Expect(errorMessages).To(HaveLen(1))
  1827  					Expect(errorMessages[0]).To(ContainSubstring(`jobs.some-other-job.plan.do[0]: unknown fields ["bogus"]`))
  1828  				})
  1829  			})
  1830  
  1831  			Context("when an across step is valid", func() {
  1832  				BeforeEach(func() {
  1833  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1834  						Config: &atc.AcrossStep{
  1835  							Step: &atc.PutStep{
  1836  								Name: "some-resource",
  1837  							},
  1838  							Vars: []atc.AcrossVarConfig{
  1839  								{
  1840  									Var:    "var1",
  1841  									Values: []interface{}{"v1", "v2"},
  1842  								},
  1843  								{
  1844  									Var:         "var2",
  1845  									MaxInFlight: &atc.MaxInFlightConfig{Limit: 2},
  1846  									Values:      []interface{}{"v1", "v2"},
  1847  								},
  1848  								{
  1849  									Var:         "var3",
  1850  									MaxInFlight: &atc.MaxInFlightConfig{All: true},
  1851  									Values:      []interface{}{"v1", "v2"},
  1852  								},
  1853  							},
  1854  						},
  1855  					})
  1856  
  1857  					config.Jobs = append(config.Jobs, job)
  1858  				})
  1859  
  1860  				It("succeeds", func() {
  1861  					Expect(errorMessages).To(HaveLen(0))
  1862  				})
  1863  			})
  1864  
  1865  			Context("when an across step has no vars", func() {
  1866  				BeforeEach(func() {
  1867  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1868  						Config: &atc.AcrossStep{
  1869  							Step: &atc.PutStep{
  1870  								Name: "some-resource",
  1871  							},
  1872  							Vars: []atc.AcrossVarConfig{},
  1873  						},
  1874  					})
  1875  
  1876  					config.Jobs = append(config.Jobs, job)
  1877  				})
  1878  
  1879  				It("returns an error", func() {
  1880  					Expect(errorMessages).To(HaveLen(1))
  1881  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].across: no vars specified"))
  1882  				})
  1883  			})
  1884  
  1885  			Context("when an across step repeats a var name", func() {
  1886  				BeforeEach(func() {
  1887  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1888  						Config: &atc.AcrossStep{
  1889  							Step: &atc.PutStep{
  1890  								Name: "some-resource",
  1891  							},
  1892  							Vars: []atc.AcrossVarConfig{
  1893  								{
  1894  									Var: "var1",
  1895  								},
  1896  								{
  1897  									Var: "var1",
  1898  								},
  1899  							},
  1900  						},
  1901  					})
  1902  
  1903  					config.Jobs = append(config.Jobs, job)
  1904  				})
  1905  
  1906  				It("returns an error", func() {
  1907  					Expect(errorMessages).To(HaveLen(1))
  1908  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].across[1]: repeated var name"))
  1909  				})
  1910  			})
  1911  
  1912  			Context("when an across step shadows a var name from a parent scope", func() {
  1913  				BeforeEach(func() {
  1914  					job.PlanSequence = append(job.PlanSequence,
  1915  						atc.Step{Config: &atc.LoadVarStep{
  1916  							Name: "var1",
  1917  							File: "unused",
  1918  						}},
  1919  						atc.Step{
  1920  							Config: &atc.AcrossStep{
  1921  								Step: &atc.PutStep{
  1922  									Name: "some-resource",
  1923  								},
  1924  								Vars: []atc.AcrossVarConfig{
  1925  									{
  1926  										Var: "var1",
  1927  									},
  1928  								},
  1929  							},
  1930  						})
  1931  
  1932  					config.Jobs = append(config.Jobs, job)
  1933  				})
  1934  
  1935  				It("returns a warning", func() {
  1936  					Expect(errorMessages).To(BeEmpty())
  1937  					Expect(warnings).To(HaveLen(1))
  1938  					Expect(warnings[0].Message).To(ContainSubstring("jobs.some-other-job.plan.do[1].across[0]: shadows local var 'var1'"))
  1939  				})
  1940  			})
  1941  
  1942  			Context("when a substep of the across step shadows a var name from a parent scope", func() {
  1943  				BeforeEach(func() {
  1944  					job.PlanSequence = append(job.PlanSequence,
  1945  						atc.Step{Config: &atc.LoadVarStep{
  1946  							Name: "a",
  1947  							File: "unused",
  1948  						}},
  1949  						atc.Step{
  1950  							Config: &atc.AcrossStep{
  1951  								Step: &atc.LoadVarStep{
  1952  									Name: "a",
  1953  									File: "unused",
  1954  								},
  1955  								Vars: []atc.AcrossVarConfig{
  1956  									{
  1957  										Var: "b",
  1958  									},
  1959  								},
  1960  							},
  1961  						})
  1962  
  1963  					config.Jobs = append(config.Jobs, job)
  1964  				})
  1965  
  1966  				It("returns a warning", func() {
  1967  					Expect(errorMessages).To(BeEmpty())
  1968  					Expect(warnings).To(HaveLen(1))
  1969  					Expect(warnings[0].Message).To(ContainSubstring("jobs.some-other-job.plan.do[1].across.load_var(a): shadows local var 'a'"))
  1970  				})
  1971  			})
  1972  
  1973  			Context("when an across step has a non-positive limit", func() {
  1974  				BeforeEach(func() {
  1975  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  1976  						Config: &atc.AcrossStep{
  1977  							Step: &atc.PutStep{
  1978  								Name: "some-resource",
  1979  							},
  1980  							Vars: []atc.AcrossVarConfig{
  1981  								{
  1982  									Var:         "var",
  1983  									MaxInFlight: &atc.MaxInFlightConfig{Limit: 0},
  1984  								},
  1985  							},
  1986  						},
  1987  					})
  1988  
  1989  					config.Jobs = append(config.Jobs, job)
  1990  				})
  1991  
  1992  				It("returns an error", func() {
  1993  					Expect(errorMessages).To(HaveLen(1))
  1994  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].across[0].max_in_flight: must be greater than 0"))
  1995  				})
  1996  			})
  1997  
  1998  			Context("when the across step is not enabled", func() {
  1999  				BeforeEach(func() {
  2000  					atc.EnableAcrossStep = false
  2001  
  2002  					job.PlanSequence = append(job.PlanSequence, atc.Step{
  2003  						Config: &atc.AcrossStep{
  2004  							Step: &atc.PutStep{
  2005  								Name: "some-resource",
  2006  							},
  2007  							Vars: []atc.AcrossVarConfig{
  2008  								{
  2009  									Var: "var",
  2010  								},
  2011  							},
  2012  						},
  2013  					})
  2014  
  2015  					config.Jobs = append(config.Jobs, job)
  2016  				})
  2017  
  2018  				It("returns an error", func() {
  2019  					Expect(errorMessages).To(HaveLen(1))
  2020  					Expect(errorMessages[0]).To(ContainSubstring("jobs.some-other-job.plan.do[0].across: the across step must be explicitly opted-in to using the `--enable-across-step` flag"))
  2021  				})
  2022  			})
  2023  		})
  2024  
  2025  		Context("when two jobs have the same name", func() {
  2026  			BeforeEach(func() {
  2027  				config.Jobs = append(config.Jobs, config.Jobs...)
  2028  			})
  2029  
  2030  			It("returns an error", func() {
  2031  				Expect(errorMessages).To(HaveLen(1))
  2032  				Expect(errorMessages[0]).To(ContainSubstring("jobs[0] and jobs[2] have the same name ('some-job')"))
  2033  			})
  2034  		})
  2035  
  2036  		Context("when a job has build_log_retention and deprecated build_logs_to_retain", func() {
  2037  			BeforeEach(func() {
  2038  				config.Jobs[0].BuildLogRetention = &atc.BuildLogRetention{
  2039  					Builds: 1,
  2040  					Days:   1,
  2041  				}
  2042  				config.Jobs[0].BuildLogsToRetain = 1
  2043  			})
  2044  
  2045  			It("returns an error", func() {
  2046  				Expect(errorMessages).To(HaveLen(1))
  2047  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-job can't use both build_log_retention and build_logs_to_retain"))
  2048  			})
  2049  		})
  2050  
  2051  		Context("when a job has negative build_logs_to_retain", func() {
  2052  			BeforeEach(func() {
  2053  				config.Jobs[0].BuildLogsToRetain = -1
  2054  			})
  2055  
  2056  			It("returns an error", func() {
  2057  				Expect(errorMessages).To(HaveLen(1))
  2058  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-job has negative build_logs_to_retain: -1"))
  2059  			})
  2060  		})
  2061  
  2062  		Context("when a job has negative build_log_retention values", func() {
  2063  			BeforeEach(func() {
  2064  				config.Jobs[0].BuildLogRetention = &atc.BuildLogRetention{
  2065  					Builds: -1,
  2066  					Days:   -1,
  2067  				}
  2068  			})
  2069  
  2070  			It("returns an error", func() {
  2071  				Expect(errorMessages).To(HaveLen(1))
  2072  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-job has negative build_log_retention.builds: -1"))
  2073  				Expect(errorMessages[0]).To(ContainSubstring("jobs.some-job has negative build_log_retention.days: -1"))
  2074  			})
  2075  		})
  2076  	})
  2077  
  2078  	Describe("validating display config", func() {
  2079  		Context("when the background image is a valid http URL", func() {
  2080  			BeforeEach(func() {
  2081  				config.Display = &atc.DisplayConfig{
  2082  					BackgroundImage: "http://example.com/image.jpg",
  2083  				}
  2084  			})
  2085  
  2086  			It("does not return an error", func() {
  2087  				Expect(errorMessages).To(HaveLen(0))
  2088  			})
  2089  		})
  2090  
  2091  		Context("when the background image is a valid relative URL", func() {
  2092  			BeforeEach(func() {
  2093  				config.Display = &atc.DisplayConfig{
  2094  					BackgroundImage: "public/images/image.jpg",
  2095  				}
  2096  			})
  2097  
  2098  			It("does not return an error", func() {
  2099  				Expect(errorMessages).To(HaveLen(0))
  2100  			})
  2101  		})
  2102  
  2103  		Context("when the background image uses an unsupported scheme", func() {
  2104  			BeforeEach(func() {
  2105  				config.Display = &atc.DisplayConfig{
  2106  					BackgroundImage: "data:image/png;base64, iVBORw0KGgoA",
  2107  				}
  2108  			})
  2109  
  2110  			It("returns an error", func() {
  2111  				Expect(errorMessages).To(HaveLen(1))
  2112  				Expect(errorMessages[0]).To(ContainSubstring("invalid display config:"))
  2113  				Expect(errorMessages[0]).To(ContainSubstring("background_image scheme must be either http, https or relative"))
  2114  			})
  2115  		})
  2116  
  2117  		Context("when the background image is an invalid URL", func() {
  2118  			BeforeEach(func() {
  2119  				config.Display = &atc.DisplayConfig{
  2120  					BackgroundImage: "://example.com",
  2121  				}
  2122  			})
  2123  
  2124  			It("returns an error", func() {
  2125  				Expect(errorMessages).To(HaveLen(1))
  2126  				Expect(errorMessages[0]).To(ContainSubstring("invalid display config:"))
  2127  				Expect(errorMessages[0]).To(ContainSubstring("background_image is not a valid URL: ://example.com"))
  2128  			})
  2129  		})
  2130  	})
  2131  
  2132  	Describe("invalid pipeline", func() {
  2133  		Context("contains zero jobs", func() {
  2134  			BeforeEach(func() {
  2135  				config = atc.Config{}
  2136  			})
  2137  			It("is an invalid pipeline", func() {
  2138  				Expect(errorMessages).To(HaveLen(1))
  2139  				Expect(errorMessages[0]).To(ContainSubstring("pipeline must contain at least one job"))
  2140  			})
  2141  
  2142  		})
  2143  	})
  2144  })