1package api_test
2
3import (
4	"bytes"
5	"encoding/json"
6	"errors"
7	"io/ioutil"
8	"net/http"
9	"time"
10
11	"github.com/concourse/concourse/atc"
12	"github.com/concourse/concourse/atc/db"
13	"github.com/concourse/concourse/atc/db/dbfakes"
14	. "github.com/concourse/concourse/atc/testhelpers"
15	. "github.com/onsi/ginkgo"
16	. "github.com/onsi/gomega"
17)
18
19var _ = Describe("Workers API", func() {
20
21	Describe("GET /api/v1/workers", func() {
22		var response *http.Response
23
24		JustBeforeEach(func() {
25			req, err := http.NewRequest("GET", server.URL+"/api/v1/workers", nil)
26			Expect(err).NotTo(HaveOccurred())
27
28			response, err = client.Do(req)
29			Expect(err).NotTo(HaveOccurred())
30		})
31
32		Context("when authenticated", func() {
33			var (
34				teamWorker1 *dbfakes.FakeWorker
35				teamWorker2 *dbfakes.FakeWorker
36			)
37
38			BeforeEach(func() {
39				fakeAccess.IsAuthenticatedReturns(true)
40				fakeAccess.IsAuthorizedReturns(true)
41				fakeAccess.TeamNamesReturns([]string{"some-team"})
42				dbWorkerFactory.VisibleWorkersReturns(nil, nil)
43
44				teamWorker1 = new(dbfakes.FakeWorker)
45				gardenAddr1 := "1.2.3.4:7777"
46				teamWorker1.GardenAddrReturns(&gardenAddr1)
47				bcURL1 := "1.2.3.4:8888"
48				teamWorker1.BaggageclaimURLReturns(&bcURL1)
49
50				teamWorker2 = new(dbfakes.FakeWorker)
51				gardenAddr2 := "5.6.7.8:7777"
52				teamWorker2.GardenAddrReturns(&gardenAddr2)
53				bcURL2 := "5.6.7.8:8888"
54				teamWorker2.BaggageclaimURLReturns(&bcURL2)
55			})
56
57			It("fetches workers by team name from worker user context", func() {
58				Expect(dbWorkerFactory.VisibleWorkersCallCount()).To(Equal(1))
59
60				teamNames := dbWorkerFactory.VisibleWorkersArgsForCall(0)
61				Expect(teamNames).To(ConsistOf("some-team"))
62			})
63
64			Context("when user is an admin", func() {
65				BeforeEach(func() {
66					fakeAccess.IsAdminReturns(true)
67					dbWorkerFactory.WorkersReturns([]db.Worker{
68						teamWorker1,
69						teamWorker2,
70					}, nil)
71				})
72
73				It("returns all the workers", func() {
74					Expect(response.StatusCode).To(Equal(http.StatusOK))
75					expectedHeaderEntries := map[string]string{
76						"Content-Type": "application/json",
77					}
78					Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
79
80					Expect(dbWorkerFactory.WorkersCallCount()).To(Equal(1))
81
82					var returnedWorkers []atc.Worker
83					err := json.NewDecoder(response.Body).Decode(&returnedWorkers)
84					Expect(err).NotTo(HaveOccurred())
85
86					Expect(returnedWorkers).To(Equal([]atc.Worker{
87						{
88							GardenAddr:      "1.2.3.4:7777",
89							BaggageclaimURL: "1.2.3.4:8888",
90						},
91						{
92							GardenAddr:      "5.6.7.8:7777",
93							BaggageclaimURL: "5.6.7.8:8888",
94						},
95					}))
96				})
97			})
98
99			Context("when the workers can be listed", func() {
100				BeforeEach(func() {
101					dbWorkerFactory.VisibleWorkersReturns([]db.Worker{
102						teamWorker1,
103						teamWorker2,
104					}, nil)
105				})
106
107				It("returns 200", func() {
108					Expect(response.StatusCode).To(Equal(http.StatusOK))
109				})
110
111				It("returns Content-Type 'application/json'", func() {
112					expectedHeaderEntries := map[string]string{
113						"Content-Type": "application/json",
114					}
115					Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
116				})
117
118				It("returns the workers", func() {
119					var returnedWorkers []atc.Worker
120					err := json.NewDecoder(response.Body).Decode(&returnedWorkers)
121					Expect(err).NotTo(HaveOccurred())
122
123					Expect(dbWorkerFactory.VisibleWorkersCallCount()).To(Equal(1))
124
125					Expect(returnedWorkers).To(Equal([]atc.Worker{
126						{
127							GardenAddr:      "1.2.3.4:7777",
128							BaggageclaimURL: "1.2.3.4:8888",
129						},
130						{
131							GardenAddr:      "5.6.7.8:7777",
132							BaggageclaimURL: "5.6.7.8:8888",
133						},
134					}))
135
136				})
137			})
138
139			Context("when getting the workers fails", func() {
140				BeforeEach(func() {
141					dbWorkerFactory.VisibleWorkersReturns(nil, errors.New("error!"))
142				})
143
144				It("returns 500", func() {
145					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
146				})
147			})
148		})
149
150		Context("when not authenticated", func() {
151			BeforeEach(func() {
152				fakeAccess.IsAuthenticatedReturns(false)
153			})
154
155			It("returns 401", func() {
156				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
157			})
158		})
159	})
160
161	Describe("POST /api/v1/workers", func() {
162		var (
163			worker    atc.Worker
164			ttl       string
165			certsPath string
166
167			response *http.Response
168		)
169
170		BeforeEach(func() {
171			certsPath = "/some/certs/path"
172			worker = atc.Worker{
173				Name:             "worker-name",
174				GardenAddr:       "1.2.3.4:7777",
175				BaggageclaimURL:  "5.6.7.8:7788",
176				CertsPath:        &certsPath,
177				HTTPProxyURL:     "http://example.com",
178				HTTPSProxyURL:    "https://example.com",
179				NoProxy:          "example.com,127.0.0.1,localhost",
180				ActiveContainers: 2,
181				ActiveVolumes:    10,
182				ActiveTasks:      42,
183				ResourceTypes: []atc.WorkerResourceType{
184					{Type: "some-resource", Image: "some-resource-image"},
185				},
186				Platform: "haiku",
187				Tags:     []string{"not", "a", "limerick"},
188				Version:  "1.2.3",
189			}
190
191			ttl = "30s"
192			fakeAccess.IsAuthorizedReturns(true)
193			fakeAccess.IsSystemReturns(true)
194		})
195
196		JustBeforeEach(func() {
197			payload, err := json.Marshal(worker)
198			Expect(err).NotTo(HaveOccurred())
199
200			req, err := http.NewRequest("POST", server.URL+"/api/v1/workers?ttl="+ttl, ioutil.NopCloser(bytes.NewBuffer(payload)))
201			Expect(err).NotTo(HaveOccurred())
202
203			response, err = client.Do(req)
204			Expect(err).NotTo(HaveOccurred())
205		})
206
207		Context("when authenticated", func() {
208			BeforeEach(func() {
209				fakeAccess.IsAuthenticatedReturns(true)
210			})
211
212			It("tries to save the worker", func() {
213				Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1))
214				savedWorker, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0)
215				Expect(savedWorker).To(Equal(atc.Worker{
216					GardenAddr:       "1.2.3.4:7777",
217					Name:             "worker-name",
218					BaggageclaimURL:  "5.6.7.8:7788",
219					CertsPath:        &certsPath,
220					HTTPProxyURL:     "http://example.com",
221					HTTPSProxyURL:    "https://example.com",
222					NoProxy:          "example.com,127.0.0.1,localhost",
223					ActiveContainers: 2,
224					ActiveVolumes:    10,
225					ActiveTasks:      42,
226					ResourceTypes: []atc.WorkerResourceType{
227						{Type: "some-resource", Image: "some-resource-image"},
228					},
229					Platform: "haiku",
230					Tags:     []string{"not", "a", "limerick"},
231					Version:  "1.2.3",
232				}))
233
234				Expect(savedTTL.String()).To(Equal(ttl))
235			})
236
237			Context("when request is not from tsa", func() {
238				Context("when system claim is false", func() {
239					BeforeEach(func() {
240						fakeAccess.IsSystemReturns(false)
241					})
242
243					It("return 403", func() {
244						Expect(response.StatusCode).To(Equal(http.StatusForbidden))
245					})
246				})
247			})
248
249			Context("when payload contains team name", func() {
250				BeforeEach(func() {
251					worker.Team = "some-team"
252				})
253
254				Context("when specified team exists", func() {
255					var foundTeam *dbfakes.FakeTeam
256
257					BeforeEach(func() {
258						foundTeam = new(dbfakes.FakeTeam)
259						dbTeamFactory.FindTeamReturns(foundTeam, true, nil)
260					})
261
262					It("saves team name in db", func() {
263						Expect(foundTeam.SaveWorkerCallCount()).To(Equal(1))
264					})
265
266					Context("when saving the worker succeeds", func() {
267						BeforeEach(func() {
268							foundTeam.SaveWorkerReturns(new(dbfakes.FakeWorker), nil)
269						})
270
271						It("returns 200", func() {
272							Expect(response.StatusCode).To(Equal(http.StatusOK))
273						})
274					})
275
276					Context("when saving the worker fails", func() {
277						BeforeEach(func() {
278							foundTeam.SaveWorkerReturns(nil, errors.New("oh no!"))
279						})
280
281						It("returns 500", func() {
282							Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
283						})
284					})
285				})
286
287				Context("when specified team does not exist", func() {
288					BeforeEach(func() {
289						dbTeamFactory.FindTeamReturns(nil, false, nil)
290					})
291
292					It("returns 400", func() {
293						Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
294					})
295				})
296			})
297
298			Context("when the worker has no name", func() {
299				BeforeEach(func() {
300					worker.Name = ""
301				})
302
303				It("tries to save the worker with the garden address as the name", func() {
304					Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1))
305
306					savedInfo, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0)
307					Expect(savedInfo).To(Equal(atc.Worker{
308						GardenAddr:       "1.2.3.4:7777",
309						Name:             "1.2.3.4:7777",
310						BaggageclaimURL:  "5.6.7.8:7788",
311						CertsPath:        &certsPath,
312						HTTPProxyURL:     "http://example.com",
313						HTTPSProxyURL:    "https://example.com",
314						NoProxy:          "example.com,127.0.0.1,localhost",
315						ActiveContainers: 2,
316						ActiveVolumes:    10,
317						ActiveTasks:      42,
318						ResourceTypes: []atc.WorkerResourceType{
319							{Type: "some-resource", Image: "some-resource-image"},
320						},
321						Platform: "haiku",
322						Tags:     []string{"not", "a", "limerick"},
323						Version:  "1.2.3",
324					}))
325
326					Expect(savedTTL.String()).To(Equal(ttl))
327				})
328			})
329
330			Context("when the certs path is null", func() {
331				BeforeEach(func() {
332					worker.CertsPath = nil
333				})
334
335				It("saves the worker with a null certs path", func() {
336					Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1))
337
338					savedInfo, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0)
339					Expect(savedInfo).To(Equal(atc.Worker{
340						GardenAddr:       "1.2.3.4:7777",
341						Name:             "worker-name",
342						BaggageclaimURL:  "5.6.7.8:7788",
343						CertsPath:        nil,
344						HTTPProxyURL:     "http://example.com",
345						HTTPSProxyURL:    "https://example.com",
346						NoProxy:          "example.com,127.0.0.1,localhost",
347						ActiveContainers: 2,
348						ActiveVolumes:    10,
349						ActiveTasks:      42,
350						ResourceTypes: []atc.WorkerResourceType{
351							{Type: "some-resource", Image: "some-resource-image"},
352						},
353						Platform: "haiku",
354						Tags:     []string{"not", "a", "limerick"},
355						Version:  "1.2.3",
356					}))
357
358					Expect(savedTTL.String()).To(Equal(ttl))
359				})
360			})
361
362			Context("when the certs path is an empty string", func() {
363				BeforeEach(func() {
364					emptyString := ""
365					worker.CertsPath = &emptyString
366				})
367
368				It("saves the worker with a null certs path", func() {
369					Expect(dbWorkerFactory.SaveWorkerCallCount()).To(Equal(1))
370
371					savedInfo, savedTTL := dbWorkerFactory.SaveWorkerArgsForCall(0)
372					Expect(savedInfo).To(Equal(atc.Worker{
373						GardenAddr:       "1.2.3.4:7777",
374						Name:             "worker-name",
375						BaggageclaimURL:  "5.6.7.8:7788",
376						CertsPath:        nil,
377						HTTPProxyURL:     "http://example.com",
378						HTTPSProxyURL:    "https://example.com",
379						NoProxy:          "example.com,127.0.0.1,localhost",
380						ActiveContainers: 2,
381						ActiveVolumes:    10,
382						ActiveTasks:      42,
383						ResourceTypes: []atc.WorkerResourceType{
384							{Type: "some-resource", Image: "some-resource-image"},
385						},
386						Platform: "haiku",
387						Tags:     []string{"not", "a", "limerick"},
388						Version:  "1.2.3",
389					}))
390
391					Expect(savedTTL.String()).To(Equal(ttl))
392				})
393			})
394
395			Context("when saving the worker succeeds", func() {
396				var fakeWorker *dbfakes.FakeWorker
397				BeforeEach(func() {
398					fakeWorker = new(dbfakes.FakeWorker)
399					dbWorkerFactory.SaveWorkerReturns(fakeWorker, nil)
400				})
401
402				It("returns 200", func() {
403					Expect(response.StatusCode).To(Equal(http.StatusOK))
404				})
405
406			})
407
408			Context("when saving the worker fails", func() {
409				BeforeEach(func() {
410					dbWorkerFactory.SaveWorkerReturns(nil, errors.New("oh no!"))
411				})
412
413				It("returns 500", func() {
414					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
415				})
416			})
417
418			Context("when the TTL is invalid", func() {
419				BeforeEach(func() {
420					ttl = "invalid-duration"
421				})
422
423				It("returns 400", func() {
424					Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
425				})
426
427				It("returns the validation error in the response body", func() {
428					Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("malformed ttl")))
429				})
430
431				It("does not save it", func() {
432					Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero())
433				})
434			})
435
436			Context("when the worker has no address", func() {
437				BeforeEach(func() {
438					worker.GardenAddr = ""
439				})
440
441				It("returns 400", func() {
442					Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
443				})
444
445				It("returns the validation error in the response body", func() {
446					Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("missing garden address")))
447				})
448
449				It("does not save it", func() {
450					Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero())
451				})
452			})
453
454			Context("when worker version is invalid", func() {
455				BeforeEach(func() {
456					worker.Version = "invalid"
457				})
458
459				It("returns 400", func() {
460					Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
461				})
462
463				It("returns the validation error in the response body", func() {
464					Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("invalid worker version, only numeric characters are allowed")))
465				})
466
467				It("does not save it", func() {
468					Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero())
469				})
470			})
471		})
472
473		Context("when not authenticated", func() {
474			BeforeEach(func() {
475				fakeAccess.IsAuthenticatedReturns(false)
476			})
477
478			It("returns 401", func() {
479				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
480			})
481
482			It("does not save the config", func() {
483				Expect(dbWorkerFactory.SaveWorkerCallCount()).To(BeZero())
484			})
485		})
486	})
487
488	Describe("PUT /api/v1/workers/:worker_name/land", func() {
489		var (
490			response   *http.Response
491			workerName string
492			fakeWorker *dbfakes.FakeWorker
493		)
494
495		JustBeforeEach(func() {
496			req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/land", nil)
497			Expect(err).NotTo(HaveOccurred())
498
499			response, err = client.Do(req)
500			Expect(err).NotTo(HaveOccurred())
501		})
502
503		BeforeEach(func() {
504			fakeWorker = new(dbfakes.FakeWorker)
505			workerName = "some-worker"
506			fakeWorker.NameReturns(workerName)
507			fakeWorker.TeamNameReturns("some-team")
508			fakeWorker.LandReturns(nil)
509
510			fakeAccess.IsAuthenticatedReturns(true)
511			dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil)
512		})
513
514		Context("when the request is authenticated as system", func() {
515			BeforeEach(func() {
516				fakeAccess.IsSystemReturns(true)
517			})
518
519			It("returns 200", func() {
520				Expect(response.StatusCode).To(Equal(http.StatusOK))
521			})
522
523			It("sees if the worker exists and attempts to land it", func() {
524				Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1))
525				Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName))
526				Expect(fakeWorker.LandCallCount()).To(Equal(1))
527			})
528
529			Context("when landing the worker fails", func() {
530				var returnedErr error
531
532				BeforeEach(func() {
533					returnedErr = errors.New("some-error")
534					fakeWorker.LandReturns(returnedErr)
535				})
536
537				It("returns 500", func() {
538					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
539				})
540			})
541
542			Context("when the worker does not exist", func() {
543				BeforeEach(func() {
544					dbWorkerFactory.GetWorkerReturns(nil, false, nil)
545				})
546
547				It("returns 404", func() {
548					Expect(response.StatusCode).To(Equal(http.StatusNotFound))
549				})
550			})
551		})
552
553		Context("when the request is authorized as the worker's owner", func() {
554			BeforeEach(func() {
555				fakeAccess.IsAuthorizedReturns(true)
556			})
557
558			It("returns 200", func() {
559				Expect(response.StatusCode).To(Equal(http.StatusOK))
560			})
561		})
562
563		Context("when the request is authorized as the wrong team", func() {
564			BeforeEach(func() {
565				fakeAccess.IsAuthorizedReturns(false)
566			})
567
568			It("returns 403", func() {
569				Expect(response.StatusCode).To(Equal(http.StatusForbidden))
570			})
571		})
572
573		Context("when not authenticated", func() {
574			BeforeEach(func() {
575				fakeAccess.IsAuthenticatedReturns(false)
576			})
577
578			It("returns 401", func() {
579				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
580			})
581
582			It("does not attempt to find the worker", func() {
583				Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero())
584			})
585		})
586	})
587
588	Describe("PUT /api/v1/workers/:worker_name/retire", func() {
589		var (
590			response   *http.Response
591			workerName string
592			fakeWorker *dbfakes.FakeWorker
593		)
594
595		JustBeforeEach(func() {
596			req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/retire", nil)
597			Expect(err).NotTo(HaveOccurred())
598
599			response, err = client.Do(req)
600			Expect(err).NotTo(HaveOccurred())
601		})
602
603		BeforeEach(func() {
604			fakeWorker = new(dbfakes.FakeWorker)
605			workerName = "some-worker"
606			fakeWorker.NameReturns(workerName)
607			fakeWorker.TeamNameReturns("some-team")
608			fakeAccess.IsAuthenticatedReturns(true)
609
610			dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil)
611			fakeWorker.RetireReturns(nil)
612		})
613
614		Context("when autheticated as system", func() {
615			BeforeEach(func() {
616				fakeAccess.IsSystemReturns(true)
617			})
618
619			It("returns 200", func() {
620				Expect(response.StatusCode).To(Equal(http.StatusOK))
621			})
622
623			It("sees if the worker exists and attempts to retire it", func() {
624				Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1))
625				Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName))
626
627				Expect(fakeWorker.RetireCallCount()).To(Equal(1))
628			})
629
630			Context("when retiring the worker fails", func() {
631				var returnedErr error
632
633				BeforeEach(func() {
634					returnedErr = errors.New("some-error")
635					fakeWorker.RetireReturns(returnedErr)
636				})
637
638				It("returns 500", func() {
639					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
640				})
641			})
642
643			Context("when the worker does not exist", func() {
644				BeforeEach(func() {
645					dbWorkerFactory.GetWorkerReturns(nil, false, nil)
646				})
647
648				It("returns 404", func() {
649					Expect(response.StatusCode).To(Equal(http.StatusNotFound))
650				})
651			})
652		})
653
654		Context("when authorized as as the worker's owner", func() {
655			BeforeEach(func() {
656				fakeAccess.IsAuthorizedReturns(true)
657			})
658
659			It("returns 200", func() {
660				Expect(response.StatusCode).To(Equal(http.StatusOK))
661			})
662		})
663
664		Context("when authorized as some other team", func() {
665			BeforeEach(func() {
666				fakeAccess.IsAuthorizedReturns(false)
667			})
668
669			It("returns 403", func() {
670				Expect(response.StatusCode).To(Equal(http.StatusForbidden))
671			})
672		})
673
674		Context("when not authenticated", func() {
675			BeforeEach(func() {
676				fakeAccess.IsAuthenticatedReturns(false)
677			})
678
679			It("returns 401", func() {
680				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
681			})
682
683			It("does not attempt to find the worker", func() {
684				Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero())
685			})
686		})
687	})
688
689	Describe("PUT /api/v1/workers/:worker_name/prune", func() {
690		var (
691			response   *http.Response
692			workerName string
693			fakeWorker *dbfakes.FakeWorker
694		)
695
696		JustBeforeEach(func() {
697			req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/prune", nil)
698			Expect(err).NotTo(HaveOccurred())
699
700			response, err = client.Do(req)
701			Expect(err).NotTo(HaveOccurred())
702		})
703
704		BeforeEach(func() {
705			fakeWorker = new(dbfakes.FakeWorker)
706			workerName = "some-worker"
707			fakeWorker.NameReturns(workerName)
708			fakeWorker.TeamNameReturns("some-team")
709
710			dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil)
711			fakeAccess.IsAuthenticatedReturns(true)
712			fakeAccess.IsAuthorizedReturns(true)
713			fakeWorker.PruneReturns(nil)
714		})
715
716		It("returns 200", func() {
717			Expect(response.StatusCode).To(Equal(http.StatusOK))
718		})
719
720		It("sees if the worker exists and attempts to prune it", func() {
721			Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName))
722			Expect(fakeWorker.PruneCallCount()).To(Equal(1))
723		})
724
725		Context("when pruning the worker fails", func() {
726			var returnedErr error
727
728			BeforeEach(func() {
729				returnedErr = errors.New("some-error")
730				fakeWorker.PruneReturns(returnedErr)
731			})
732
733			It("returns 500", func() {
734				Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
735			})
736		})
737
738		Context("when the worker does not exist", func() {
739			BeforeEach(func() {
740				dbWorkerFactory.GetWorkerReturns(nil, false, nil)
741			})
742
743			It("returns 404", func() {
744				Expect(response.StatusCode).To(Equal(http.StatusNotFound))
745			})
746		})
747
748		Context("when the worker is running", func() {
749			BeforeEach(func() {
750				fakeWorker.PruneReturns(db.ErrCannotPruneRunningWorker)
751			})
752
753			It("returns 400", func() {
754				Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
755				Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"stderr":"cannot prune running worker"}`))
756			})
757		})
758
759		Context("when not authenticated", func() {
760			BeforeEach(func() {
761				fakeAccess.IsAuthenticatedReturns(false)
762			})
763
764			It("returns 401", func() {
765				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
766			})
767
768			It("does not attempt to find the worker", func() {
769				Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero())
770			})
771		})
772	})
773
774	Describe("PUT /api/v1/workers/:worker_name/heartbeat", func() {
775		var (
776			response   *http.Response
777			workerName string
778			ttlStr     string
779			ttl        time.Duration
780			err        error
781
782			worker     atc.Worker
783			fakeWorker *dbfakes.FakeWorker
784		)
785
786		BeforeEach(func() {
787			fakeWorker = new(dbfakes.FakeWorker)
788			workerName = "some-name"
789			fakeWorker.NameReturns(workerName)
790			fakeWorker.ActiveContainersReturns(2)
791			fakeWorker.ActiveVolumesReturns(10)
792			fakeWorker.ActiveTasksReturns(42, nil)
793			fakeWorker.PlatformReturns("penguin")
794			fakeWorker.TagsReturns([]string{"some-tag"})
795			fakeWorker.StateReturns(db.WorkerStateRunning)
796			fakeWorker.TeamNameReturns("some-team")
797			fakeWorker.EphemeralReturns(true)
798
799			ttlStr = "30s"
800			ttl, err = time.ParseDuration(ttlStr)
801			Expect(err).NotTo(HaveOccurred())
802
803			worker = atc.Worker{
804				Name:             workerName,
805				ActiveContainers: 2,
806			}
807			fakeAccess.IsAuthenticatedReturns(true)
808			dbWorkerFactory.HeartbeatWorkerReturns(fakeWorker, nil)
809		})
810
811		JustBeforeEach(func() {
812			payload, err := json.Marshal(worker)
813			Expect(err).NotTo(HaveOccurred())
814
815			req, err := http.NewRequest("PUT", server.URL+"/api/v1/workers/"+workerName+"/heartbeat?ttl="+ttlStr, ioutil.NopCloser(bytes.NewBuffer(payload)))
816			Expect(err).NotTo(HaveOccurred())
817
818			response, err = client.Do(req)
819			Expect(err).NotTo(HaveOccurred())
820		})
821
822		It("returns 200", func() {
823			Expect(response.StatusCode).To(Equal(http.StatusOK))
824		})
825
826		It("returns Content-Type 'application/json'", func() {
827			expectedHeaderEntries := map[string]string{
828				"Content-Type": "application/json",
829			}
830			Expect(response).Should(IncludeHeaderEntries(expectedHeaderEntries))
831		})
832
833		It("returns saved worker", func() {
834			contents, err := ioutil.ReadAll(response.Body)
835			Expect(err).NotTo(HaveOccurred())
836
837			Expect(contents).To(MatchJSON(`{
838				"name": "some-name",
839				"state": "running",
840				"addr": "",
841				"baggageclaim_url": "",
842				"active_containers": 2,
843				"active_volumes": 10,
844				"active_tasks": 42,
845				"resource_types": null,
846				"platform": "penguin",
847				"ephemeral": true,
848				"tags": ["some-tag"],
849				"team": "some-team",
850				"start_time": 0,
851				"version": ""
852			}`))
853		})
854
855		It("sees if the worker exists and attempts to heartbeat with provided ttl", func() {
856			Expect(dbWorkerFactory.HeartbeatWorkerCallCount()).To(Equal(1))
857
858			w, t := dbWorkerFactory.HeartbeatWorkerArgsForCall(0)
859			Expect(w).To(Equal(worker))
860			Expect(t).To(Equal(ttl))
861		})
862
863		Context("when the TTL is invalid", func() {
864			BeforeEach(func() {
865				ttlStr = "invalid-duration"
866			})
867
868			It("returns 400", func() {
869				Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
870			})
871
872			It("returns the validation error in the response body", func() {
873				Expect(ioutil.ReadAll(response.Body)).To(Equal([]byte("malformed ttl")))
874			})
875
876			It("does not heartbeat worker", func() {
877				Expect(dbWorkerFactory.HeartbeatWorkerCallCount()).To(BeZero())
878			})
879		})
880
881		Context("when heartbeating the worker fails", func() {
882			var returnedErr error
883
884			BeforeEach(func() {
885				returnedErr = errors.New("some-error")
886				dbWorkerFactory.HeartbeatWorkerReturns(nil, returnedErr)
887			})
888
889			It("returns 500", func() {
890				Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
891			})
892		})
893
894		Context("when the worker does not exist", func() {
895			BeforeEach(func() {
896				dbWorkerFactory.HeartbeatWorkerReturns(nil, db.ErrWorkerNotPresent)
897			})
898
899			It("returns 404", func() {
900				Expect(response.StatusCode).To(Equal(http.StatusNotFound))
901			})
902		})
903
904		Context("when not authenticated", func() {
905			BeforeEach(func() {
906				fakeAccess.IsAuthenticatedReturns(false)
907			})
908
909			It("returns 401", func() {
910				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
911			})
912
913			It("does not heartbeat the worker", func() {
914				Expect(dbWorkerFactory.HeartbeatWorkerCallCount()).To(BeZero())
915			})
916		})
917	})
918
919	Describe("DELETE /api/v1/workers/:worker_name", func() {
920		var (
921			response   *http.Response
922			workerName string
923			fakeWorker *dbfakes.FakeWorker
924		)
925
926		JustBeforeEach(func() {
927			req, err := http.NewRequest("DELETE", server.URL+"/api/v1/workers/"+workerName, nil)
928			Expect(err).NotTo(HaveOccurred())
929
930			response, err = client.Do(req)
931			Expect(err).NotTo(HaveOccurred())
932		})
933
934		BeforeEach(func() {
935			fakeWorker = new(dbfakes.FakeWorker)
936			workerName = "some-worker"
937			fakeWorker.NameReturns(workerName)
938
939			fakeAccess.IsAuthenticatedReturns(true)
940			fakeWorker.DeleteReturns(nil)
941			dbWorkerFactory.GetWorkerReturns(fakeWorker, true, nil)
942		})
943
944		Context("when user is system user", func() {
945			BeforeEach(func() {
946				fakeAccess.IsSystemReturns(true)
947			})
948			It("deletes the worker from the DB", func() {
949				Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1))
950				Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName))
951
952				Expect(fakeWorker.DeleteCallCount()).To(Equal(1))
953			})
954			It("returns 200", func() {
955				Expect(response.StatusCode).To(Equal(http.StatusOK))
956			})
957
958			Context("when the given worker has already been deleted", func() {
959				BeforeEach(func() {
960					dbWorkerFactory.GetWorkerReturns(nil, false, nil)
961				})
962				It("returns 500", func() {
963					Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1))
964					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
965				})
966			})
967
968			Context("when deleting the worker fails", func() {
969				var returnedErr error
970
971				BeforeEach(func() {
972					returnedErr = errors.New("some-error")
973					fakeWorker.DeleteReturns(returnedErr)
974				})
975
976				It("returns 500", func() {
977					Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
978				})
979			})
980		})
981
982		Context("when user is admin user", func() {
983			BeforeEach(func() {
984				fakeAccess.IsAdminReturns(true)
985			})
986			It("deletes the worker from the DB", func() {
987				Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1))
988				Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName))
989
990				Expect(fakeWorker.DeleteCallCount()).To(Equal(1))
991			})
992			It("returns 200", func() {
993				Expect(response.StatusCode).To(Equal(http.StatusOK))
994			})
995		})
996
997		Context("when user is authorized for team", func() {
998			BeforeEach(func() {
999				fakeWorker.TeamNameReturns("some-team")
1000				fakeAccess.IsAuthorizedReturns(true)
1001			})
1002			It("deletes the worker from the DB", func() {
1003				Expect(dbWorkerFactory.GetWorkerCallCount()).To(Equal(1))
1004				Expect(dbWorkerFactory.GetWorkerArgsForCall(0)).To(Equal(workerName))
1005
1006				Expect(fakeWorker.DeleteCallCount()).To(Equal(1))
1007			})
1008			It("returns 200", func() {
1009				Expect(response.StatusCode).To(Equal(http.StatusOK))
1010			})
1011		})
1012
1013		Context("when not authenticated", func() {
1014			BeforeEach(func() {
1015				fakeAccess.IsAuthenticatedReturns(false)
1016			})
1017
1018			It("returns 401", func() {
1019				Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
1020			})
1021
1022			It("does not attempt to find the worker", func() {
1023				Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero())
1024			})
1025		})
1026	})
1027})
1028