Add HTTP API package
authorMaciej Wereski <m.wereski@partner.samsung.com>
Thu, 31 Aug 2017 14:11:31 +0000 (16:11 +0200)
committerMaciej Wereski <m.wereski@partner.samsung.com>
Tue, 5 Jun 2018 09:24:40 +0000 (11:24 +0200)
HTTP API package will provide all HTTP API which will be exposed to the
Boruta clients. Only User API counterpart of Boruta is added for now.
Currently the package provides only one function, which takes pointer to
httprouter.Router and registers all paths and HTTP methods in it.

HTTP API module will have a lot of test cases agregated in test tables.
To improve test cases development time and readability of test files,
flag '-update' is added to test. It will run tests and for all
testcases a file with results will be generated in 'testdata'
subdirectory. During testing (running without '-update' flag) those
files are read and compared with results of tested functions.

Generating testcase files:
$ go test git.tizen.org/tools/boruta/server/api/v1 -update
Testing (as usual):
$ go test git.tizen.org/tools/boruta/server/api/v1

After updating testcase files output must be inspected by developer
before commiting to the repository (and should be also checked by
reviewers before merging).

Change-Id: I6d98f093cce116512dc305e07a8614990580faff
Signed-off-by: Maciej Wereski <m.wereski@partner.samsung.com>
21 files changed:
boruta.go
mocks/mock_requests.go [new file with mode: 0644]
mocks/mock_workers.go [new file with mode: 0644]
server/api/v1/api.go [new file with mode: 0644]
server/api/v1/api_test.go [new file with mode: 0644]
server/api/v1/error.go [new file with mode: 0644]
server/api/v1/handlers.go [new file with mode: 0644]
server/api/v1/handlers_test.go [new file with mode: 0644]
server/api/v1/testdata/acquire-worker-POST.json [new file with mode: 0644]
server/api/v1/testdata/close-req-POST.json [new file with mode: 0644]
server/api/v1/testdata/list-reqs-all-GET.json [new file with mode: 0644]
server/api/v1/testdata/list-reqs-filter-POST.json [new file with mode: 0644]
server/api/v1/testdata/list-workers-all-GET.json [new file with mode: 0644]
server/api/v1/testdata/list-workers-filter-POST.json [new file with mode: 0644]
server/api/v1/testdata/new-req-POST.json [new file with mode: 0644]
server/api/v1/testdata/panic-other-error.txt [new file with mode: 0644]
server/api/v1/testdata/panic-server-error.txt [new file with mode: 0644]
server/api/v1/testdata/prolong-access-POST.json [new file with mode: 0644]
server/api/v1/testdata/req-info-GET.json [new file with mode: 0644]
server/api/v1/testdata/update-req-POST.json [new file with mode: 0644]
server/api/v1/testdata/worker-info-GET.json [new file with mode: 0644]

index a65a699..00f38b3 100644 (file)
--- a/boruta.go
+++ b/boruta.go
@@ -21,6 +21,9 @@
 // Worker - MuxPi with a target device, which executes the Jobs from the Server.
 package boruta
 
