1package api_test
2
3import (
4	"bytes"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io/ioutil"
9	"net/http"
10	"time"
11
12	"github.com/concourse/concourse/atc"
13	"github.com/concourse/concourse/atc/db"
14	"github.com/concourse/concourse/atc/db/dbfakes"
15	. "github.com/concourse/concourse/atc/testhelpers"
16	. "github.com/onsi/ginkgo"
17	. "github.com/onsi/gomega"
18)
19
20var _ = Describe("Builds API", func() {
21
22	Describe("POST /api/v1/builds", func() {
23		var plan atc.Plan
24		var response *http.Response
25
26		BeforeEach(func() {
27			plan = atc.Plan{
28				Task: &atc.TaskPlan{
29					Config: &atc.TaskConfig{
30						Run: atc.TaskRunConfig{
31							Path: "ls",
32						},
33					},
34				},
35			}
36		})
37
38		JustBeforeEach(func() {
39			reqPayload, err := json.Marshal(plan)
40			Expect(err).NotTo(HaveOccurred())
41
42			req, err := http.NewRequest("POST", server.URL+"/api/v1/teams/some-team/builds", bytes.NewBuffer(reqPayload))
43			Expect(err).NotTo(HaveOccurred())
44
45			req.Header.Set("Content-Type", "application/json")
46
47			response, err = client.Do(req)
48			Expect(err).NotTo(HaveOccurred())
49		})
50
51		Context("when not authenticated", func() {
52			BeforeEach(func() {
53				fakeAccess.IsAuthenticatedReturns(false)
54			})
55
56			It("returns 401", func() {
57				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
58			})
59
60			It("does not trigger a build", func() {
61				Expect(dbTeam.CreateStartedBuildCallCount()).To(BeZero())
62			})
63		})
64
65		Context("when authenticated", func() {
66			BeforeEach(func() {
67				fakeAccess.IsAuthenticatedReturns(true)
68			})
69
70			Context("when not authorized", func() {
71				BeforeEach(func() {
72					fakeAccess.IsAuthorizedReturns(false)
73				})
74
75				It("returns 403", func() {
76					Expect(response.StatusCode).To(Equal(http.StatusForbidden))
77				})
78			})
79
80			Context("when authorized", func() {
81				BeforeEach(func() {
82					fakeAccess.IsAuthorizedReturns(true)
83				})
84
85				Context("when creating a started build fails", func() {
86					BeforeEach(func() {
87						dbTeam.CreateStartedBuildReturns(nil, errors.New("oh no!"))
88					})
89
90					It("returns 500 Internal Server Error", func() {
91						Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
92					})
93				})
94
95				Context("when creating a started build succeeds", func() {
96					var fakeBuild *dbfakes.FakeBuild
97
98					BeforeEach(func() {
99						fakeBuild = new(dbfakes.FakeBuild)
100						fakeBuild.IDReturns(42)
101						fakeBuild.NameReturns("1")
102						fakeBuild.TeamNameReturns("some-team")
103						fakeBuild.StatusReturns("started")
104						fakeBuild.StartTimeReturns(time.Unix(1, 0))
105						fakeBuild.EndTimeReturns(time.Unix(100, 0))
106						fakeBuild.ReapTimeReturns(time.Unix(200, 0))
107
108						dbTeam.CreateStartedBuildReturns(fakeBuild, nil)
109					})
110
111					It("returns 201 Created", func() {
112						Expect(response.StatusCode).To(Equal(http.StatusCreated))
113					})
114
115					It("returns Content-Type 'application/json'", func() {
116						expectedHeaderEntries := map[string]string{
117							"Content-Type": "application/json",
118						}
119						Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
120					})
121
122					It("creates a started build", func() {
123						Expect(dbTeam.CreateStartedBuildCallCount()).To(Equal(1))
124						Expect(dbTeam.CreateStartedBuildArgsForCall(0)).To(Equal(plan))
125					})
126
127					It("returns the created build", func() {
128						body, err := ioutil.ReadAll(response.Body)
129						Expect(err).NotTo(HaveOccurred())
130
131						Expect(body).To(MatchJSON(`{
132							"id": 42,
133							"name": "1",
134							"team_name": "some-team",
135							"status": "started",
136							"api_url": "/api/v1/builds/42",
137							"start_time": 1,
138							"end_time": 100,
139							"reap_time": 200
140						}`))
141					})
142
143				})
144			})
145		})
146	})
147
148	Describe("GET /api/v1/builds", func() {
149		var response *http.Response
150		var queryParams string
151		var returnedBuilds []db.Build
152
153		BeforeEach(func() {
154			queryParams = ""
155			build1 := new(dbfakes.FakeBuild)
156			build1.IDReturns(4)
157			build1.NameReturns("2")
158			build1.JobNameReturns("job2")
159			build1.PipelineNameReturns("pipeline2")
160			build1.TeamNameReturns("some-team")
161			build1.StatusReturns(db.BuildStatusStarted)
162			build1.StartTimeReturns(time.Unix(1, 0))
163			build1.EndTimeReturns(time.Unix(100, 0))
164			build1.ReapTimeReturns(time.Unix(300, 0))
165
166			build2 := new(dbfakes.FakeBuild)
167			build2.IDReturns(3)
168			build2.NameReturns("1")
169			build2.JobNameReturns("job1")
170			build2.PipelineNameReturns("pipeline1")
171			build2.TeamNameReturns("some-team")
172			build2.StatusReturns(db.BuildStatusSucceeded)
173			build2.StartTimeReturns(time.Unix(101, 0))
174			build2.EndTimeReturns(time.Unix(200, 0))
175			build2.ReapTimeReturns(time.Unix(400, 0))
176
177			returnedBuilds = []db.Build{build1, build2}
178			fakeAccess.TeamNamesReturns([]string{"some-team"})
179		})
180
181		JustBeforeEach(func() {
182			var err error
183
184			response, err = client.Get(server.URL + "/api/v1/builds" + queryParams)
185			Expect(err).NotTo(HaveOccurred())
186		})
187
188		Context("when not authenticated", func() {
189			BeforeEach(func() {
190				fakeAccess.IsAuthenticatedReturns(false)
191			})
192
193			Context("when no params are passed", func() {
194				BeforeEach(func() {
195					queryParams = ""
196				})
197
198				It("does not set defaults for since and until", func() {
199					Expect(dbBuildFactory.VisibleBuildsCallCount()).To(Equal(1))
200
201					teamName, page := dbBuildFactory.VisibleBuildsArgsForCall(0)
202					Expect(page).To(Equal(db.Page{
203						Limit: 100,
204					}))
205					Expect(teamName).To(ConsistOf("some-team"))
206				})
207			})
208
209			Context("when all the params are passed", func() {
210				BeforeEach(func() {
211					queryParams = "?from=2&to=3&limit=8"
212				})
213
214				It("passes them through", func() {
215					Expect(dbBuildFactory.VisibleBuildsCallCount()).To(Equal(1))
216
217					_, page := dbBuildFactory.VisibleBuildsArgsForCall(0)
218					Expect(page).To(Equal(db.Page{
219						From:  db.NewIntPtr(2),
220						To:    db.NewIntPtr(3),
221						Limit: 8,
222					}))
223				})
224
225				Context("timestamp is provided", func() {
226					BeforeEach(func() {
227						queryParams = "?timestamps=true"
228					})
229
230					It("calls AllBuilds", func() {
231						_, page := dbBuildFactory.VisibleBuildsArgsForCall(0)
232						Expect(page.UseDate).To(Equal(true))
233					})
234				})
235			})
236
237			Context("when getting the builds succeeds", func() {
238				BeforeEach(func() {
239					dbBuildFactory.VisibleBuildsReturns(returnedBuilds, db.Pagination{}, nil)
240				})
241
242				It("returns 200 OK", func() {
243					Expect(response.StatusCode).To(Equal(http.StatusOK))
244				})
245
246				It("returns Content-Type 'application/json'", func() {
247					expectedHeaderEntries := map[string]string{
248						"Content-Type": "application/json",
249					}
250					Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
251				})
252
253				It("returns all builds", func() {
254					body, err := ioutil.ReadAll(response.Body)
255					Expect(err).NotTo(HaveOccurred())
256
257					Expect(body).To(MatchJSON(`[
258						{
259							"id": 4,
260							"name": "2",
261							"job_name": "job2",
262							"pipeline_name": "pipeline2",
263							"team_name": "some-team",
264							"status": "started",
265							"api_url": "/api/v1/builds/4",
266							"start_time": 1,
267							"end_time": 100,
268							"reap_time": 300
269						},
270						{
271							"id": 3,
272							"name": "1",
273							"job_name": "job1",
274							"pipeline_name": "pipeline1",
275							"team_name": "some-team",
276							"status": "succeeded",
277							"api_url": "/api/v1/builds/3",
278							"start_time": 101,
279							"end_time": 200,
280							"reap_time": 400
281						}
282					]`))
283				})
284			})
285
286			Context("when next/previous pages are available", func() {
287				BeforeEach(func() {
288					dbBuildFactory.VisibleBuildsReturns(returnedBuilds, db.Pagination{
289						Newer: &db.Page{From: db.NewIntPtr(4), Limit: 2},
290						Older: &db.Page{To: db.NewIntPtr(3), Limit: 2},
291					}, nil)
292				})
293
294				It("returns Link headers per rfc5988", func() {
295					Expect(response.Header["Link"]).To(ConsistOf([]string{
296						fmt.Sprintf(`<%s/api/v1/builds?from=4&limit=2>; rel="previous"`, externalURL),
297						fmt.Sprintf(`<%s/api/v1/builds?to=3&limit=2>; rel="next"`, externalURL),
298					}))
299				})
300			})
301
302			Context("when getting all builds fails", func() {
303				BeforeEach(func() {
304					dbBuildFactory.VisibleBuildsReturns(nil, db.Pagination{}, errors.New("oh no!"))
305				})
306
307				It("returns 500 Internal Server Error", func() {
308					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
309				})
310			})
311		})
312
313		Context("when authenticated", func() {
314			BeforeEach(func() {
315				fakeAccess.IsAuthenticatedReturns(true)
316			})
317
318			Context("when user has the admin privilege", func() {
319				BeforeEach(func() {
320					fakeAccess.IsAdminReturns(true)
321				})
322
323				It("calls AllBuilds", func() {
324					Expect(dbBuildFactory.AllBuildsCallCount()).To(Equal(1))
325					Expect(dbBuildFactory.VisibleBuildsCallCount()).To(Equal(0))
326				})
327
328			})
329
330			Context("when no params are passed", func() {
331				BeforeEach(func() {
332					queryParams = ""
333				})
334
335				It("does not set defaults for since and until", func() {
336					Expect(dbBuildFactory.VisibleBuildsCallCount()).To(Equal(1))
337
338					_, page := dbBuildFactory.VisibleBuildsArgsForCall(0)
339					Expect(page).To(Equal(db.Page{
340						Limit: 100,
341					}))
342				})
343			})
344
345			Context("when all the params are passed", func() {
346				BeforeEach(func() {
347					queryParams = "?from=2&to=3&limit=8"
348				})
349
350				It("passes them through", func() {
351					Expect(dbBuildFactory.VisibleBuildsCallCount()).To(Equal(1))
352
353					_, page := dbBuildFactory.VisibleBuildsArgsForCall(0)
354					Expect(page).To(Equal(db.Page{
355						From:  db.NewIntPtr(2),
356						To:    db.NewIntPtr(3),
357						Limit: 8,
358					}))
359				})
360			})
361
362			Context("when getting the builds succeeds", func() {
363				BeforeEach(func() {
364					dbBuildFactory.VisibleBuildsReturns(returnedBuilds, db.Pagination{}, nil)
365				})
366
367				It("returns 200 OK", func() {
368					Expect(response.StatusCode).To(Equal(http.StatusOK))
369				})
370
371				It("returns Content-Type 'application/json'", func() {
372					expectedHeaderEntries := map[string]string{
373						"Content-Type": "application/json",
374					}
375					Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
376				})
377
378				It("returns all builds", func() {
379					body, err := ioutil.ReadAll(response.Body)
380					Expect(err).NotTo(HaveOccurred())
381
382					Expect(body).To(MatchJSON(`[
383						{
384							"id": 4,
385							"name": "2",
386							"job_name": "job2",
387							"pipeline_name": "pipeline2",
388							"team_name": "some-team",
389							"status": "started",
390							"api_url": "/api/v1/builds/4",
391							"start_time": 1,
392							"end_time": 100,
393							"reap_time": 300
394						},
395						{
396							"id": 3,
397							"name": "1",
398							"job_name": "job1",
399							"pipeline_name": "pipeline1",
400							"team_name": "some-team",
401							"status": "succeeded",
402							"api_url": "/api/v1/builds/3",
403							"start_time": 101,
404							"end_time": 200,
405							"reap_time": 400
406						}
407					]`))
408				})
409
410				It("returns builds for teams from the token", func() {
411					Expect(dbBuildFactory.VisibleBuildsCallCount()).To(Equal(1))
412					teamName, _ := dbBuildFactory.VisibleBuildsArgsForCall(0)
413					Expect(teamName).To(ConsistOf("some-team"))
414				})
415			})
416
417			Context("when next/previous pages are available", func() {
418				BeforeEach(func() {
419					dbBuildFactory.VisibleBuildsReturns(returnedBuilds, db.Pagination{
420						Newer: &db.Page{From: db.NewIntPtr(4), Limit: 2},
421						Older: &db.Page{To: db.NewIntPtr(3), Limit: 2},
422					}, nil)
423				})
424
425				It("returns Link headers per rfc5988", func() {
426					Expect(response.Header["Link"]).To(ConsistOf([]string{
427						fmt.Sprintf(`<%s/api/v1/builds?from=4&limit=2>; rel="previous"`, externalURL),
428						fmt.Sprintf(`<%s/api/v1/builds?to=3&limit=2>; rel="next"`, externalURL),
429					}))
430				})
431			})
432
433			Context("when getting all builds fails", func() {
434				BeforeEach(func() {
435					dbBuildFactory.VisibleBuildsReturns(nil, db.Pagination{}, errors.New("oh no!"))
436				})
437
438				It("returns 500 Internal Server Error", func() {
439					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
440				})
441			})
442		})
443	})
444
445	Describe("GET /api/v1/builds/:build_id", func() {
446		var response *http.Response
447
448		Context("when parsing the build_id fails", func() {
449			BeforeEach(func() {
450				var err error
451
452				response, err = client.Get(server.URL + "/api/v1/builds/nope")
453				Expect(err).NotTo(HaveOccurred())
454			})
455
456			It("returns Bad Request", func() {
457				Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
458			})
459		})
460
461		Context("when parsing the build_id succeeds", func() {
462			JustBeforeEach(func() {
463				var err error
464
465				response, err = client.Get(server.URL + "/api/v1/builds/1")
466				Expect(err).NotTo(HaveOccurred())
467			})
468
469			Context("when calling the database fails", func() {
470				BeforeEach(func() {
471					dbBuildFactory.BuildReturns(nil, false, errors.New("disaster"))
472				})
473
474				It("returns 500 Internal Server Error", func() {
475					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
476				})
477			})
478
479			Context("when the build cannot be found", func() {
480				BeforeEach(func() {
481					dbBuildFactory.BuildReturns(nil, false, nil)
482				})
483
484				It("returns Not Found", func() {
485					Expect(response.StatusCode).To(Equal(http.StatusNotFound))
486				})
487			})
488
489			Context("when the build can be found", func() {
490				BeforeEach(func() {
491					build.IDReturns(1)
492					build.NameReturns("1")
493					build.JobNameReturns("job1")
494					build.PipelineNameReturns("pipeline1")
495					build.TeamNameReturns("some-team")
496					build.StatusReturns(db.BuildStatusSucceeded)
497					build.StartTimeReturns(time.Unix(1, 0))
498					build.EndTimeReturns(time.Unix(100, 0))
499					build.ReapTimeReturns(time.Unix(200, 0))
500					dbBuildFactory.BuildReturns(build, true, nil)
501					build.PipelineReturns(fakePipeline, true, nil)
502					fakePipeline.PublicReturns(true)
503				})
504
505				Context("when not authenticated", func() {
506					BeforeEach(func() {
507						fakeAccess.IsAuthenticatedReturns(false)
508						fakeAccess.IsAuthorizedReturns(false)
509					})
510
511					Context("and build is one off", func() {
512						BeforeEach(func() {
513							build.PipelineReturns(nil, false, nil)
514						})
515
516						It("returns 401", func() {
517							Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
518						})
519					})
520
521					Context("and the pipeline is private", func() {
522						BeforeEach(func() {
523							fakePipeline.PublicReturns(false)
524							build.PipelineReturns(fakePipeline, true, nil)
525						})
526
527						It("returns 401", func() {
528							Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
529						})
530					})
531
532					Context("and the pipeline is public", func() {
533						BeforeEach(func() {
534							fakePipeline.PublicReturns(true)
535							build.PipelineReturns(fakePipeline, true, nil)
536						})
537
538						It("returns 200", func() {
539							Expect(response.StatusCode).To(Equal(http.StatusOK))
540						})
541					})
542				})
543
544				Context("when authenticated", func() {
545					BeforeEach(func() {
546						fakeAccess.IsAuthenticatedReturns(true)
547					})
548
549					Context("when user is not authorized", func() {
550						BeforeEach(func() {
551							fakeAccess.IsAuthorizedReturns(false)
552
553						})
554						It("returns 200 OK", func() {
555							Expect(response.StatusCode).To(Equal(http.StatusOK))
556						})
557					})
558
559					Context("when user is authorized", func() {
560						BeforeEach(func() {
561							fakeAccess.IsAuthorizedReturns(true)
562						})
563
564						It("returns 200 OK", func() {
565							Expect(response.StatusCode).To(Equal(http.StatusOK))
566						})
567
568						It("returns Content-Type 'application/json'", func() {
569							expectedHeaderEntries := map[string]string{
570								"Content-Type": "application/json",
571							}
572							Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
573						})
574
575						It("returns the build with the given build_id", func() {
576							Expect(dbBuildFactory.BuildCallCount()).To(Equal(1))
577							buildID := dbBuildFactory.BuildArgsForCall(0)
578							Expect(buildID).To(Equal(1))
579
580							body, err := ioutil.ReadAll(response.Body)
581							Expect(err).NotTo(HaveOccurred())
582
583							Expect(body).To(MatchJSON(`{
584						"id": 1,
585						"name": "1",
586						"status": "succeeded",
587						"job_name": "job1",
588						"pipeline_name": "pipeline1",
589						"team_name": "some-team",
590						"api_url": "/api/v1/builds/1",
591						"start_time": 1,
592						"end_time": 100,
593						"reap_time": 200
594					}`))
595						})
596					})
597				})
598			})
599		})
600	})
601
602	Describe("GET /api/v1/builds/:build_id/resources", func() {
603		var response *http.Response
604
605		Context("when the build is found", func() {
606			BeforeEach(func() {
607				build.JobNameReturns("job1")
608				build.TeamNameReturns("some-team")
609				build.PipelineReturns(fakePipeline, true, nil)
610				build.PipelineIDReturns(42)
611				dbBuildFactory.BuildReturns(build, true, nil)
612			})
613
614			JustBeforeEach(func() {
615				var err error
616
617				response, err = client.Get(server.URL + "/api/v1/builds/3/resources")
618				Expect(err).NotTo(HaveOccurred())
619			})
620
621			Context("when not authenticated", func() {
622				BeforeEach(func() {
623					fakeAccess.IsAuthenticatedReturns(false)
624				})
625
626				Context("and build is one off", func() {
627					BeforeEach(func() {
628						build.PipelineReturns(nil, false, nil)
629					})
630
631					It("returns 401", func() {
632						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
633					})
634				})
635
636				Context("and the pipeline is private", func() {
637					BeforeEach(func() {
638						fakePipeline.PublicReturns(false)
639						build.PipelineReturns(fakePipeline, true, nil)
640					})
641
642					It("returns 401", func() {
643						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
644					})
645				})
646
647				Context("and the pipeline is public", func() {
648					BeforeEach(func() {
649						fakePipeline.PublicReturns(true)
650						build.PipelineReturns(fakePipeline, true, nil)
651					})
652
653					It("returns 200", func() {
654						Expect(response.StatusCode).To(Equal(http.StatusOK))
655					})
656				})
657			})
658
659			Context("when authenticated, but not authorized", func() {
660				BeforeEach(func() {
661					fakeAccess.IsAuthenticatedReturns(true)
662					fakeAccess.IsAuthorizedReturns(false)
663				})
664
665				It("returns 403", func() {
666					Expect(response.StatusCode).To(Equal(http.StatusForbidden))
667				})
668			})
669
670			Context("when authenticated and authorized", func() {
671				BeforeEach(func() {
672					fakeAccess.IsAuthenticatedReturns(true)
673					fakeAccess.IsAuthorizedReturns(true)
674				})
675
676				It("returns 200 OK", func() {
677					Expect(response.StatusCode).To(Equal(http.StatusOK))
678				})
679
680				Context("when the build inputs/outputs are not empty", func() {
681					BeforeEach(func() {
682						build.ResourcesReturns([]db.BuildInput{
683							{
684								Name:            "input1",
685								Version:         atc.Version{"version": "value1"},
686								ResourceID:      1,
687								FirstOccurrence: true,
688							},
689							{
690								Name:            "input2",
691								Version:         atc.Version{"version": "value2"},
692								ResourceID:      2,
693								FirstOccurrence: false,
694							},
695						},
696							[]db.BuildOutput{
697								{
698									Name:    "myresource3",
699									Version: atc.Version{"version": "value3"},
700								},
701								{
702									Name:    "myresource4",
703									Version: atc.Version{"version": "value4"},
704								},
705							}, nil)
706					})
707
708					It("returns Content-Type 'application/json'", func() {
709						expectedHeaderEntries := map[string]string{
710							"Content-Type": "application/json",
711						}
712						Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
713					})
714
715					It("returns the build with it's input and output versioned resources", func() {
716						body, err := ioutil.ReadAll(response.Body)
717						Expect(err).NotTo(HaveOccurred())
718
719						Expect(body).To(MatchJSON(`{
720							"inputs": [
721								{
722									"name": "input1",
723									"version": {"version": "value1"},
724									"pipeline_id": 42,
725									"first_occurrence": true
726								},
727								{
728									"name": "input2",
729									"version": {"version": "value2"},
730									"pipeline_id": 42,
731									"first_occurrence": false
732								}
733							],
734							"outputs": [
735								{
736									"name": "myresource3",
737									"version": {"version": "value3"}
738								},
739								{
740									"name": "myresource4",
741									"version": {"version": "value4"}
742								}
743							]
744						}`))
745					})
746				})
747
748				Context("when the build resources error", func() {
749					BeforeEach(func() {
750						build.ResourcesReturns([]db.BuildInput{}, []db.BuildOutput{}, errors.New("where are my feedback?"))
751					})
752
753					It("returns internal server error", func() {
754						Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
755					})
756				})
757
758				Context("with an invalid build", func() {
759					Context("when the lookup errors", func() {
760						BeforeEach(func() {
761							dbBuildFactory.BuildReturns(build, false, errors.New("Freakin' out man, I'm freakin' out!"))
762						})
763
764						It("returns internal server error", func() {
765							Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
766						})
767					})
768
769					Context("when the build does not exist", func() {
770						BeforeEach(func() {
771							dbBuildFactory.BuildReturns(nil, false, nil)
772						})
773
774						It("returns internal server error", func() {
775							Expect(response.StatusCode).To(Equal(http.StatusNotFound))
776						})
777					})
778				})
779			})
780		})
781
782		Context("with an invalid build_id", func() {
783			JustBeforeEach(func() {
784				var err error
785
786				response, err = client.Get(server.URL + "/api/v1/builds/nope/resources")
787				Expect(err).NotTo(HaveOccurred())
788			})
789
790			It("returns internal server error", func() {
791				Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
792			})
793		})
794	})
795
796	Describe("GET /api/v1/builds/:build_id/events", func() {
797		var (
798			request  *http.Request
799			response *http.Response
800		)
801
802		BeforeEach(func() {
803			var err error
804
805			request, err = http.NewRequest("GET", server.URL+"/api/v1/builds/128/events", nil)
806			Expect(err).NotTo(HaveOccurred())
807		})
808
809		JustBeforeEach(func() {
810			var err error
811
812			response, err = client.Do(request)
813			Expect(err).NotTo(HaveOccurred())
814		})
815
816		Context("when the build can be found", func() {
817			BeforeEach(func() {
818				build.JobNameReturns("some-job")
819				build.TeamNameReturns("some-team")
820				build.PipelineReturns(fakePipeline, true, nil)
821				dbBuildFactory.BuildReturns(build, true, nil)
822			})
823
824			Context("when authenticated, but not authorized", func() {
825				BeforeEach(func() {
826					fakeAccess.IsAuthenticatedReturns(true)
827					fakeAccess.IsAuthorizedReturns(false)
828				})
829
830				It("returns 403", func() {
831					Expect(response.StatusCode).To(Equal(http.StatusForbidden))
832				})
833			})
834
835			Context("when authorized", func() {
836				BeforeEach(func() {
837					fakeAccess.IsAuthenticatedReturns(true)
838					fakeAccess.IsAuthorizedReturns(true)
839				})
840
841				It("returns 200", func() {
842					Expect(response.StatusCode).To(Equal(200))
843				})
844
845				It("serves the request via the event handler", func() {
846					body, err := ioutil.ReadAll(response.Body)
847					Expect(err).NotTo(HaveOccurred())
848
849					Expect(string(body)).To(Equal("fake event handler factory was here"))
850
851					Expect(constructedEventHandler.build).To(Equal(build))
852					Expect(dbBuildFactory.BuildCallCount()).To(Equal(1))
853					buildID := dbBuildFactory.BuildArgsForCall(0)
854					Expect(buildID).To(Equal(128))
855				})
856			})
857
858			Context("when not authenticated", func() {
859				BeforeEach(func() {
860					fakeAccess.IsAuthenticatedReturns(false)
861				})
862
863				Context("and the pipeline is private", func() {
864					BeforeEach(func() {
865						build.PipelineReturns(fakePipeline, true, nil)
866						fakePipeline.PublicReturns(false)
867					})
868
869					It("returns 401", func() {
870						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
871					})
872				})
873
874				Context("and the pipeline is public", func() {
875					BeforeEach(func() {
876						build.PipelineReturns(fakePipeline, true, nil)
877						fakePipeline.PublicReturns(true)
878					})
879
880					Context("when the job is found", func() {
881						var fakeJob *dbfakes.FakeJob
882
883						BeforeEach(func() {
884							fakeJob = new(dbfakes.FakeJob)
885							fakePipeline.JobReturns(fakeJob, true, nil)
886						})
887
888						Context("and the job is private", func() {
889							BeforeEach(func() {
890								fakeJob.PublicReturns(false)
891							})
892
893							It("returns 401", func() {
894								Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
895							})
896						})
897
898						Context("and the job is public", func() {
899							BeforeEach(func() {
900								fakeJob.PublicReturns(true)
901							})
902
903							It("returns 200", func() {
904								Expect(response.StatusCode).To(Equal(200))
905							})
906
907							It("serves the request via the event handler", func() {
908								body, err := ioutil.ReadAll(response.Body)
909								Expect(err).NotTo(HaveOccurred())
910
911								Expect(string(body)).To(Equal("fake event handler factory was here"))
912
913								Expect(constructedEventHandler.build).To(Equal(build))
914								Expect(dbBuildFactory.BuildCallCount()).To(Equal(1))
915								buildID := dbBuildFactory.BuildArgsForCall(0)
916								Expect(buildID).To(Equal(128))
917							})
918						})
919					})
920
921					Context("when finding the job fails", func() {
922						BeforeEach(func() {
923							fakePipeline.JobReturns(nil, false, errors.New("nope"))
924						})
925
926						It("returns Internal Server Error", func() {
927							Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
928						})
929					})
930
931					Context("when the job cannot be found", func() {
932						BeforeEach(func() {
933							fakePipeline.JobReturns(nil, false, nil)
934						})
935
936						It("returns Not Found", func() {
937							Expect(response.StatusCode).To(Equal(http.StatusNotFound))
938						})
939					})
940				})
941
942				Context("when the build can not be found", func() {
943					BeforeEach(func() {
944						dbBuildFactory.BuildReturns(nil, false, nil)
945					})
946
947					It("returns Not Found", func() {
948						Expect(response.StatusCode).To(Equal(http.StatusNotFound))
949					})
950				})
951
952				Context("when calling the database fails", func() {
953					BeforeEach(func() {
954						dbBuildFactory.BuildReturns(nil, false, errors.New("nope"))
955					})
956
957					It("returns Internal Server Error", func() {
958						Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
959					})
960				})
961			})
962		})
963
964		Context("when calling the database fails", func() {
965			BeforeEach(func() {
966				dbBuildFactory.BuildReturns(nil, false, errors.New("nope"))
967			})
968
969			It("returns Internal Server Error", func() {
970				Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
971			})
972		})
973	})
974
975	Describe("PUT /api/v1/builds/:build_id/abort", func() {
976		var (
977			response *http.Response
978		)
979
980		JustBeforeEach(func() {
981			var err error
982
983			req, err := http.NewRequest("PUT", server.URL+"/api/v1/builds/128/abort", nil)
984			Expect(err).NotTo(HaveOccurred())
985
986			response, err = client.Do(req)
987			Expect(err).NotTo(HaveOccurred())
988		})
989
990		Context("when not authenticated", func() {
991			BeforeEach(func() {
992				fakeAccess.IsAuthenticatedReturns(false)
993			})
994
995			It("returns 401", func() {
996				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
997			})
998		})
999
1000		Context("when authenticated", func() {
1001			BeforeEach(func() {
1002				fakeAccess.IsAuthenticatedReturns(true)
1003			})
1004
1005			Context("when looking up the build fails", func() {
1006				BeforeEach(func() {
1007					dbBuildFactory.BuildReturns(nil, false, errors.New("nope"))
1008				})
1009
1010				It("returns 500", func() {
1011					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1012				})
1013			})
1014
1015			Context("when the build can not be found", func() {
1016				BeforeEach(func() {
1017					dbBuildFactory.BuildReturns(nil, false, nil)
1018				})
1019
1020				It("returns 404", func() {
1021					Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1022				})
1023			})
1024
1025			Context("when the build is found", func() {
1026				BeforeEach(func() {
1027					build.TeamNameReturns("some-team")
1028					dbBuildFactory.BuildReturns(build, true, nil)
1029				})
1030
1031				Context("when not authorized", func() {
1032					BeforeEach(func() {
1033						fakeAccess.IsAuthorizedReturns(false)
1034					})
1035
1036					It("returns 403", func() {
1037						Expect(response.StatusCode).To(Equal(http.StatusForbidden))
1038					})
1039				})
1040
1041				Context("when authorized", func() {
1042					BeforeEach(func() {
1043						fakeAccess.IsAuthorizedReturns(true)
1044					})
1045
1046					Context("when aborting the build fails", func() {
1047						BeforeEach(func() {
1048							build.MarkAsAbortedReturns(errors.New("nope"))
1049						})
1050
1051						It("returns 500", func() {
1052							Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1053						})
1054					})
1055
1056					Context("when aborting succeeds", func() {
1057						BeforeEach(func() {
1058							build.MarkAsAbortedReturns(nil)
1059						})
1060
1061						It("returns 204", func() {
1062							Expect(response.StatusCode).To(Equal(http.StatusNoContent))
1063						})
1064					})
1065				})
1066			})
1067		})
1068	})
1069
1070	Describe("GET /api/v1/builds/:build_id/preparation", func() {
1071		var response *http.Response
1072
1073		JustBeforeEach(func() {
1074			var err error
1075			response, err = http.Get(server.URL + "/api/v1/builds/42/preparation")
1076			Expect(err).NotTo(HaveOccurred())
1077		})
1078
1079		Context("when the build is found", func() {
1080			var buildPrep db.BuildPreparation
1081
1082			BeforeEach(func() {
1083				buildPrep = db.BuildPreparation{
1084					BuildID:          42,
1085					PausedPipeline:   db.BuildPreparationStatusNotBlocking,
1086					PausedJob:        db.BuildPreparationStatusNotBlocking,
1087					MaxRunningBuilds: db.BuildPreparationStatusBlocking,
1088					Inputs: map[string]db.BuildPreparationStatus{
1089						"foo": db.BuildPreparationStatusNotBlocking,
1090						"bar": db.BuildPreparationStatusBlocking,
1091					},
1092					InputsSatisfied:     db.BuildPreparationStatusBlocking,
1093					MissingInputReasons: db.MissingInputReasons{"some-input": "some-reason"},
1094				}
1095				dbBuildFactory.BuildReturns(build, true, nil)
1096				build.JobNameReturns("job1")
1097				build.TeamNameReturns("some-team")
1098				build.PreparationReturns(buildPrep, true, nil)
1099			})
1100
1101			Context("when authenticated, but not authorized", func() {
1102				BeforeEach(func() {
1103					fakeAccess.IsAuthenticatedReturns(true)
1104					fakeAccess.IsAuthorizedReturns(false)
1105					build.PipelineReturns(fakePipeline, true, nil)
1106				})
1107
1108				It("returns 403", func() {
1109					Expect(response.StatusCode).To(Equal(http.StatusForbidden))
1110				})
1111			})
1112
1113			Context("when not authenticated", func() {
1114				BeforeEach(func() {
1115					fakeAccess.IsAuthenticatedReturns(false)
1116				})
1117
1118				Context("and build is one off", func() {
1119					BeforeEach(func() {
1120						build.PipelineReturns(nil, false, nil)
1121					})
1122
1123					It("returns 401", func() {
1124						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1125					})
1126				})
1127
1128				Context("and the pipeline is private", func() {
1129					BeforeEach(func() {
1130						build.PipelineReturns(fakePipeline, true, nil)
1131						fakePipeline.PublicReturns(false)
1132					})
1133
1134					It("returns 401", func() {
1135						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1136					})
1137				})
1138
1139				Context("and the pipeline is public", func() {
1140					BeforeEach(func() {
1141						build.PipelineReturns(fakePipeline, true, nil)
1142						fakePipeline.PublicReturns(true)
1143					})
1144
1145					Context("when the job is found", func() {
1146						var fakeJob *dbfakes.FakeJob
1147						BeforeEach(func() {
1148							fakeJob = new(dbfakes.FakeJob)
1149							fakePipeline.JobReturns(fakeJob, true, nil)
1150						})
1151
1152						Context("when job is private", func() {
1153							BeforeEach(func() {
1154								fakeJob.PublicReturns(false)
1155							})
1156
1157							It("returns 401", func() {
1158								Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1159							})
1160						})
1161
1162						Context("when job is public", func() {
1163							BeforeEach(func() {
1164								fakeJob.PublicReturns(true)
1165							})
1166
1167							It("returns 200", func() {
1168								Expect(response.StatusCode).To(Equal(http.StatusOK))
1169							})
1170						})
1171					})
1172
1173					Context("when finding the job fails", func() {
1174						BeforeEach(func() {
1175							fakePipeline.JobReturns(nil, false, errors.New("nope"))
1176						})
1177
1178						It("returns Internal Server Error", func() {
1179							Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1180						})
1181					})
1182
1183					Context("when the job cannot be found", func() {
1184						BeforeEach(func() {
1185							fakePipeline.JobReturns(nil, false, nil)
1186						})
1187
1188						It("returns Not Found", func() {
1189							Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1190						})
1191					})
1192				})
1193			})
1194
1195			Context("when authenticated", func() {
1196				BeforeEach(func() {
1197					fakeAccess.IsAuthenticatedReturns(true)
1198					fakeAccess.IsAuthorizedReturns(true)
1199				})
1200
1201				It("fetches data from the db", func() {
1202					Expect(build.PreparationCallCount()).To(Equal(1))
1203				})
1204
1205				It("returns OK", func() {
1206					Expect(response.StatusCode).To(Equal(http.StatusOK))
1207				})
1208
1209				It("returns Content-Type 'application/json'", func() {
1210					expectedHeaderEntries := map[string]string{
1211						"Content-Type": "application/json",
1212					}
1213					Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
1214				})
1215
1216				It("returns the build preparation", func() {
1217					body, err := ioutil.ReadAll(response.Body)
1218					Expect(err).NotTo(HaveOccurred())
1219
1220					Expect(body).To(MatchJSON(`{
1221					"build_id": 42,
1222					"paused_pipeline": "not_blocking",
1223					"paused_job": "not_blocking",
1224					"max_running_builds": "blocking",
1225					"inputs": {
1226						"foo": "not_blocking",
1227						"bar": "blocking"
1228					},
1229					"inputs_satisfied": "blocking",
1230					"missing_input_reasons": {
1231						"some-input": "some-reason"
1232					}
1233				}`))
1234				})
1235
1236				Context("when the build preparation is not found", func() {
1237					BeforeEach(func() {
1238						dbBuildFactory.BuildReturns(build, true, nil)
1239						build.PreparationReturns(db.BuildPreparation{}, false, nil)
1240					})
1241
1242					It("returns Not Found", func() {
1243						Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1244					})
1245				})
1246
1247				Context("when looking up the build preparation fails", func() {
1248					BeforeEach(func() {
1249						dbBuildFactory.BuildReturns(build, true, nil)
1250						build.PreparationReturns(db.BuildPreparation{}, false, errors.New("ho ho ho merry festivus"))
1251					})
1252
1253					It("returns 500 Internal Server Error", func() {
1254						Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1255					})
1256				})
1257			})
1258		})
1259
1260		Context("when looking up the build fails", func() {
1261			BeforeEach(func() {
1262				dbBuildFactory.BuildReturns(nil, false, errors.New("ho ho ho merry festivus"))
1263			})
1264
1265			It("returns 500 Internal Server Error", func() {
1266				Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1267			})
1268		})
1269
1270		Context("when build is not found", func() {
1271			BeforeEach(func() {
1272				dbBuildFactory.BuildReturns(nil, false, nil)
1273			})
1274
1275			It("returns 404", func() {
1276				Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1277			})
1278		})
1279	})
1280
1281	Describe("GET /api/v1/builds/:build_id/plan", func() {
1282		var plan *json.RawMessage
1283
1284		var response *http.Response
1285
1286		BeforeEach(func() {
1287			data := []byte(`{"some":"plan"}`)
1288			plan = (*json.RawMessage)(&data)
1289		})
1290
1291		JustBeforeEach(func() {
1292			var err error
1293			response, err = http.Get(server.URL + "/api/v1/builds/42/plan")
1294			Expect(err).NotTo(HaveOccurred())
1295		})
1296
1297		Context("when the build is found", func() {
1298			BeforeEach(func() {
1299				build.JobNameReturns("job1")
1300				build.TeamNameReturns("some-team")
1301				dbBuildFactory.BuildReturns(build, true, nil)
1302			})
1303
1304			Context("when authenticated, but not authorized", func() {
1305				BeforeEach(func() {
1306					fakeAccess.IsAuthenticatedReturns(true)
1307					fakeAccess.IsAuthorizedReturns(false)
1308
1309					build.PipelineReturns(fakePipeline, true, nil)
1310				})
1311
1312				It("returns 403", func() {
1313					Expect(response.StatusCode).To(Equal(http.StatusForbidden))
1314				})
1315			})
1316
1317			Context("when not authenticated", func() {
1318				BeforeEach(func() {
1319					fakeAccess.IsAuthenticatedReturns(false)
1320				})
1321
1322				Context("and build is one off", func() {
1323					BeforeEach(func() {
1324						build.PipelineReturns(nil, false, nil)
1325					})
1326
1327					It("returns 401", func() {
1328						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1329					})
1330				})
1331
1332				Context("and the pipeline is private", func() {
1333					BeforeEach(func() {
1334						build.PipelineReturns(fakePipeline, true, nil)
1335						fakePipeline.PublicReturns(false)
1336					})
1337
1338					It("returns 401", func() {
1339						Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1340					})
1341				})
1342
1343				Context("and the pipeline is public", func() {
1344					BeforeEach(func() {
1345						build.PipelineReturns(fakePipeline, true, nil)
1346						fakePipeline.PublicReturns(true)
1347					})
1348
1349					Context("when finding the job fails", func() {
1350						BeforeEach(func() {
1351							fakePipeline.JobReturns(nil, false, errors.New("nope"))
1352						})
1353						It("returns 500", func() {
1354							Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1355						})
1356					})
1357
1358					Context("when the job does not exist", func() {
1359						BeforeEach(func() {
1360							fakePipeline.JobReturns(nil, false, nil)
1361						})
1362						It("returns 404", func() {
1363							Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1364						})
1365					})
1366
1367					Context("when the job exists", func() {
1368						var fakeJob *dbfakes.FakeJob
1369
1370						BeforeEach(func() {
1371							fakeJob = new(dbfakes.FakeJob)
1372							fakePipeline.JobReturns(fakeJob, true, nil)
1373						})
1374
1375						Context("and the job is public", func() {
1376							BeforeEach(func() {
1377								fakeJob.PublicReturns(true)
1378							})
1379							Context("and the build has a plan", func() {
1380								BeforeEach(func() {
1381									build.HasPlanReturns(true)
1382								})
1383								It("returns 200", func() {
1384									Expect(response.StatusCode).To(Equal(http.StatusOK))
1385								})
1386							})
1387							Context("and the build has no plan", func() {
1388								BeforeEach(func() {
1389									build.HasPlanReturns(false)
1390								})
1391								It("returns 404", func() {
1392									Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1393								})
1394							})
1395						})
1396
1397						Context("and the job is private", func() {
1398							BeforeEach(func() {
1399								fakeJob.PublicReturns(false)
1400							})
1401							It("returns 401", func() {
1402								Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1403							})
1404						})
1405					})
1406				})
1407			})
1408
1409			Context("when authenticated", func() {
1410				BeforeEach(func() {
1411					fakeAccess.IsAuthenticatedReturns(true)
1412					fakeAccess.IsAuthorizedReturns(true)
1413				})
1414
1415				Context("when the build returns a plan", func() {
1416					BeforeEach(func() {
1417						build.HasPlanReturns(true)
1418						build.PublicPlanReturns(plan)
1419						build.SchemaReturns("some-schema")
1420					})
1421
1422					It("returns OK", func() {
1423						Expect(response.StatusCode).To(Equal(http.StatusOK))
1424					})
1425
1426					It("returns Content-Type 'application/json'", func() {
1427						expectedHeaderEntries := map[string]string{
1428							"Content-Type": "application/json",
1429						}
1430						Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
1431					})
1432
1433					It("returns the plan", func() {
1434						body, err := ioutil.ReadAll(response.Body)
1435						Expect(err).NotTo(HaveOccurred())
1436
1437						Expect(body).To(MatchJSON(`{
1438						"schema": "some-schema",
1439						"plan": {"some":"plan"}
1440					}`))
1441					})
1442				})
1443
1444				Context("when the build has no plan", func() {
1445					BeforeEach(func() {
1446						build.HasPlanReturns(false)
1447					})
1448
1449					It("returns no Content-Type header", func() {
1450						expectedHeaderEntries := map[string]string{
1451							"Content-Type": "",
1452						}
1453						Expect(response).ShouldNot(IncludeHeaderEntries(expectedHeaderEntries))
1454					})
1455
1456					It("returns not found", func() {
1457						Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1458					})
1459				})
1460			})
1461		})
1462
1463		Context("when the build is not found", func() {
1464			BeforeEach(func() {
1465				dbBuildFactory.BuildReturns(nil, false, nil)
1466			})
1467
1468			It("returns Not Found", func() {
1469				Expect(response.StatusCode).To(Equal(http.StatusNotFound))
1470			})
1471		})
1472
1473		Context("when looking up the build fails", func() {
1474			BeforeEach(func() {
1475				dbBuildFactory.BuildReturns(nil, false, errors.New("oh no!"))
1476			})
1477
1478			It("returns 500 Internal Server Error", func() {
1479				Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
1480			})
1481		})
1482	})
1483})
1484