diff --git a/web/server/api/server.go b/web/server/api/server.go index 6cea26da..aae954e8 100644 --- a/web/server/api/server.go +++ b/web/server/api/server.go @@ -1,164 +1,190 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "os" +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "os" "strconv" + "sync" "time" - - "github.com/smartystreets/goconvey/web/server/contract" - "github.com/smartystreets/goconvey/web/server/messaging" -) - -type HTTPServer struct { - watcher chan messaging.WatcherCommand - executor contract.Executor - latest *contract.CompleteOutput - currentRoot string - longpoll chan chan string + + "github.com/smartystreets/goconvey/web/server/contract" + "github.com/smartystreets/goconvey/web/server/messaging" +) + +type HTTPServer struct { + watcher chan messaging.WatcherCommand + executor contract.Executor + latest *contract.CompleteOutput + currentRoot string + longpoll chan chan string paused bool + stateMu sync.RWMutex } - + func (self *HTTPServer) ReceiveUpdate(root string, update *contract.CompleteOutput) { + self.stateMu.Lock() + defer self.stateMu.Unlock() self.currentRoot = root self.latest = update } - + func (self *HTTPServer) Watch(response http.ResponseWriter, request *http.Request) { - if request.Method == "POST" { + switch request.Method { + case http.MethodPost: self.adjustRoot(response, request) - } else if request.Method == "GET" { - response.Write([]byte(self.currentRoot)) + case http.MethodGet: + self.stateMu.RLock() + root := self.currentRoot + self.stateMu.RUnlock() + _, _ = response.Write([]byte(root)) + default: + response.WriteHeader(http.StatusMethodNotAllowed) } } - -func (self *HTTPServer) adjustRoot(response http.ResponseWriter, request *http.Request) { - newRoot := self.parseQueryString("root", response, request) - if newRoot == "" { + +func (self *HTTPServer) adjustRoot(response http.ResponseWriter, request *http.Request) { + newRoot := self.parseQueryString("root", response, request) + if newRoot == "" { + return + } + info, err := os.Stat(newRoot) + if err != nil { + http.Error(response, fmt.Sprintf("Directory does not exist: '%s'", newRoot), http.StatusNotFound) return } - info, err := os.Stat(newRoot) // TODO: how to unit test? - if !info.IsDir() || err != nil { - http.Error(response, err.Error(), http.StatusNotFound) + if !info.IsDir() { + http.Error(response, fmt.Sprintf("Path is not a directory: '%s'", newRoot), http.StatusNotFound) return } - - self.watcher <- messaging.WatcherCommand{ - Instruction: messaging.WatcherAdjustRoot, - Details: newRoot, - } -} - -func (self *HTTPServer) Ignore(response http.ResponseWriter, request *http.Request) { - paths := self.parseQueryString("paths", response, request) - if paths != "" { - self.watcher <- messaging.WatcherCommand{ - Instruction: messaging.WatcherIgnore, - Details: paths, - } - } -} - -func (self *HTTPServer) Reinstate(response http.ResponseWriter, request *http.Request) { - paths := self.parseQueryString("paths", response, request) - if paths != "" { - self.watcher <- messaging.WatcherCommand{ - Instruction: messaging.WatcherReinstate, - Details: paths, - } - } -} - -func (self *HTTPServer) parseQueryString(key string, response http.ResponseWriter, request *http.Request) string { - value := request.URL.Query()[key] - - if len(value) == 0 { - http.Error(response, fmt.Sprintf("No '%s' query string parameter included!", key), http.StatusBadRequest) - return "" - } - - path := value[0] - if path == "" { - http.Error(response, "You must provide a non-blank path.", http.StatusBadRequest) - } - return path -} - -func (self *HTTPServer) Status(response http.ResponseWriter, request *http.Request) { - status := self.executor.Status() - response.Write([]byte(status)) -} - -func (self *HTTPServer) LongPollStatus(response http.ResponseWriter, request *http.Request) { - if self.executor.ClearStatusFlag() { - response.Write([]byte(self.executor.Status())) - return - } - - timeout, err := strconv.Atoi(request.URL.Query().Get("timeout")) - if err != nil || timeout > 180000 || timeout < 0 { - timeout = 60000 // default timeout is 60 seconds + + self.watcher <- messaging.WatcherCommand{ + Instruction: messaging.WatcherAdjustRoot, + Details: newRoot, + } +} + +func (self *HTTPServer) Ignore(response http.ResponseWriter, request *http.Request) { + paths := self.parseQueryString("paths", response, request) + if paths != "" { + self.watcher <- messaging.WatcherCommand{ + Instruction: messaging.WatcherIgnore, + Details: paths, + } + } +} + +func (self *HTTPServer) Reinstate(response http.ResponseWriter, request *http.Request) { + paths := self.parseQueryString("paths", response, request) + if paths != "" { + self.watcher <- messaging.WatcherCommand{ + Instruction: messaging.WatcherReinstate, + Details: paths, + } + } +} + +func (self *HTTPServer) parseQueryString(key string, response http.ResponseWriter, request *http.Request) string { + value := request.URL.Query()[key] + + if len(value) == 0 { + http.Error(response, fmt.Sprintf("No '%s' query string parameter included!", key), http.StatusBadRequest) + return "" + } + + path := value[0] + if path == "" { + http.Error(response, "You must provide a non-blank path.", http.StatusBadRequest) + } + return path +} + +func (self *HTTPServer) Status(response http.ResponseWriter, request *http.Request) { + status := self.executor.Status() + response.Write([]byte(status)) +} + +func (self *HTTPServer) LongPollStatus(response http.ResponseWriter, request *http.Request) { + if self.executor.ClearStatusFlag() { + response.Write([]byte(self.executor.Status())) + return + } + + timeout, err := strconv.Atoi(request.URL.Query().Get("timeout")) + if err != nil || timeout > 180000 || timeout < 0 { + timeout = 60000 // default timeout is 60 seconds + } + + myReqChan := make(chan string) + + select { + case self.longpoll <- myReqChan: // this case means the executor's status is changing + case <-time.After(time.Duration(timeout) * time.Millisecond): // this case means the executor hasn't changed status + return + } + + out := <-myReqChan + + if out != "" { // TODO: Why is this check necessary? Sometimes it writes empty string... + response.Write([]byte(out)) + } +} + +func (self *HTTPServer) Results(response http.ResponseWriter, request *http.Request) { + response.Header().Set("Content-Type", "application/json") + response.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + response.Header().Set("Pragma", "no-cache") + response.Header().Set("Expires", "0") + self.stateMu.RLock() + var latest *contract.CompleteOutput + if self.latest != nil { + latestCopy := *self.latest + latestCopy.Paused = self.paused + latest = &latestCopy } + self.stateMu.RUnlock() - myReqChan := make(chan string) - - select { - case self.longpoll <- myReqChan: // this case means the executor's status is changing - case <-time.After(time.Duration(timeout) * time.Millisecond): // this case means the executor hasn't changed status + stuff, err := json.Marshal(latest) + if err != nil { + http.Error(response, "Unable to encode the latest results", http.StatusInternalServerError) return } - - out := <-myReqChan - - if out != "" { // TODO: Why is this check necessary? Sometimes it writes empty string... - response.Write([]byte(out)) - } -} - -func (self *HTTPServer) Results(response http.ResponseWriter, request *http.Request) { - response.Header().Set("Content-Type", "application/json") - response.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - response.Header().Set("Pragma", "no-cache") - response.Header().Set("Expires", "0") - if self.latest != nil { - self.latest.Paused = self.paused - } - stuff, _ := json.Marshal(self.latest) - response.Write(stuff) -} - -func (self *HTTPServer) Execute(response http.ResponseWriter, request *http.Request) { - go self.execute() -} - -func (self *HTTPServer) execute() { - self.watcher <- messaging.WatcherCommand{Instruction: messaging.WatcherExecute} -} - + _, _ = response.Write(stuff) +} + +func (self *HTTPServer) Execute(response http.ResponseWriter, request *http.Request) { + go self.execute() +} + +func (self *HTTPServer) execute() { + self.watcher <- messaging.WatcherCommand{Instruction: messaging.WatcherExecute} +} + func (self *HTTPServer) TogglePause(response http.ResponseWriter, request *http.Request) { + self.stateMu.Lock() instruction := messaging.WatcherPause if self.paused { instruction = messaging.WatcherResume } - - self.watcher <- messaging.WatcherCommand{Instruction: instruction} self.paused = !self.paused + paused := self.paused + self.stateMu.Unlock() - fmt.Fprint(response, self.paused) // we could write out whatever helps keep the UI honest... -} - -func NewHTTPServer( - root string, - watcher chan messaging.WatcherCommand, - executor contract.Executor, - status chan chan string) *HTTPServer { - - self := new(HTTPServer) - self.currentRoot = root - self.watcher = watcher - self.executor = executor - self.longpoll = status - return self + self.watcher <- messaging.WatcherCommand{Instruction: instruction} + _, _ = fmt.Fprint(response, paused) } + +func NewHTTPServer( + root string, + watcher chan messaging.WatcherCommand, + executor contract.Executor, + status chan chan string) *HTTPServer { + + self := new(HTTPServer) + self.currentRoot = root + self.watcher = watcher + self.executor = executor + self.longpoll = status + return self +} diff --git a/web/server/api/server_test.go b/web/server/api/server_test.go index 202f7817..9da97f19 100644 --- a/web/server/api/server_test.go +++ b/web/server/api/server_test.go @@ -1,462 +1,480 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" - - . "github.com/smartystreets/goconvey/convey" - "github.com/smartystreets/goconvey/web/server/contract" - "github.com/smartystreets/goconvey/web/server/messaging" -) - -const initialRoot = "/root/gopath/src/github.com/smartystreets/project" -const nonexistentRoot = "I don't exist" -const unreadableContent = "!!error!!" - -func TestHTTPServer(t *testing.T) { - // TODO: fix the skipped tests... - - Convey("Subject: HttpServer responds to requests appropriately", t, func() { - fixture := newServerFixture() - - Convey("Before any update is received", func() { - Convey("When the update is requested", func() { - update, _ := fixture.RequestLatest() - - Convey("No panic should occur", func() { - So(func() { fixture.RequestLatest() }, ShouldNotPanic) - }) - - Convey("The update will be empty", func() { - So(update, ShouldResemble, new(contract.CompleteOutput)) - }) - }) - }) - - Convey("Given an update is received", func() { - fixture.ReceiveUpdate("", &contract.CompleteOutput{Revision: "asdf"}) - - Convey("When the update is requested", func() { - update, response := fixture.RequestLatest() - - Convey("The server returns it", func() { - So(update, ShouldResemble, &contract.CompleteOutput{Revision: "asdf"}) - }) - - Convey("The server returns 200", func() { - So(response.Code, ShouldEqual, http.StatusOK) - }) - - Convey("The server should include important cache-related headers", func() { - So(len(response.HeaderMap), ShouldEqual, 4) - So(response.HeaderMap["Content-Type"][0], ShouldEqual, "application/json") - So(response.HeaderMap["Cache-Control"][0], ShouldEqual, "no-cache, no-store, must-revalidate") - So(response.HeaderMap["Pragma"][0], ShouldEqual, "no-cache") - So(response.HeaderMap["Expires"][0], ShouldEqual, "0") - }) - }) - }) - - Convey("When the root watch is queried", func() { - root, status := fixture.QueryRootWatch(false) - - SkipConvey("The server returns it", func() { - So(root, ShouldEqual, initialRoot) - }) - - Convey("The server returns HTTP 200 - OK", func() { - So(status, ShouldEqual, http.StatusOK) - }) - }) - - SkipConvey("When the root watch is adjusted", func() { - - Convey("But the request has no root parameter", func() { - status, body := fixture.AdjustRootWatchMalformed() - - Convey("The server returns HTTP 400 - Bad Input", func() { - So(status, ShouldEqual, http.StatusBadRequest) - }) - - Convey("The body should contain a helpful error message", func() { - So(body, ShouldEqual, "No 'root' query string parameter included!") - }) - - Convey("The server should not change the existing root", func() { - root, _ := fixture.QueryRootWatch(false) - So(root, ShouldEqual, initialRoot) - }) - }) - - Convey("But the root parameter is empty", func() { - status, body := fixture.AdjustRootWatch("") - - Convey("The server returns HTTP 400 - Bad Input", func() { - So(status, ShouldEqual, http.StatusBadRequest) - }) - - Convey("The server should provide a helpful error message", func() { - So(body, ShouldEqual, "You must provide a non-blank path.") - }) - - Convey("The server should not change the existing root", func() { - root, _ := fixture.QueryRootWatch(false) - So(root, ShouldEqual, initialRoot) - }) - }) - - Convey("And the new root exists", func() { - status, body := fixture.AdjustRootWatch(initialRoot + "/package") - - Convey("The server returns HTTP 200 - OK", func() { - So(status, ShouldEqual, http.StatusOK) - }) - - Convey("The body should NOT contain any error message or content", func() { - So(body, ShouldEqual, "") - }) - - Convey("The server informs the watcher of the new root", func() { - root, _ := fixture.QueryRootWatch(false) - So(root, ShouldEqual, initialRoot+"/package") - }) - }) - - Convey("And the new root does NOT exist", func() { - status, body := fixture.AdjustRootWatch(nonexistentRoot) - - Convey("The server returns HTTP 404 - Not Found", func() { - So(status, ShouldEqual, http.StatusNotFound) - }) - - Convey("The body should contain a helpful error message", func() { - So(body, ShouldEqual, fmt.Sprintf("Directory does not exist: '%s'", nonexistentRoot)) - }) - - Convey("The server should not change the existing root", func() { - root, _ := fixture.QueryRootWatch(false) - So(root, ShouldEqual, initialRoot) - }) - }) - }) - - SkipConvey("When a package is ignored", func() { - - Convey("But the request has no path parameter", func() { - status, body := fixture.IgnoreMalformed() - - Convey("The server returns HTTP 400 - Bad Input", func() { - So(status, ShouldEqual, http.StatusBadRequest) - }) - - Convey("The body should contain a helpful error message", func() { - So(body, ShouldEqual, "No 'paths' query string parameter included!") - }) - - SkipConvey("The server should not ignore anything", func() { - // So(fixture.watcher.ignored, ShouldEqual, "") - }) - }) - - Convey("But the request is blank", func() { - status, body := fixture.Ignore("") - - Convey("The server returns HTTP 400 - Bad Input", func() { - So(status, ShouldEqual, http.StatusBadRequest) - }) - - Convey("The body should contain a helpful error message", func() { - So(body, ShouldEqual, "You must provide a non-blank path.") - }) - }) - - Convey("And the request is well formed", func() { - status, _ := fixture.Ignore(initialRoot) - - SkipConvey("The server informs the watcher", func() { - // So(fixture.watcher.ignored, ShouldEqual, initialRoot) - }) - Convey("The server returns HTTP 200 - OK", func() { - So(status, ShouldEqual, http.StatusOK) - }) - }) - }) - - SkipConvey("When a package is reinstated", func() { - Convey("But the request has no path parameter", func() { - status, body := fixture.ReinstateMalformed() - - Convey("The server returns HTTP 400 - Bad Input", func() { - So(status, ShouldEqual, http.StatusBadRequest) - }) - - Convey("The body should contain a helpful error message", func() { - So(body, ShouldEqual, "No 'paths' query string parameter included!") - }) - - SkipConvey("The server should not ignore anything", func() { - // So(fixture.watcher.reinstated, ShouldEqual, "") - }) - }) - - Convey("But the request is blank", func() { - status, body := fixture.Reinstate("") - - Convey("The server returns HTTP 400 - Bad Input", func() { - So(status, ShouldEqual, http.StatusBadRequest) - }) - - Convey("The body should contain a helpful error message", func() { - So(body, ShouldEqual, "You must provide a non-blank path.") - }) - }) - - Convey("And the request is well formed", func() { - status, _ := fixture.Reinstate(initialRoot) - - SkipConvey("The server informs the watcher", func() { - // So(fixture.watcher.reinstated, ShouldEqual, initialRoot) - }) - Convey("The server returns HTTP 200 - OK", func() { - So(status, ShouldEqual, http.StatusOK) - }) - }) - }) - - Convey("When the status of the executor is requested", func() { - fixture.executor.status = "blah blah blah" - statusCode, statusBody := fixture.RequestExecutorStatus() - - Convey("The server asks the executor its status and returns it", func() { - So(statusBody, ShouldEqual, "blah blah blah") - }) - - Convey("The server returns HTTP 200 - OK", func() { - So(statusCode, ShouldEqual, http.StatusOK) - }) - }) - - Convey("When a manual execution of the test packages is requested", func() { - status := fixture.ManualExecution() - update, _ := fixture.RequestLatest() - - SkipConvey("The server invokes the executor using the watcher's listing and save the result", func() { - So(update, ShouldResemble, &contract.CompleteOutput{Revision: initialRoot}) - }) - - Convey("The server returns HTTP 200 - OK", func() { - So(status, ShouldEqual, http.StatusOK) - }) - }) - - SkipConvey("When the pause setting is toggled via the server", func() { - paused := fixture.TogglePause() - - SkipConvey("The pause channel buffer should have a true value", func() { - // var value bool - // select { - // case value = <-fixture.pauseUpdate: - // default: - // } - // So(value, ShouldBeTrue) - }) - - Convey("The latest results should show that the server is paused", func() { - fixture.ReceiveUpdate("", &contract.CompleteOutput{Revision: "asdf"}) - update, _ := fixture.RequestLatest() - - So(update.Paused, ShouldBeTrue) - }) - - Convey("The toggle handler should return its new status", func() { - So(paused, ShouldEqual, "true") - }) - }) - }) -} - -/********* Server Fixture *********/ - -type ServerFixture struct { - server *HTTPServer - watcher chan messaging.WatcherCommand - executor *FakeExecutor - statusUpdate chan bool -} - -func (self *ServerFixture) ReceiveUpdate(root string, update *contract.CompleteOutput) { - self.server.ReceiveUpdate(root, update) -} - -func (self *ServerFixture) RequestLatest() (*contract.CompleteOutput, *httptest.ResponseRecorder) { - request, _ := http.NewRequest("GET", "http://localhost:8080/results", nil) - response := httptest.NewRecorder() - - self.server.Results(response, request) - - decoder := json.NewDecoder(strings.NewReader(response.Body.String())) - update := new(contract.CompleteOutput) - decoder.Decode(update) - return update, response -} - -func (self *ServerFixture) QueryRootWatch(newclient bool) (string, int) { - url := "http://localhost:8080/watch" - if newclient { - url += "?newclient=1" +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" + "github.com/smartystreets/goconvey/web/server/contract" + "github.com/smartystreets/goconvey/web/server/messaging" +) + +const initialRoot = "/root/gopath/src/github.com/smartystreets/project" +const nonexistentRoot = "I don't exist" +const unreadableContent = "!!error!!" + +func TestHTTPServer(t *testing.T) { + // TODO: fix the skipped tests... + + Convey("Subject: HttpServer responds to requests appropriately", t, func() { + fixture := newServerFixture() + + Convey("Before any update is received", func() { + Convey("When the update is requested", func() { + update, _ := fixture.RequestLatest() + + Convey("No panic should occur", func() { + So(func() { fixture.RequestLatest() }, ShouldNotPanic) + }) + + Convey("The update will be empty", func() { + So(update, ShouldResemble, new(contract.CompleteOutput)) + }) + }) + }) + + Convey("Given an update is received", func() { + fixture.ReceiveUpdate("", &contract.CompleteOutput{Revision: "asdf"}) + + Convey("When the update is requested", func() { + update, response := fixture.RequestLatest() + + Convey("The server returns it", func() { + So(update, ShouldResemble, &contract.CompleteOutput{Revision: "asdf"}) + }) + + Convey("The server returns 200", func() { + So(response.Code, ShouldEqual, http.StatusOK) + }) + + Convey("The server should include important cache-related headers", func() { + So(len(response.HeaderMap), ShouldEqual, 4) + So(response.HeaderMap["Content-Type"][0], ShouldEqual, "application/json") + So(response.HeaderMap["Cache-Control"][0], ShouldEqual, "no-cache, no-store, must-revalidate") + So(response.HeaderMap["Pragma"][0], ShouldEqual, "no-cache") + So(response.HeaderMap["Expires"][0], ShouldEqual, "0") + }) + }) + }) + + Convey("When the root watch is queried", func() { + root, status := fixture.QueryRootWatch(false) + + SkipConvey("The server returns it", func() { + So(root, ShouldEqual, initialRoot) + }) + + Convey("The server returns HTTP 200 - OK", func() { + So(status, ShouldEqual, http.StatusOK) + }) + }) + + SkipConvey("When the root watch is adjusted", func() { + + Convey("But the request has no root parameter", func() { + status, body := fixture.AdjustRootWatchMalformed() + + Convey("The server returns HTTP 400 - Bad Input", func() { + So(status, ShouldEqual, http.StatusBadRequest) + }) + + Convey("The body should contain a helpful error message", func() { + So(body, ShouldEqual, "No 'root' query string parameter included!") + }) + + Convey("The server should not change the existing root", func() { + root, _ := fixture.QueryRootWatch(false) + So(root, ShouldEqual, initialRoot) + }) + }) + + Convey("But the root parameter is empty", func() { + status, body := fixture.AdjustRootWatch("") + + Convey("The server returns HTTP 400 - Bad Input", func() { + So(status, ShouldEqual, http.StatusBadRequest) + }) + + Convey("The server should provide a helpful error message", func() { + So(body, ShouldEqual, "You must provide a non-blank path.") + }) + + Convey("The server should not change the existing root", func() { + root, _ := fixture.QueryRootWatch(false) + So(root, ShouldEqual, initialRoot) + }) + }) + + Convey("And the new root exists", func() { + status, body := fixture.AdjustRootWatch(initialRoot + "/package") + + Convey("The server returns HTTP 200 - OK", func() { + So(status, ShouldEqual, http.StatusOK) + }) + + Convey("The body should NOT contain any error message or content", func() { + So(body, ShouldEqual, "") + }) + + Convey("The server informs the watcher of the new root", func() { + root, _ := fixture.QueryRootWatch(false) + So(root, ShouldEqual, initialRoot+"/package") + }) + }) + + Convey("And the new root does NOT exist", func() { + status, body := fixture.AdjustRootWatch(nonexistentRoot) + + Convey("The server returns HTTP 404 - Not Found", func() { + So(status, ShouldEqual, http.StatusNotFound) + }) + + Convey("The body should contain a helpful error message", func() { + So(body, ShouldEqual, fmt.Sprintf("Directory does not exist: '%s'", nonexistentRoot)) + }) + + Convey("The server should not change the existing root", func() { + root, _ := fixture.QueryRootWatch(false) + So(root, ShouldEqual, initialRoot) + }) + }) + }) + + SkipConvey("When a package is ignored", func() { + + Convey("But the request has no path parameter", func() { + status, body := fixture.IgnoreMalformed() + + Convey("The server returns HTTP 400 - Bad Input", func() { + So(status, ShouldEqual, http.StatusBadRequest) + }) + + Convey("The body should contain a helpful error message", func() { + So(body, ShouldEqual, "No 'paths' query string parameter included!") + }) + + SkipConvey("The server should not ignore anything", func() { + // So(fixture.watcher.ignored, ShouldEqual, "") + }) + }) + + Convey("But the request is blank", func() { + status, body := fixture.Ignore("") + + Convey("The server returns HTTP 400 - Bad Input", func() { + So(status, ShouldEqual, http.StatusBadRequest) + }) + + Convey("The body should contain a helpful error message", func() { + So(body, ShouldEqual, "You must provide a non-blank path.") + }) + }) + + Convey("And the request is well formed", func() { + status, _ := fixture.Ignore(initialRoot) + + SkipConvey("The server informs the watcher", func() { + // So(fixture.watcher.ignored, ShouldEqual, initialRoot) + }) + Convey("The server returns HTTP 200 - OK", func() { + So(status, ShouldEqual, http.StatusOK) + }) + }) + }) + + SkipConvey("When a package is reinstated", func() { + Convey("But the request has no path parameter", func() { + status, body := fixture.ReinstateMalformed() + + Convey("The server returns HTTP 400 - Bad Input", func() { + So(status, ShouldEqual, http.StatusBadRequest) + }) + + Convey("The body should contain a helpful error message", func() { + So(body, ShouldEqual, "No 'paths' query string parameter included!") + }) + + SkipConvey("The server should not ignore anything", func() { + // So(fixture.watcher.reinstated, ShouldEqual, "") + }) + }) + + Convey("But the request is blank", func() { + status, body := fixture.Reinstate("") + + Convey("The server returns HTTP 400 - Bad Input", func() { + So(status, ShouldEqual, http.StatusBadRequest) + }) + + Convey("The body should contain a helpful error message", func() { + So(body, ShouldEqual, "You must provide a non-blank path.") + }) + }) + + Convey("And the request is well formed", func() { + status, _ := fixture.Reinstate(initialRoot) + + SkipConvey("The server informs the watcher", func() { + // So(fixture.watcher.reinstated, ShouldEqual, initialRoot) + }) + Convey("The server returns HTTP 200 - OK", func() { + So(status, ShouldEqual, http.StatusOK) + }) + }) + }) + + Convey("When the status of the executor is requested", func() { + fixture.executor.status = "blah blah blah" + statusCode, statusBody := fixture.RequestExecutorStatus() + + Convey("The server asks the executor its status and returns it", func() { + So(statusBody, ShouldEqual, "blah blah blah") + }) + + Convey("The server returns HTTP 200 - OK", func() { + So(statusCode, ShouldEqual, http.StatusOK) + }) + }) + + Convey("When a manual execution of the test packages is requested", func() { + status := fixture.ManualExecution() + update, _ := fixture.RequestLatest() + + SkipConvey("The server invokes the executor using the watcher's listing and save the result", func() { + So(update, ShouldResemble, &contract.CompleteOutput{Revision: initialRoot}) + }) + + Convey("The server returns HTTP 200 - OK", func() { + So(status, ShouldEqual, http.StatusOK) + }) + }) + + SkipConvey("When the pause setting is toggled via the server", func() { + paused := fixture.TogglePause() + + SkipConvey("The pause channel buffer should have a true value", func() { + // var value bool + // select { + // case value = <-fixture.pauseUpdate: + // default: + // } + // So(value, ShouldBeTrue) + }) + + Convey("The latest results should show that the server is paused", func() { + fixture.ReceiveUpdate("", &contract.CompleteOutput{Revision: "asdf"}) + update, _ := fixture.RequestLatest() + + So(update.Paused, ShouldBeTrue) + }) + + Convey("The toggle handler should return its new status", func() { + So(paused, ShouldEqual, "true") + }) + }) + }) +} + +/********* Server Fixture *********/ + +type ServerFixture struct { + server *HTTPServer + watcher chan messaging.WatcherCommand + executor *FakeExecutor + statusUpdate chan bool +} + +func (self *ServerFixture) ReceiveUpdate(root string, update *contract.CompleteOutput) { + self.server.ReceiveUpdate(root, update) +} + +func (self *ServerFixture) RequestLatest() (*contract.CompleteOutput, *httptest.ResponseRecorder) { + request, _ := http.NewRequest("GET", "http://localhost:8080/results", nil) + response := httptest.NewRecorder() + + self.server.Results(response, request) + + decoder := json.NewDecoder(strings.NewReader(response.Body.String())) + update := new(contract.CompleteOutput) + decoder.Decode(update) + return update, response +} + +func (self *ServerFixture) QueryRootWatch(newclient bool) (string, int) { + url := "http://localhost:8080/watch" + if newclient { + url += "?newclient=1" + } + request, _ := http.NewRequest("GET", url, nil) + response := httptest.NewRecorder() + + self.server.Watch(response, request) + + return strings.TrimSpace(response.Body.String()), response.Code +} + +func (self *ServerFixture) AdjustRootWatchMalformed() (status int, body string) { + request, _ := http.NewRequest("POST", "http://localhost:8080/watch", nil) + response := httptest.NewRecorder() + + self.server.Watch(response, request) + + status, body = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) AdjustRootWatch(newRoot string) (status int, body string) { + escapedRoot := url.QueryEscape(newRoot) + request, _ := http.NewRequest("POST", "http://localhost:8080/watch?root="+escapedRoot, nil) + response := httptest.NewRecorder() + + self.server.Watch(response, request) + + status, body = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) IgnoreMalformed() (status int, body string) { + request, _ := http.NewRequest("POST", "http://localhost:8080/ignore", nil) + response := httptest.NewRecorder() + + self.server.Ignore(response, request) + + status, body = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) Ignore(folder string) (status int, body string) { + escapedFolder := url.QueryEscape(folder) + request, _ := http.NewRequest("POST", "http://localhost:8080/ignore?paths="+escapedFolder, nil) + response := httptest.NewRecorder() + + self.server.Ignore(response, request) + + status, body = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) ReinstateMalformed() (status int, body string) { + request, _ := http.NewRequest("POST", "http://localhost:8080/reinstate", nil) + response := httptest.NewRecorder() + + self.server.Reinstate(response, request) + + status, body = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) Reinstate(folder string) (status int, body string) { + escapedFolder := url.QueryEscape(folder) + request, _ := http.NewRequest("POST", "http://localhost:8080/reinstate?paths="+escapedFolder, nil) + response := httptest.NewRecorder() + + self.server.Reinstate(response, request) + + status, body = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) SetExecutorStatus(status string) { + // self.executor.status = status + // select { + // case self.executor.statusUpdate <- make(chan string): + // default: + // } +} + +func (self *ServerFixture) RequestExecutorStatus() (code int, status string) { + request, _ := http.NewRequest("GET", "http://localhost:8080/status", nil) + response := httptest.NewRecorder() + + self.server.Status(response, request) + + code, status = response.Code, strings.TrimSpace(response.Body.String()) + return +} + +func (self *ServerFixture) ManualExecution() int { + request, _ := http.NewRequest("POST", "http://localhost:8080/execute", nil) + response := httptest.NewRecorder() + + self.server.Execute(response, request) + nap, _ := time.ParseDuration("100ms") + time.Sleep(nap) + return response.Code +} + +func (self *ServerFixture) TogglePause() string { + request, _ := http.NewRequest("POST", "http://localhost:8080/pause", nil) + response := httptest.NewRecorder() + + self.server.TogglePause(response, request) + + return response.Body.String() +} + +func newServerFixture() *ServerFixture { + self := new(ServerFixture) + self.watcher = make(chan messaging.WatcherCommand) + // self.watcher.SetRootWatch(initialRoot) + statusUpdate := make(chan chan string) + self.executor = newFakeExecutor("", statusUpdate) + self.server = NewHTTPServer("initial-working-dir", self.watcher, self.executor, statusUpdate) + return self +} + +/********* Fake Executor *********/ + +type FakeExecutor struct { + status string + executed bool + statusFlag bool + statusUpdate chan chan string +} + +func (self *FakeExecutor) Status() string { + return self.status +} + +func (self *FakeExecutor) ClearStatusFlag() bool { + hasNewStatus := self.statusFlag + self.statusFlag = false + return hasNewStatus +} + +func (self *FakeExecutor) ExecuteTests(watched []*contract.Package) *contract.CompleteOutput { + output := new(contract.CompleteOutput) + output.Revision = watched[0].Path + return output +} + +func newFakeExecutor(status string, statusUpdate chan chan string) *FakeExecutor { + self := new(FakeExecutor) + self.status = status + self.statusUpdate = statusUpdate + return self +} + +func TestAdjustRootRejectsMissingDirectoryWithoutPanic(t *testing.T) { + fixture := newServerFixture() + request, err := http.NewRequest(http.MethodPost, "http://localhost:8080/watch?root="+url.QueryEscape(nonexistentRoot), nil) + if err != nil { + t.Fatal(err) } - request, _ := http.NewRequest("GET", url, nil) - response := httptest.NewRecorder() - - self.server.Watch(response, request) - - return strings.TrimSpace(response.Body.String()), response.Code -} - -func (self *ServerFixture) AdjustRootWatchMalformed() (status int, body string) { - request, _ := http.NewRequest("POST", "http://localhost:8080/watch", nil) - response := httptest.NewRecorder() - - self.server.Watch(response, request) - - status, body = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) AdjustRootWatch(newRoot string) (status int, body string) { - escapedRoot := url.QueryEscape(newRoot) - request, _ := http.NewRequest("POST", "http://localhost:8080/watch?root="+escapedRoot, nil) - response := httptest.NewRecorder() - - self.server.Watch(response, request) - - status, body = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) IgnoreMalformed() (status int, body string) { - request, _ := http.NewRequest("POST", "http://localhost:8080/ignore", nil) - response := httptest.NewRecorder() - - self.server.Ignore(response, request) - - status, body = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) Ignore(folder string) (status int, body string) { - escapedFolder := url.QueryEscape(folder) - request, _ := http.NewRequest("POST", "http://localhost:8080/ignore?paths="+escapedFolder, nil) - response := httptest.NewRecorder() - - self.server.Ignore(response, request) - - status, body = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) ReinstateMalformed() (status int, body string) { - request, _ := http.NewRequest("POST", "http://localhost:8080/reinstate", nil) - response := httptest.NewRecorder() - - self.server.Reinstate(response, request) - - status, body = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) Reinstate(folder string) (status int, body string) { - escapedFolder := url.QueryEscape(folder) - request, _ := http.NewRequest("POST", "http://localhost:8080/reinstate?paths="+escapedFolder, nil) - response := httptest.NewRecorder() - - self.server.Reinstate(response, request) - - status, body = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) SetExecutorStatus(status string) { - // self.executor.status = status - // select { - // case self.executor.statusUpdate <- make(chan string): - // default: - // } -} - -func (self *ServerFixture) RequestExecutorStatus() (code int, status string) { - request, _ := http.NewRequest("GET", "http://localhost:8080/status", nil) - response := httptest.NewRecorder() - - self.server.Status(response, request) - - code, status = response.Code, strings.TrimSpace(response.Body.String()) - return -} - -func (self *ServerFixture) ManualExecution() int { - request, _ := http.NewRequest("POST", "http://localhost:8080/execute", nil) - response := httptest.NewRecorder() - - self.server.Execute(response, request) - nap, _ := time.ParseDuration("100ms") - time.Sleep(nap) - return response.Code -} - -func (self *ServerFixture) TogglePause() string { - request, _ := http.NewRequest("POST", "http://localhost:8080/pause", nil) response := httptest.NewRecorder() - self.server.TogglePause(response, request) - - return response.Body.String() -} - -func newServerFixture() *ServerFixture { - self := new(ServerFixture) - self.watcher = make(chan messaging.WatcherCommand) - // self.watcher.SetRootWatch(initialRoot) - statusUpdate := make(chan chan string) - self.executor = newFakeExecutor("", statusUpdate) - self.server = NewHTTPServer("initial-working-dir", self.watcher, self.executor, statusUpdate) - return self -} - -/********* Fake Executor *********/ - -type FakeExecutor struct { - status string - executed bool - statusFlag bool - statusUpdate chan chan string -} + fixture.server.Watch(response, request) -func (self *FakeExecutor) Status() string { - return self.status -} - -func (self *FakeExecutor) ClearStatusFlag() bool { - hasNewStatus := self.statusFlag - self.statusFlag = false - return hasNewStatus -} - -func (self *FakeExecutor) ExecuteTests(watched []*contract.Package) *contract.CompleteOutput { - output := new(contract.CompleteOutput) - output.Revision = watched[0].Path - return output -} - -func newFakeExecutor(status string, statusUpdate chan chan string) *FakeExecutor { - self := new(FakeExecutor) - self.status = status - self.statusUpdate = statusUpdate - return self + if response.Code != http.StatusNotFound { + t.Fatalf("expected status %d, got %d", http.StatusNotFound, response.Code) + } + if !strings.Contains(response.Body.String(), "Directory does not exist") { + t.Fatalf("expected a missing-directory error, got %q", response.Body.String()) + } }