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

     1  package exec_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  
     9  	. "github.com/onsi/ginkgo"
    10  	. "github.com/onsi/gomega"
    11  	"go.opentelemetry.io/otel/api/trace"
    12  
    13  	"code.cloudfoundry.org/lager/lagerctx"
    14  	"code.cloudfoundry.org/lager/lagertest"
    15  	"github.com/pf-qiu/concourse/v6/atc"
    16  	"github.com/pf-qiu/concourse/v6/atc/db"
    17  	"github.com/pf-qiu/concourse/v6/atc/db/dbfakes"
    18  	"github.com/pf-qiu/concourse/v6/atc/exec"
    19  	"github.com/pf-qiu/concourse/v6/atc/exec/build"
    20  	"github.com/pf-qiu/concourse/v6/atc/exec/build/buildfakes"
    21  	"github.com/pf-qiu/concourse/v6/atc/exec/execfakes"
    22  	"github.com/pf-qiu/concourse/v6/atc/policy"
    23  	"github.com/pf-qiu/concourse/v6/atc/policy/policyfakes"
    24  	"github.com/pf-qiu/concourse/v6/atc/worker/workerfakes"
    25  	"github.com/pf-qiu/concourse/v6/vars"
    26  	"github.com/onsi/gomega/gbytes"
    27  )
    28  
    29  var _ = Describe("SetPipelineStep", func() {
    30  
    31  	const badPipelineContentWithInvalidSyntax = `
    32  ---
    33  jobs:
    34  - name:
    35  `
    36  
    37  	const badPipelineContentWithEmptyContent = `
    38  ---
    39  `
    40  
    41  	const pipelineContent = `
    42  ---
    43  jobs:
    44  - name: some-job
    45    plan:
    46    - task: some-task
    47      config:
    48        platform: linux
    49        image_resource:
    50          type: registry-image
    51          source: {repository: busybox}
    52        run:
    53          path: echo
    54          args:
    55           - hello
    56  `
    57  
    58  	var pipelineObject = atc.Config{
    59  		Jobs: atc.JobConfigs{
    60  			{
    61  				Name: "some-job",
    62  				PlanSequence: []atc.Step{
    63  					{
    64  						Config: &atc.TaskStep{
    65  							Name: "some-task",
    66  							Config: &atc.TaskConfig{
    67  								Platform: "linux",
    68  								ImageResource: &atc.ImageResource{
    69  									Type:   "registry-image",
    70  									Source: atc.Source{"repository": "busybox"},
    71  								},
    72  								Run: atc.TaskRunConfig{
    73  									Path: "echo",
    74  									Args: []string{"hello"},
    75  								},
    76  							},
    77  						},
    78  					},
    79  				},
    80  			},
    81  		},
    82  	}
    83  
    84  	var (
    85  		ctx        context.Context
    86  		cancel     func()
    87  		testLogger *lagertest.TestLogger
    88  
    89  		fakeTeamFactory  *dbfakes.FakeTeamFactory
    90  		fakeBuildFactory *dbfakes.FakeBuildFactory
    91  		fakeBuild        *dbfakes.FakeBuild
    92  		fakeTeam         *dbfakes.FakeTeam
    93  		fakePipeline     *dbfakes.FakePipeline
    94  		spanCtx          context.Context
    95  
    96  		fakeDelegate        *execfakes.FakeSetPipelineStepDelegate
    97  		fakeDelegateFactory *execfakes.FakeSetPipelineStepDelegateFactory
    98  
    99  		filter      policy.Filter
   100  		fakeAgent   *policyfakes.FakeAgent
   101  		fakeChecker policy.Checker
   102  
   103  		fakeWorkerClient *workerfakes.FakeClient
   104  
   105  		spPlan             *atc.SetPipelinePlan
   106  		artifactRepository *build.Repository
   107  		state              *execfakes.FakeRunState
   108  		fakeSource         *buildfakes.FakeRegisterableArtifact
   109  
   110  		spStep  exec.Step
   111  		stepOk  bool
   112  		stepErr error
   113  
   114  		stepMetadata = exec.StepMetadata{
   115  			TeamID:               123,
   116  			TeamName:             "some-team",
   117  			JobID:                87,
   118  			JobName:              "some-job",
   119  			BuildID:              42,
   120  			BuildName:            "some-build",
   121  			PipelineID:           4567,
   122  			PipelineName:         "some-pipeline",
   123  			PipelineInstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   124  		}
   125  
   126  		stdout, stderr *gbytes.Buffer
   127  
   128  		planID = "56"
   129  	)
   130  
   131  	BeforeEach(func() {
   132  		testLogger = lagertest.NewTestLogger("set-pipeline-action-test")
   133  		ctx, cancel = context.WithCancel(context.Background())
   134  		ctx = lagerctx.NewContext(ctx, testLogger)
   135  
   136  		artifactRepository = build.NewRepository()
   137  		state = new(execfakes.FakeRunState)
   138  		state.ArtifactRepositoryReturns(artifactRepository)
   139  
   140  		state.GetStub = vars.StaticVariables{"source-param": "super-secret-source"}.Get
   141  
   142  		fakeSource = new(buildfakes.FakeRegisterableArtifact)
   143  		artifactRepository.RegisterArtifact("some-resource", fakeSource)
   144  
   145  		stdout = gbytes.NewBuffer()
   146  		stderr = gbytes.NewBuffer()
   147  
   148  		fakeDelegate = new(execfakes.FakeSetPipelineStepDelegate)
   149  		fakeDelegate.StdoutReturns(stdout)
   150  		fakeDelegate.StderrReturns(stderr)
   151  
   152  		spanCtx = context.Background()
   153  		fakeDelegate.StartSpanReturns(spanCtx, trace.NoopSpan{})
   154  
   155  		fakeDelegateFactory = new(execfakes.FakeSetPipelineStepDelegateFactory)
   156  		fakeDelegateFactory.SetPipelineStepDelegateReturns(fakeDelegate)
   157  
   158  		fakeTeamFactory = new(dbfakes.FakeTeamFactory)
   159  		fakeBuildFactory = new(dbfakes.FakeBuildFactory)
   160  		fakeBuild = new(dbfakes.FakeBuild)
   161  		fakeTeam = new(dbfakes.FakeTeam)
   162  		fakePipeline = new(dbfakes.FakePipeline)
   163  
   164  		stepMetadata = exec.StepMetadata{
   165  			TeamID:               123,
   166  			TeamName:             "some-team",
   167  			BuildID:              42,
   168  			BuildName:            "some-build",
   169  			PipelineID:           4567,
   170  			PipelineName:         "some-pipeline",
   171  			PipelineInstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   172  		}
   173  
   174  		fakeTeam.IDReturns(stepMetadata.TeamID)
   175  		fakeTeam.NameReturns(stepMetadata.TeamName)
   176  
   177  		fakePipeline.NameReturns("some-pipeline")
   178  		fakePipeline.InstanceVarsReturns(atc.InstanceVars{"branch": "feature/foo"})
   179  		fakeTeamFactory.GetByIDReturns(fakeTeam)
   180  		fakeBuildFactory.BuildReturns(fakeBuild, true, nil)
   181  
   182  		filter = policy.Filter{
   183  			Actions: []string{exec.ActionRunSetPipeline},
   184  		}
   185  
   186  		fakeAgent = new(policyfakes.FakeAgent)
   187  		fakeAgent.CheckReturns(policy.PassedPolicyCheck(), nil)
   188  		fakePolicyAgentFactory.NewAgentReturns(fakeAgent, nil)
   189  
   190  		fakeChecker, _ = policy.Initialize(testLogger, "some-cluster", "some-version", filter)
   191  
   192  		fakeWorkerClient = new(workerfakes.FakeClient)
   193  
   194  		spPlan = &atc.SetPipelinePlan{
   195  			Name:         "some-pipeline",
   196  			File:         "some-resource/pipeline.yml",
   197  			InstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   198  		}
   199  	})
   200  
   201  	AfterEach(func() {
   202  		cancel()
   203  	})
   204  
   205  	JustBeforeEach(func() {
   206  		plan := atc.Plan{
   207  			ID:          atc.PlanID(planID),
   208  			SetPipeline: spPlan,
   209  		}
   210  
   211  		spStep = exec.NewSetPipelineStep(
   212  			plan.ID,
   213  			*plan.SetPipeline,
   214  			stepMetadata,
   215  			fakeDelegateFactory,
   216  			fakeTeamFactory,
   217  			fakeBuildFactory,
   218  			fakeWorkerClient,
   219  			fakeChecker,
   220  		)
   221  
   222  		stepOk, stepErr = spStep.Run(ctx, state)
   223  	})
   224  
   225  	Context("when file is not configured", func() {
   226  		BeforeEach(func() {
   227  			spPlan = &atc.SetPipelinePlan{
   228  				Name: "some-pipeline",
   229  			}
   230  		})
   231  
   232  		It("should fail with error of file not configured", func() {
   233  			Expect(stepErr).To(HaveOccurred())
   234  			Expect(stepErr.Error()).To(Equal("file is not specified"))
   235  		})
   236  	})
   237  
   238  	Context("when file is configured", func() {
   239  		Context("pipeline file not exist", func() {
   240  			BeforeEach(func() {
   241  				fakeWorkerClient.StreamFileFromArtifactReturns(nil, errors.New("file not found"))
   242  			})
   243  
   244  			It("should fail with error of file not configured", func() {
   245  				Expect(stepErr).To(HaveOccurred())
   246  				Expect(stepErr.Error()).To(Equal("file not found"))
   247  			})
   248  		})
   249  
   250  		Context("when pipeline file exists but bad syntax", func() {
   251  			BeforeEach(func() {
   252  				fakeWorkerClient.StreamFileFromArtifactReturns(&fakeReadCloser{str: badPipelineContentWithInvalidSyntax}, nil)
   253  			})
   254  
   255  			It("should not return error", func() {
   256  				Expect(stepErr).NotTo(HaveOccurred())
   257  			})
   258  
   259  			It("should stderr have error message", func() {
   260  				Expect(stderr).To(gbytes.Say("invalid pipeline:"))
   261  				Expect(stderr).To(gbytes.Say("- invalid jobs:"))
   262  			})
   263  
   264  			It("should finish unsuccessfully", func() {
   265  				Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
   266  				_, succeeded := fakeDelegate.FinishedArgsForCall(0)
   267  				Expect(succeeded).To(BeFalse())
   268  			})
   269  		})
   270  
   271  		Context("when pipeline file exists but is empty", func() {
   272  			BeforeEach(func() {
   273  				fakeWorkerClient.StreamFileFromArtifactReturns(&fakeReadCloser{str: badPipelineContentWithEmptyContent}, nil)
   274  			})
   275  
   276  			It("should return an error", func() {
   277  				Expect(stepErr).NotTo(HaveOccurred())
   278  			})
   279  
   280  			It("should log an error message", func() {
   281  				Expect(stderr).To(gbytes.Say("pipeline must contain at least one job"))
   282  			})
   283  
   284  			It("should not update the job and build id", func() {
   285  				Expect(fakePipeline.SetParentIDsCallCount()).To(Equal(0))
   286  			})
   287  		})
   288  
   289  		Context("when pipeline file is good", func() {
   290  			BeforeEach(func() {
   291  				fakeWorkerClient.StreamFileFromArtifactReturns(&fakeReadCloser{str: pipelineContent}, nil)
   292  			})
   293  
   294  			Context("when get pipeline fails", func() {
   295  				BeforeEach(func() {
   296  					fakeTeam.PipelineReturns(nil, false, errors.New("fail to get pipeline"))
   297  				})
   298  
   299  				It("should return error", func() {
   300  					Expect(stepErr).To(HaveOccurred())
   301  					Expect(stepErr.Error()).To(Equal("fail to get pipeline"))
   302  				})
   303  			})
   304  
   305  			Context("when specified pipeline not found", func() {
   306  				BeforeEach(func() {
   307  					fakeTeam.PipelineReturns(nil, false, nil)
   308  					fakeBuild.SavePipelineReturns(fakePipeline, true, nil)
   309  				})
   310  
   311  				It("should save the pipeline", func() {
   312  					Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1))
   313  					ref, _, _, _, paused := fakeBuild.SavePipelineArgsForCall(0)
   314  					Expect(ref).To(Equal(atc.PipelineRef{
   315  						Name:         "some-pipeline",
   316  						InstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   317  					}))
   318  					Expect(paused).To(BeFalse())
   319  				})
   320  
   321  				It("should stdout have message", func() {
   322  					Expect(stdout).To(gbytes.Say("done"))
   323  				})
   324  			})
   325  
   326  			Context("when specified pipeline exists already", func() {
   327  				BeforeEach(func() {
   328  					fakeTeam.PipelineReturns(fakePipeline, true, nil)
   329  					fakeBuild.SavePipelineReturns(fakePipeline, false, nil)
   330  				})
   331  
   332  				Context("when no diff", func() {
   333  					BeforeEach(func() {
   334  						fakePipeline.ConfigReturns(pipelineObject, nil)
   335  						fakePipeline.SetParentIDsReturns(nil)
   336  					})
   337  
   338  					It("should log 'no changes to apply'", func() {
   339  						Expect(stdout).To(gbytes.Say("no changes to apply."))
   340  					})
   341  
   342  					It("should send a set pipeline changed event", func() {
   343  						Expect(fakeDelegate.SetPipelineChangedCallCount()).To(Equal(1))
   344  						_, changed := fakeDelegate.SetPipelineChangedArgsForCall(0)
   345  						Expect(changed).To(BeFalse())
   346  					})
   347  
   348  					It("should update the job and build id", func() {
   349  						Expect(fakePipeline.SetParentIDsCallCount()).To(Equal(1))
   350  						jobID, buildID := fakePipeline.SetParentIDsArgsForCall(0)
   351  						Expect(jobID).To(Equal(stepMetadata.JobID))
   352  						Expect(buildID).To(Equal(stepMetadata.BuildID))
   353  					})
   354  				})
   355  
   356  				Context("when there are some diff", func() {
   357  					BeforeEach(func() {
   358  						pipelineObject.Jobs[0].PlanSequence[0].Config.(*atc.TaskStep).Config.Run.Args = []string{"hello world"}
   359  						fakePipeline.ConfigReturns(pipelineObject, nil)
   360  					})
   361  
   362  					It("should log diff", func() {
   363  						Expect(stdout).To(gbytes.Say("job some-job has changed:"))
   364  					})
   365  
   366  					It("should send a set pipeline changed event", func() {
   367  						Expect(fakeDelegate.SetPipelineChangedCallCount()).To(Equal(1))
   368  						_, changed := fakeDelegate.SetPipelineChangedArgsForCall(0)
   369  						Expect(changed).To(BeTrue())
   370  					})
   371  				})
   372  
   373  				Context("when SavePipeline fails", func() {
   374  					BeforeEach(func() {
   375  						fakeBuild.SavePipelineReturns(nil, false, errors.New("failed to save"))
   376  					})
   377  
   378  					It("should return error", func() {
   379  						Expect(stepErr).To(HaveOccurred())
   380  						Expect(stepErr.Error()).To(Equal("failed to save"))
   381  					})
   382  
   383  					Context("due to the pipeline being set by a newer build", func() {
   384  						BeforeEach(func() {
   385  							fakeBuild.SavePipelineReturns(nil, false, db.ErrSetByNewerBuild)
   386  						})
   387  						It("logs a warning", func() {
   388  							Expect(stderr).To(gbytes.Say("WARNING: the pipeline was not saved because it was already saved by a newer build"))
   389  						})
   390  						It("does not fail the step", func() {
   391  							Expect(stepErr).ToNot(HaveOccurred())
   392  							Expect(stepOk).To(BeTrue())
   393  						})
   394  					})
   395  				})
   396  
   397  				It("should save the pipeline un-paused", func() {
   398  					Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1))
   399  					ref, _, _, _, paused := fakeBuild.SavePipelineArgsForCall(0)
   400  					Expect(ref).To(Equal(atc.PipelineRef{
   401  						Name:         "some-pipeline",
   402  						InstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   403  					}))
   404  					Expect(paused).To(BeFalse())
   405  				})
   406  
   407  				It("should stdout have message", func() {
   408  					Expect(stdout).To(gbytes.Say("setting pipeline: some-pipeline"))
   409  					Expect(stdout).To(gbytes.Say("done"))
   410  				})
   411  
   412  				It("should finish successfully", func() {
   413  					Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
   414  					_, succeeded := fakeDelegate.FinishedArgsForCall(0)
   415  					Expect(succeeded).To(BeTrue())
   416  				})
   417  			})
   418  
   419  			Context("when set-pipeline self", func() {
   420  				BeforeEach(func() {
   421  					spPlan = &atc.SetPipelinePlan{
   422  						Name:         "self",
   423  						File:         "some-resource/pipeline.yml",
   424  						Team:         "foo-team",
   425  						InstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   426  					}
   427  					fakeBuild.SavePipelineReturns(fakePipeline, false, nil)
   428  				})
   429  
   430  				It("should save the pipeline itself", func() {
   431  					Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1))
   432  					pipelineRef, _, _, _, _ := fakeBuild.SavePipelineArgsForCall(0)
   433  					Expect(pipelineRef).To(Equal(atc.PipelineRef{
   434  						Name:         "some-pipeline",
   435  						InstanceVars: atc.InstanceVars{"branch": "feature/foo"},
   436  					}))
   437  				})
   438  
   439  				It("should save to the current team", func() {
   440  					Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1))
   441  					_, teamId, _, _, _ := fakeBuild.SavePipelineArgsForCall(0)
   442  					Expect(teamId).To(Equal(fakeTeam.ID()))
   443  				})
   444  
   445  				It("should print an experimental message", func() {
   446  					Expect(stderr).To(gbytes.Say("WARNING: 'set_pipeline: self' is experimental"))
   447  					Expect(stderr).To(gbytes.Say("contribute to discussion #5732"))
   448  					Expect(stderr).To(gbytes.Say("discussions/5732"))
   449  				})
   450  			})
   451  
   452  			Context("when team is configured", func() {
   453  				var (
   454  					fakeUserCurrentTeam *dbfakes.FakeTeam
   455  				)
   456  
   457  				BeforeEach(func() {
   458  					fakeUserCurrentTeam = new(dbfakes.FakeTeam)
   459  					fakeUserCurrentTeam.IDReturns(111)
   460  					fakeUserCurrentTeam.NameReturns("main")
   461  					fakeUserCurrentTeam.AdminReturns(false)
   462  
   463  					stepMetadata.TeamID = fakeUserCurrentTeam.ID()
   464  					stepMetadata.TeamName = fakeUserCurrentTeam.Name()
   465  					fakeTeamFactory.FindTeamReturnsOnCall(
   466  						0,
   467  						fakeUserCurrentTeam, true, nil,
   468  					)
   469  				})
   470  
   471  				Context("when team is set to the empty string", func() {
   472  					BeforeEach(func() {
   473  						fakeBuild.PipelineReturns(fakePipeline, true, nil)
   474  						fakeBuild.SavePipelineReturns(fakePipeline, false, nil)
   475  						spPlan.Team = ""
   476  					})
   477  
   478  					It("should finish successfully", func() {
   479  						Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
   480  						_, succeeded := fakeDelegate.FinishedArgsForCall(0)
   481  						Expect(succeeded).To(BeTrue())
   482  					})
   483  				})
   484  
   485  				Context("when team does not exist", func() {
   486  					BeforeEach(func() {
   487  						spPlan.Team = "not-found"
   488  						fakeTeamFactory.FindTeamReturnsOnCall(
   489  							1,
   490  							nil, false, nil,
   491  						)
   492  					})
   493  
   494  					It("should return error", func() {
   495  						Expect(stepErr).To(HaveOccurred())
   496  						Expect(stepErr.Error()).To(Equal("team not-found not found"))
   497  					})
   498  				})
   499  
   500  				Context("when team exists", func() {
   501  					Context("when the target team is the current team", func() {
   502  						BeforeEach(func() {
   503  							spPlan.Team = fakeUserCurrentTeam.Name()
   504  							fakeTeamFactory.FindTeamReturnsOnCall(
   505  								1,
   506  								fakeUserCurrentTeam, true, nil,
   507  							)
   508  
   509  							fakeBuild.PipelineReturns(fakePipeline, true, nil)
   510  							fakeBuild.SavePipelineReturns(fakePipeline, false, nil)
   511  						})
   512  
   513  						It("should finish successfully", func() {
   514  							_, teamID, _, _, _ := fakeBuild.SavePipelineArgsForCall(0)
   515  							Expect(teamID).To(Equal(fakeUserCurrentTeam.ID()))
   516  							Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
   517  							_, succeeded := fakeDelegate.FinishedArgsForCall(0)
   518  							Expect(succeeded).To(BeTrue())
   519  						})
   520  
   521  						It("should print an experimental message", func() {
   522  							Expect(stderr).To(gbytes.Say("WARNING: specifying the team"))
   523  							Expect(stderr).To(gbytes.Say("contribute to discussion #5731"))
   524  							Expect(stderr).To(gbytes.Say("discussions/5731"))
   525  						})
   526  					})
   527  
   528  					Context("when the team is not the current team", func() {
   529  						BeforeEach(func() {
   530  							spPlan.Team = fakeTeam.Name()
   531  							fakeTeamFactory.FindTeamReturnsOnCall(
   532  								1,
   533  								fakeTeam, true, nil,
   534  							)
   535  						})
   536  
   537  						Context("when the current team is an admin team", func() {
   538  							BeforeEach(func() {
   539  								fakeUserCurrentTeam.AdminReturns(true)
   540  
   541  								fakeBuild.PipelineReturns(fakePipeline, true, nil)
   542  								fakeBuild.SavePipelineReturns(fakePipeline, false, nil)
   543  							})
   544  
   545  							It("should finish successfully", func() {
   546  								_, teamID, _, _, _ := fakeBuild.SavePipelineArgsForCall(0)
   547  								Expect(teamID).To(Equal(fakeTeam.ID()))
   548  								Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
   549  								_, succeeded := fakeDelegate.FinishedArgsForCall(0)
   550  								Expect(succeeded).To(BeTrue())
   551  							})
   552  						})
   553  
   554  						Context("when the current team is not an admin team", func() {
   555  							It("should return error", func() {
   556  
   557  								Expect(stepErr).To(HaveOccurred())
   558  								Expect(stepErr.Error()).To(Equal(
   559  									"only main team can set another team's pipeline",
   560  								))
   561  							})
   562  						})
   563  					})
   564  				})
   565  			})
   566  
   567  			Context("when policy checker enabled", func() {
   568  				Context("policy check errors", func() {
   569  					BeforeEach(func() {
   570  						result := policy.FailedPolicyCheck()
   571  						fakeAgent.CheckReturns(result, fmt.Errorf("unexpected error"))
   572  					})
   573  
   574  					It("should return error", func() {
   575  						Expect(stepErr).To(HaveOccurred())
   576  						Expect(stepErr.Error()).To(Equal("error checking policy enforcement"))
   577  					})
   578  				})
   579  
   580  				Context("policy check fails", func() {
   581  					BeforeEach(func() {
   582  						result := policy.FailedPolicyCheck()
   583  						result.Reasons = append(result.Reasons, "foo", "bar")
   584  						fakeAgent.CheckReturns(result, nil)
   585  					})
   586  
   587  					It("should return error", func() {
   588  						Expect(stepErr).To(HaveOccurred())
   589  						Expect(stepErr.Error()).To(Equal("policy check failed for set_pipeline: foo, bar"))
   590  					})
   591  				})
   592  
   593  				Context("policy check succeeds", func() {
   594  					BeforeEach(func() {
   595  						fakeBuild.PipelineReturns(fakePipeline, true, nil)
   596  						fakeBuild.SavePipelineReturns(fakePipeline, false, nil)
   597  						spPlan.Team = ""
   598  					})
   599  
   600  					It("should finish successfully", func() {
   601  						_, teamID, _, _, _ := fakeBuild.SavePipelineArgsForCall(0)
   602  						Expect(teamID).To(Equal(fakeTeam.ID()))
   603  						Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
   604  						_, succeeded := fakeDelegate.FinishedArgsForCall(0)
   605  						Expect(succeeded).To(BeTrue())
   606  					})
   607  				})
   608  			})
   609  		})
   610  	})
   611  })
   612  
   613  type fakeReadCloser struct {
   614  	str   string
   615  	index int
   616  }
   617  
   618  func (r *fakeReadCloser) Read(p []byte) (int, error) {
   619  	if r.index >= len(r.str) {
   620  		return 0, io.EOF
   621  	}
   622  	l := copy(p, []byte(r.str)[r.index:])
   623  	r.index += l
   624  	return l, nil
   625  }
   626  
   627  func (r *fakeReadCloser) Close() error {
   628  	return nil
   629  }