+//go:generate mockgen -destination=mocks/mock_requests.go -package=mocks git.tizen.org/tools/boruta Requests
+//go:generate mockgen -destination=mocks/mock_workers.go -package=mocks git.tizen.org/tools/boruta Workers
+
 import (
        "crypto/rsa"
        "net"
diff --git a/mocks/mock_requests.go b/mocks/mock_requests.go
new file mode 100644 (file)
index 0000000..b9ff76f
--- /dev/null
@@ -0,0 +1,123 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: git.tizen.org/tools/boruta (interfaces: Requests)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+       boruta "git.tizen.org/tools/boruta"
+       gomock "github.com/golang/mock/gomock"
+       reflect "reflect"
+       time "time"
+)
+
+// MockRequests is a mock of Requests interface
+type MockRequests struct {
+       ctrl     *gomock.Controller
+       recorder *MockRequestsMockRecorder
+}
+
+// MockRequestsMockRecorder is the mock recorder for MockRequests
+type MockRequestsMockRecorder struct {
+       mock *MockRequests
+}
+
+// NewMockRequests creates a new mock instance
+func NewMockRequests(ctrl *gomock.Controller) *MockRequests {
+       mock := &MockRequests{ctrl: ctrl}
+       mock.recorder = &MockRequestsMockRecorder{mock}
+       return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockRequests) EXPECT() *MockRequestsMockRecorder {
+       return m.recorder
+}
+
+// AcquireWorker mocks base method
+func (m *MockRequests) AcquireWorker(arg0 boruta.ReqID) (boruta.AccessInfo, error) {
+       ret := m.ctrl.Call(m, "AcquireWorker", arg0)
+       ret0, _ := ret[0].(boruta.AccessInfo)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// AcquireWorker indicates an expected call of AcquireWorker
+func (mr *MockRequestsMockRecorder) AcquireWorker(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireWorker", reflect.TypeOf((*MockRequests)(nil).AcquireWorker), arg0)
+}
+
+// CloseRequest mocks base method
+func (m *MockRequests) CloseRequest(arg0 boruta.ReqID) error {
+       ret := m.ctrl.Call(m, "CloseRequest", arg0)
+       ret0, _ := ret[0].(error)
+       return ret0
+}
+
+// CloseRequest indicates an expected call of CloseRequest
+func (mr *MockRequestsMockRecorder) CloseRequest(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseRequest", reflect.TypeOf((*MockRequests)(nil).CloseRequest), arg0)
+}
+
+// GetRequestInfo mocks base method
+func (m *MockRequests) GetRequestInfo(arg0 boruta.ReqID) (boruta.ReqInfo, error) {
+       ret := m.ctrl.Call(m, "GetRequestInfo", arg0)
+       ret0, _ := ret[0].(boruta.ReqInfo)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// GetRequestInfo indicates an expected call of GetRequestInfo
+func (mr *MockRequestsMockRecorder) GetRequestInfo(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRequestInfo", reflect.TypeOf((*MockRequests)(nil).GetRequestInfo), arg0)
+}
+
+// ListRequests mocks base method
+func (m *MockRequests) ListRequests(arg0 boruta.ListFilter) ([]boruta.ReqInfo, error) {
+       ret := m.ctrl.Call(m, "ListRequests", arg0)
+       ret0, _ := ret[0].([]boruta.ReqInfo)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// ListRequests indicates an expected call of ListRequests
+func (mr *MockRequestsMockRecorder) ListRequests(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRequests", reflect.TypeOf((*MockRequests)(nil).ListRequests), arg0)
+}
+
+// NewRequest mocks base method
+func (m *MockRequests) NewRequest(arg0 boruta.Capabilities, arg1 boruta.Priority, arg2 boruta.UserInfo, arg3, arg4 time.Time) (boruta.ReqID, error) {
+       ret := m.ctrl.Call(m, "NewRequest", arg0, arg1, arg2, arg3, arg4)
+       ret0, _ := ret[0].(boruta.ReqID)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// NewRequest indicates an expected call of NewRequest
+func (mr *MockRequestsMockRecorder) NewRequest(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewRequest", reflect.TypeOf((*MockRequests)(nil).NewRequest), arg0, arg1, arg2, arg3, arg4)
+}
+
+// ProlongAccess mocks base method
+func (m *MockRequests) ProlongAccess(arg0 boruta.ReqID) error {
+       ret := m.ctrl.Call(m, "ProlongAccess", arg0)
+       ret0, _ := ret[0].(error)
+       return ret0
+}
+
+// ProlongAccess indicates an expected call of ProlongAccess
+func (mr *MockRequestsMockRecorder) ProlongAccess(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProlongAccess", reflect.TypeOf((*MockRequests)(nil).ProlongAccess), arg0)
+}
+
+// UpdateRequest mocks base method
+func (m *MockRequests) UpdateRequest(arg0 *boruta.ReqInfo) error {
+       ret := m.ctrl.Call(m, "UpdateRequest", arg0)
+       ret0, _ := ret[0].(error)
+       return ret0
+}
+
+// UpdateRequest indicates an expected call of UpdateRequest
+func (mr *MockRequestsMockRecorder) UpdateRequest(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRequest", reflect.TypeOf((*MockRequests)(nil).UpdateRequest), arg0)
+}
diff --git a/mocks/mock_workers.go b/mocks/mock_workers.go
new file mode 100644 (file)
index 0000000..590e252
--- /dev/null
@@ -0,0 +1,96 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: git.tizen.org/tools/boruta (interfaces: Workers)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+       boruta "git.tizen.org/tools/boruta"
+       gomock "github.com/golang/mock/gomock"
+       reflect "reflect"
+)
+
+// MockWorkers is a mock of Workers interface
+type MockWorkers struct {
+       ctrl     *gomock.Controller
+       recorder *MockWorkersMockRecorder
+}
+
+// MockWorkersMockRecorder is the mock recorder for MockWorkers
+type MockWorkersMockRecorder struct {
+       mock *MockWorkers
+}
+
+// NewMockWorkers creates a new mock instance
+func NewMockWorkers(ctrl *gomock.Controller) *MockWorkers {
+       mock := &MockWorkers{ctrl: ctrl}
+       mock.recorder = &MockWorkersMockRecorder{mock}
+       return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockWorkers) EXPECT() *MockWorkersMockRecorder {
+       return m.recorder
+}
+
+// Deregister mocks base method
+func (m *MockWorkers) Deregister(arg0 boruta.WorkerUUID) error {
+       ret := m.ctrl.Call(m, "Deregister", arg0)
+       ret0, _ := ret[0].(error)
+       return ret0
+}
+
+// Deregister indicates an expected call of Deregister
+func (mr *MockWorkersMockRecorder) Deregister(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deregister", reflect.TypeOf((*MockWorkers)(nil).Deregister), arg0)
+}
+
+// GetWorkerInfo mocks base method
+func (m *MockWorkers) GetWorkerInfo(arg0 boruta.WorkerUUID) (boruta.WorkerInfo, error) {
+       ret := m.ctrl.Call(m, "GetWorkerInfo", arg0)
+       ret0, _ := ret[0].(boruta.WorkerInfo)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// GetWorkerInfo indicates an expected call of GetWorkerInfo
+func (mr *MockWorkersMockRecorder) GetWorkerInfo(arg0 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkerInfo", reflect.TypeOf((*MockWorkers)(nil).GetWorkerInfo), arg0)
+}
+
+// ListWorkers mocks base method
+func (m *MockWorkers) ListWorkers(arg0 boruta.Groups, arg1 boruta.Capabilities) ([]boruta.WorkerInfo, error) {
+       ret := m.ctrl.Call(m, "ListWorkers", arg0, arg1)
+       ret0, _ := ret[0].([]boruta.WorkerInfo)
+       ret1, _ := ret[1].(error)
+       return ret0, ret1
+}
+
+// ListWorkers indicates an expected call of ListWorkers
+func (mr *MockWorkersMockRecorder) ListWorkers(arg0, arg1 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWorkers", reflect.TypeOf((*MockWorkers)(nil).ListWorkers), arg0, arg1)
+}
+
+// SetGroups mocks base method
+func (m *MockWorkers) SetGroups(arg0 boruta.WorkerUUID, arg1 boruta.Groups) error {
+       ret := m.ctrl.Call(m, "SetGroups", arg0, arg1)
+       ret0, _ := ret[0].(error)
+       return ret0
+}
+
+// SetGroups indicates an expected call of SetGroups
+func (mr *MockWorkersMockRecorder) SetGroups(arg0, arg1 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGroups", reflect.TypeOf((*MockWorkers)(nil).SetGroups), arg0, arg1)
+}
+
+// SetState mocks base method
+func (m *MockWorkers) SetState(arg0 boruta.WorkerUUID, arg1 boruta.WorkerState) error {
+       ret := m.ctrl.Call(m, "SetState", arg0, arg1)
+       ret0, _ := ret[0].(error)
+       return ret0
+}
+
+// SetState indicates an expected call of SetState
+func (mr *MockWorkersMockRecorder) SetState(arg0, arg1 interface{}) *gomock.Call {
+       return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetState", reflect.TypeOf((*MockWorkers)(nil).SetState), arg0, arg1)
+}
diff --git a/server/api/v1/api.go b/server/api/v1/api.go
new file mode 100644 (file)
index 0000000..4331679
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ *  Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+// Package v1 provides HTTP API version 1 of Boruta. Through this API clients may:
+// * list, create, manage and get details of requests;
+// * list, acquire, prolong access to and get details of workers.
+package v1
+
+import (
+       "encoding/json"
+       "fmt"
+       "net/http"
+
+       "github.com/dimfeld/httptreemux"
+)
+
+// responseData type denotes data returned by HTTP request handler functions.
+// Returned values are directly converted to JSON responses.
+type responseData interface{}
+
+// reqHandler denotes function that parses HTTP request and returns responseData.
+type reqHandler func(*http.Request, map[string]string) responseData
+
+// API provides HTTP API handlers.
+type API struct {
+       r *httptreemux.TreeMux
+}
+
+// jsonMustMarshal tries to marshal responseData to JSON. Panics if error occurs.
+// TODO(mwereski): check type of data.
+func jsonMustMarshal(data responseData) []byte {
+       res, err := json.Marshal(data)
+       if err != nil {
+               msg := "unable to marshal JSON:" + err.Error()
+               panic(newServerError(ErrInternalServerError, msg))
+       }
+       return res
+}
+
+// panicHandler is intended as a httptreemux PanicHandler function. It sends
+// InternalServerError with details to client whose request caused panic.
+func panicHandler(w http.ResponseWriter, r *http.Request, err interface{}) {
+       var reason interface{}
+       var status = http.StatusInternalServerError
+       switch srvErr := err.(type) {
+       case *serverError:
+               reason = srvErr.Err
+               status = srvErr.Status
+       default:
+               reason = srvErr
+       }
+       // Because marshalling JSON may fail, data is sent in plaintext.
+       w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+       w.WriteHeader(status)
+       w.Write([]byte(fmt.Sprintf("Internal Server Error:\n%s", reason)))
+}
+
+// routerSetHandler wraps fn by adding HTTP headers, handling error and
+// marshalling. Such wrapped function is then registered in the API router as a
+// handler for given path, provided methods and HTTP success status that should
+// be used when funcion succeeds.
+func routerSetHandler(grp *httptreemux.Group, path string, fn reqHandler,
+       status int, methods ...string) {
+       newHandler := func(handle reqHandler) httptreemux.HandlerFunc {
+               return func(w http.ResponseWriter, r *http.Request,
+                       ps map[string]string) {
+                       status := status
+                       rdata := handle(r, ps)
+                       if data, isErr := rdata.(*serverError); isErr &&
+                               data != nil {
+                               status = data.Status
+                       }
+                       if status != http.StatusNoContent {
+                               w.Header().Set("Content-Type", "application/json")
+                       }
+                       w.WriteHeader(status)
+                       if status != http.StatusNoContent {
+                               w.Write(jsonMustMarshal(rdata))
+                       }
+               }
+       }
+       for _, method := range methods {
+               grp.Handle(method, path, newHandler(fn))
+       }
+}
+
+// NewAPI takes router and registers HTTP API in it. httptreemux.PanicHandler
+// function is set. Also other setting of the router may be modified.
+func NewAPI(router *httptreemux.TreeMux) (api *API) {
+       api = new(API)
+
+       api.r = router
+       api.r.PanicHandler = panicHandler
+
+       root := api.r.NewGroup("/api/v1")
+       reqs := root.NewGroup("/reqs")
+       workers := root.NewGroup("/workers")
+
+       // Requests API
+       routerSetHandler(reqs, "/", api.listRequestsHandler, http.StatusOK,
+               http.MethodGet, http.MethodHead)
+       routerSetHandler(reqs, "/list", api.listRequestsHandler, http.StatusOK,
+               http.MethodPost)
+       routerSetHandler(reqs, "/", api.newRequestHandler, http.StatusCreated,
+               http.MethodPost)
+       routerSetHandler(reqs, "/:id", api.getRequestInfoHandler, http.StatusOK,
+               http.MethodGet, http.MethodHead)
+       routerSetHandler(reqs, "/:id", api.updateRequestHandler,
+               http.StatusNoContent, http.MethodPost)
+       routerSetHandler(reqs, "/:id/close", api.closeRequestHandler,
+               http.StatusNoContent, http.MethodPost)
+       routerSetHandler(reqs, "/:id/acquire_worker", api.acquireWorkerHandler,
+               http.StatusOK, http.MethodPost)
+       routerSetHandler(reqs, "/:id/prolong", api.prolongAccessHandler,
+               http.StatusNoContent, http.MethodPost)
+
+       // Workers API
+       routerSetHandler(workers, "/", api.listWorkersHandler, http.StatusOK,
+               http.MethodGet, http.MethodHead)
+       routerSetHandler(workers, "/list", api.listWorkersHandler, http.StatusOK,
+               http.MethodPost)
+       routerSetHandler(workers, "/:id", api.getWorkerInfoHandler, http.StatusOK,
+               http.MethodGet, http.MethodHead)
+
+       return
+}
diff --git a/server/api/v1/api_test.go b/server/api/v1/api_test.go
new file mode 100644 (file)
index 0000000..038b678
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ *  Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package v1
+
+import (
+       "errors"
+       "flag"
+       "io"
+       "io/ioutil"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+
+       "git.tizen.org/tools/boruta/mocks"
+       "github.com/dimfeld/httptreemux"
+       "github.com/golang/mock/gomock"
+       "github.com/stretchr/testify/assert"
+)
+
+const contentTypeJSON = "application/json"
+
+var update bool
+
+type requestTest struct {
+       name        string
+       path        string
+       methods     []string
+       json        string
+       contentType string
+       status      int
+}
+
+type allMocks struct {
+       ctrl *gomock.Controller
+       rq   *mocks.MockRequests
+       wm   *mocks.MockWorkers
+}
+
+func TestMain(m *testing.M) {
+       flag.BoolVar(&update, "update", false, "update testdata")
+       flag.Parse()
+       os.Exit(m.Run())
+}
+
+func initTest(t *testing.T) (*assert.Assertions, *allMocks, *API) {
+       ctrl := gomock.NewController(t)
+       m := &allMocks{
+               ctrl: ctrl,
+               rq:   mocks.NewMockRequests(ctrl),
+               wm:   mocks.NewMockWorkers(ctrl),
+       }
+       return assert.New(t), m, NewAPI(httptreemux.New())
+}
+
+func (m *allMocks) finish() {
+       m.ctrl.Finish()
+}
+
+func runTests(assert *assert.Assertions, api *API, tests []requestTest) {
+       srv := httptest.NewServer(api.r)
+       defer srv.Close()
+       var req *http.Request
+       var err error
+       var tcaseErrStr string
+
+       for _, test := range tests {
+               tcaseErrStr = test.name + ": FAILED"
+               for _, method := range test.methods {
+                       // prepare and do HTTP request
+                       if test.json == "" {
+                               req, err = http.NewRequest(method,
+                                       srv.URL+test.path, nil)
+                       } else {
+                               req, err = http.NewRequest(method,
+                                       srv.URL+test.path,
+                                       strings.NewReader(test.json))
+                       }
+                       assert.Nil(err)
+                       req.Header["Content-Type"] = []string{test.contentType}
+                       resp, err := srv.Client().Do(req)
+                       assert.Nil(err)
+                       defer resp.Body.Close()
+
+                       // read expected results from file or generate the file
+                       tdata := filepath.Join("testdata", test.name+"-"+method+".json")
+                       body, err := ioutil.ReadAll(resp.Body)
+                       assert.Nil(err)
+                       if update && method != http.MethodHead {
+                               err = ioutil.WriteFile(tdata, body, 0644)
+                               assert.Nil(err)
+                       }
+
+                       // check status code
+                       assert.Equal(test.status, resp.StatusCode, tcaseErrStr)
+                       if resp.StatusCode == http.StatusNoContent {
+                               continue
+                       }
+                       // check content type
+                       assert.Equal(test.contentType,
+                               resp.Header.Get("Content-Type"))
+                       if method == http.MethodHead {
+                               assert.Zero(len(body), tcaseErrStr)
+                               continue
+                       }
+                       // if update was set then file was just generated,
+                       // so there's no sense in rereading and comparing it.
+                       if update {
+                               continue
+                       }
+                       // check result JSON
+                       expected, err := ioutil.ReadFile(tdata)
+                       assert.Nil(err, tcaseErrStr)
+                       assert.JSONEq(string(expected), string(body), tcaseErrStr)
+               }
+       }
+}
+
+func TestNewAPI(t *testing.T) {
+       assert, m, api := initTest(t)
+       assert.NotNil(api)
+       m.finish()
+}
+
+func TestNewServerError(t *testing.T) {
+       assert := assert.New(t)
+       badRequest := &serverError{
+               Err:    "invalid request: foo",
+               Status: http.StatusBadRequest,
+       }
+       nobody := "no body provided in HTTP request"
+       missingBody := &serverError{
+               Err:    nobody,
+               Status: http.StatusBadRequest,
+       }
+       notImplemented := &serverError{
+               Err:    ErrNotImplemented.Error(),
+               Status: http.StatusNotImplemented,
+       }
+       internalErr := &serverError{
+               Err:    ErrInternalServerError.Error(),
+               Status: http.StatusInternalServerError,
+       }
+       customErr := &serverError{
+               Err:    "invalid request: more details",
+               Status: http.StatusBadRequest,
+       }
+       assert.Equal(badRequest, newServerError(errors.New("foo")))
+       assert.Equal(missingBody, newServerError(io.EOF))
+       assert.Equal(notImplemented, newServerError(ErrNotImplemented))
+       assert.Equal(internalErr, newServerError(ErrInternalServerError))
+       assert.Equal(customErr, newServerError(ErrBadRequest, "more details"))
+       assert.Nil(newServerError(nil))
+}
+
+func TestJsonMustMarshal(t *testing.T) {
+       assert := assert.New(t)
+       assert.Panics(func() { jsonMustMarshal(make(chan bool)) })
+}
+
+func TestPanicHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+       msg := "Test PanicHandler: server panic"
+       otherErr := "some error"
+       tests := [...]struct {
+               name string
+               path string
+               err  interface{}
+       }{
+               {
+                       name: "panic-server-error",
+                       path: "/priv/api/panic/srvErr/",
+                       err: &serverError{
+                               Err:    msg,
+                               Status: http.StatusInternalServerError,
+                       },
+               },
+               {
+                       name: "panic-other-error",
+                       path: "/priv/api/panic/otherErr/",
+                       err:  otherErr,
+               },
+       }
+       contentType := "text/plain; charset=utf-8"
+
+       newHandler := func(err interface{}) reqHandler {
+               return func(r *http.Request, ps map[string]string) responseData {
+                       panic(err)
+               }
+       }
+       for _, test := range tests {
+               routerSetHandler(api.r.NewGroup("/"), test.path, newHandler(test.err),
+                       http.StatusOK, http.MethodGet)
+       }
+       var tcaseErrStr string
+       srv := httptest.NewServer(api.r)
+       assert.NotNil(srv)
+       defer srv.Close()
+       for _, test := range tests {
+               tcaseErrStr = test.name + ": FAILED"
+               resp, err := http.Get(srv.URL + test.path)
+               assert.Nil(err)
+               tdata := filepath.Join("testdata", test.name+".txt")
+               body, err := ioutil.ReadAll(resp.Body)
+               assert.Nil(err)
+               if update {
+                       ioutil.WriteFile(tdata, body, 0644)
+               }
+               expected, err := ioutil.ReadFile(tdata)
+               assert.Nil(err)
+               assert.Equal(http.StatusInternalServerError, resp.StatusCode, tcaseErrStr)
+               assert.Equal(contentType, resp.Header.Get("Content-Type"), tcaseErrStr)
+               assert.Equal(expected, body, tcaseErrStr)
+       }
+}
diff --git a/server/api/v1/error.go b/server/api/v1/error.go
new file mode 100644 (file)
index 0000000..31ae7ba
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ *  Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+// File server/api/v1/errors.go provides errors that may occur when interacting
+// with Boruta HTTP API.
+
+package v1
+
+import (
+       "errors"
+       "io"
+       "net/http"
+)
+
+// serverError represents error that occured while creating response.
+type serverError struct {
+       // Err contains general error string.
+       Err string `json:"error"`
+       // Status contains HTTP error code that should be returned with the error.
+       Status int `json:"-"`
+}
+
+var (
+       // ErrNotImplemented is returned when requested functionality isn't
+       // implemented yet.
+       ErrNotImplemented = errors.New("not implemented yet")
+       // ErrInternalServerError is returned when serious error in the server
+       // occurs which isn't users' fault.
+       ErrInternalServerError = errors.New("internal server error")
+       // ErrBadRequest is returned when User request is invalid.
+       ErrBadRequest = errors.New("invalid request")
+)
+
+// newServerError provides pointer to initialized serverError.
+func newServerError(err error, details ...string) (ret *serverError) {
+       if err == nil {
+               return nil
+       }
+
+       ret = new(serverError)
+
+       ret.Err = err.Error()
+       if len(details) > 0 {
+               ret.Err += ": " + details[0]
+       }
+
+       switch err {
+       case ErrNotImplemented:
+               ret.Status = http.StatusNotImplemented
+       case ErrInternalServerError:
+               ret.Status = http.StatusInternalServerError
+       case ErrBadRequest:
+               ret.Status = http.StatusBadRequest
+       case io.EOF:
+               ret.Err = "no body provided in HTTP request"
+               ret.Status = http.StatusBadRequest
+       default:
+               ret.Err = ErrBadRequest.Error() + ": " + ret.Err
+               ret.Status = http.StatusBadRequest
+       }
+
+       return
+}
diff --git a/server/api/v1/handlers.go b/server/api/v1/handlers.go
new file mode 100644 (file)
index 0000000..def6731
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ *  Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+// File server/api/v1/handlers.go contains all handlers that are used in v1 API.
+
+package v1
+
+import (
+       "net/http"
+)
+
+// newRequestHandler parses HTTP request for creating new Boruta request and
+// calls NewRequest().
+func (api *API) newRequestHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "new request")
+}
+
+// closeRequestHandler parses HTTP request for closing existing Boruta request
+// and calls CloseRequest().
+func (api *API) closeRequestHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "close request")
+}
+
+// updateRequestHandler parses HTTP request for modification of existing Boruta
+// request and calls appropriate methods: SetRequestValidAfter(),
+// SetRequestDeadline() and SetRequestPriority().
+func (api *API) updateRequestHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "update request")
+}
+
+// getRequestInfoHandler parses HTTP request for getting information about Boruta
+// request and calls GetRequestInfo().
+func (api *API) getRequestInfoHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "get request info")
+}
+
+// listRequestsHandler parses HTTP request for listing Boruta requests and calls
+// ListRequests().
+func (api *API) listRequestsHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "list requests")
+}
+
+// acquireWorkerHandler parses HTTP request for acquiring worker for Boruta
+// request and calls AcquireWorker().
+func (api *API) acquireWorkerHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "acquire worker")
+}
+
+// prolongAccessHandler parses HTTP request for prolonging previously acquired
+// worker and calls ProlongAccess().
+func (api *API) prolongAccessHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "prolong access")
+}
+
+// listWorkersHandler parses HTTP request for listing workers and calls ListWorkers().
+func (api *API) listWorkersHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "list workers")
+}
+
+// getWorkerInfoHandler parses HTTP request for obtaining worker information and
+// calls GetWorkerInfo().
+func (api *API) getWorkerInfoHandler(r *http.Request, ps map[string]string) responseData {
+       return newServerError(ErrNotImplemented, "get worker info")
+}
diff --git a/server/api/v1/handlers_test.go b/server/api/v1/handlers_test.go
new file mode 100644 (file)
index 0000000..69bd11c
--- /dev/null
@@ -0,0 +1,199 @@
+/* *  Copyright (c) 2017-2018 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package v1
+
+import (
+       "net/http"
+       "testing"
+)
+
+func TestNewRequestHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "new-req",
+                       path:        "/api/v1/reqs/",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestCloseRequestHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "close-req",
+                       path:        "/api/v1/reqs/8/close",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestUpdateRequestHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "update-req",
+                       path:        "/api/v1/reqs/8",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestGetRequestInfoHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "req-info",
+                       path:        "/api/v1/reqs/8",
+                       methods:     []string{http.MethodGet, http.MethodHead},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestListRequestsHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "list-reqs-all",
+                       path:        "/api/v1/reqs/",
+                       methods:     []string{http.MethodGet, http.MethodHead},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+               {
+                       name:        "list-reqs-filter",
+                       path:        "/api/v1/reqs/list",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestAcquireWorkerHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "acquire-worker",
+                       path:        "/api/v1/reqs/8/acquire_worker",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestProlongAccessHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "prolong-access",
+                       path:        "/api/v1/reqs/8/prolong",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestListWorkersHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "list-workers-all",
+                       path:        "/api/v1/workers/",
+                       methods:     []string{http.MethodGet, http.MethodHead},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+               {
+                       name:        "list-workers-filter",
+                       path:        "/api/v1/workers/list",
+                       methods:     []string{http.MethodPost},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
+
+func TestGetWorkerInfoHandler(t *testing.T) {
+       assert, m, api := initTest(t)
+       defer m.finish()
+
+       tests := []requestTest{
+               {
+                       name:        "worker-info",
+                       path:        "/api/v1/workers/8",
+                       methods:     []string{http.MethodGet, http.MethodHead},
+                       json:        ``,
+                       contentType: contentTypeJSON,
+                       status:      http.StatusNotImplemented,
+               },
+       }
+
+       runTests(assert, api, tests)
+}
diff --git a/server/api/v1/testdata/acquire-worker-POST.json b/server/api/v1/testdata/acquire-worker-POST.json
new file mode 100644 (file)
index 0000000..6cada1f
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: acquire worker"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/close-req-POST.json b/server/api/v1/testdata/close-req-POST.json
new file mode 100644 (file)
index 0000000..2938a2a
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: close request"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/list-reqs-all-GET.json b/server/api/v1/testdata/list-reqs-all-GET.json
new file mode 100644 (file)
index 0000000..ed5be09
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: list requests"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/list-reqs-filter-POST.json b/server/api/v1/testdata/list-reqs-filter-POST.json
new file mode 100644 (file)
index 0000000..ed5be09
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: list requests"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/list-workers-all-GET.json b/server/api/v1/testdata/list-workers-all-GET.json
new file mode 100644 (file)
index 0000000..7fb8fd4
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: list workers"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/list-workers-filter-POST.json b/server/api/v1/testdata/list-workers-filter-POST.json
new file mode 100644 (file)
index 0000000..7fb8fd4
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: list workers"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/new-req-POST.json b/server/api/v1/testdata/new-req-POST.json
new file mode 100644 (file)
index 0000000..620302e
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: new request"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/panic-other-error.txt b/server/api/v1/testdata/panic-other-error.txt
new file mode 100644 (file)
index 0000000..577a25d
--- /dev/null
@@ -0,0 +1,2 @@
+Internal Server Error:
+some error
\ No newline at end of file
diff --git a/server/api/v1/testdata/panic-server-error.txt b/server/api/v1/testdata/panic-server-error.txt
new file mode 100644 (file)
index 0000000..a1e327d
--- /dev/null
@@ -0,0 +1,2 @@
+Internal Server Error:
+Test PanicHandler: server panic
\ No newline at end of file
diff --git a/server/api/v1/testdata/prolong-access-POST.json b/server/api/v1/testdata/prolong-access-POST.json
new file mode 100644 (file)
index 0000000..02eed3c
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: prolong access"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/req-info-GET.json b/server/api/v1/testdata/req-info-GET.json
new file mode 100644 (file)
index 0000000..3c5978f
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: get request info"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/update-req-POST.json b/server/api/v1/testdata/update-req-POST.json
new file mode 100644 (file)
index 0000000..dba72fb
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: update request"}
\ No newline at end of file
diff --git a/server/api/v1/testdata/worker-info-GET.json b/server/api/v1/testdata/worker-info-GET.json
new file mode 100644 (file)
index 0000000..c021cc4
--- /dev/null
@@ -0,0 +1 @@
+{"error":"not implemented yet: get worker info"}
\ No newline at end of file