--- /dev/null
+/*
+ * 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 api aggregates all availabe Boruta HTTP API versions.
+package api
+
+import (
+ "fmt"
+ "net/http"
+
+ . "git.tizen.org/tools/boruta"
+ util "git.tizen.org/tools/boruta/http"
+ "git.tizen.org/tools/boruta/http/server/api/v1"
+ "github.com/dimfeld/httptreemux"
+)
+
+// defaultAPI contains information which version of the API is treated as default.
+// It should always be latest stable version.
+const defaultAPI = v1.Version
+
+// API provides HTTP API handlers.
+type API struct {
+ r *httptreemux.TreeMux
+ reqs Requests
+ workers Workers
+}
+
+// panicHandler is desired as 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 *util.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)))
+}
+
+// redirectToDefault redirects requests which lack API version information to
+// default API. For example, if "v1" is the default API version, then request
+// with path "/api/reqs/list" will be redirected to "/api/v1/reqs/list".
+func redirectToDefault(w http.ResponseWriter, r *http.Request,
+ p map[string]string) {
+ u := *r.URL
+ u.Path = "/api/" + defaultAPI + "/" + p["path"]
+ http.Redirect(w, r, u.String(), http.StatusPermanentRedirect)
+}
+
+// setDefaultAPI register handler for API calls that lack API version in path.
+func setDefaultAPIRedirect(prefix *httptreemux.Group) {
+ for _, method := range [...]string{
+ http.MethodGet,
+ http.MethodHead,
+ http.MethodPost,
+ http.MethodPut,
+ http.MethodPatch,
+ http.MethodDelete,
+ http.MethodConnect,
+ http.MethodOptions,
+ http.MethodTrace,
+ } {
+ prefix.Handle(method, "/*path", redirectToDefault)
+ }
+}
+
+// NewAPI registers all available Boruta HTTP APIs on provided router. It also
+// sets panicHandler for all panics that may occur in any API. Finally it sets
+// default API version to which requests that miss API version are redirected.
+func NewAPI(router *httptreemux.TreeMux, requestsAPI Requests,
+ workersAPI Workers) (api *API) {
+ api = new(API)
+
+ api.reqs = requestsAPI
+ api.workers = workersAPI
+
+ api.r = router
+ api.r.PanicHandler = panicHandler
+ api.r.RedirectBehavior = httptreemux.Redirect308
+
+ all := api.r.NewGroup("/api")
+ v1group := all.NewGroup("/" + v1.Version)
+
+ _ = v1.NewAPI(v1group, api.reqs, api.workers)
+ setDefaultAPIRedirect(all)
+
+ return
+}
--- /dev/null
+/*
+ * 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 api
+
+import (
+ "errors"
+ "flag"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ util "git.tizen.org/tools/boruta/http"
+ "git.tizen.org/tools/boruta/matcher"
+ "git.tizen.org/tools/boruta/requests"
+ "git.tizen.org/tools/boruta/workers"
+ "github.com/dimfeld/httptreemux"
+ "github.com/stretchr/testify/assert"
+)
+
+var update bool
+
+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, *API) {
+ wm := workers.NewWorkerList()
+ return assert.New(t), NewAPI(httptreemux.New(),
+ requests.NewRequestQueue(wm, matcher.NewJobsManager(wm)), wm)
+}
+
+func TestPanicHandler(t *testing.T) {
+ assert, api := initTest(t)
+ 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: &util.ServerError{
+ Err: msg,
+ Status: http.StatusInternalServerError,
+ },
+ },
+ {
+ name: "panic-other-error",
+ path: "/priv/api/panic/otherErr/",
+ err: otherErr,
+ },
+ }
+ contentType := "text/plain; charset=utf-8"
+ grp := api.r.NewGroup("/")
+
+ newHandler := func(err interface{}) httptreemux.HandlerFunc {
+ return func(_ http.ResponseWriter, _ *http.Request, _ map[string]string) {
+ panic(err)
+ }
+ }
+ for _, test := range tests {
+ grp.GET(test.path, newHandler(test.err))
+ }
+ srv := httptest.NewServer(api.r)
+ assert.NotNil(srv)
+ defer srv.Close()
+ for _, test := range tests {
+ 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)
+ assert.Equal(contentType, resp.Header.Get("Content-Type"))
+ assert.Equal(expected, body)
+ }
+}
+
+func TestNewAPI(t *testing.T) {
+ assert, api := initTest(t)
+ assert.NotNil(api)
+}
+
+func TestRedirectToDefault(t *testing.T) {
+ assert, api := initTest(t)
+ srv := httptest.NewServer(api.r)
+ defer srv.Close()
+
+ reqPath := "/api/reqs"
+ redirPath := "/api/" + defaultAPI + "/reqs/"
+ var i int
+ redirCheck := func(req *http.Request, via []*http.Request) error {
+ switch {
+ case i == 0:
+ // Check if proper status code was set.
+ assert.Equal(http.StatusPermanentRedirect, req.Response.StatusCode)
+ // Check if method hasn't changed.
+ assert.Equal(via[0].Method, req.Method, "first redirection")
+ case i == 1:
+ // Check if proper URL was set.
+ assert.Equal(srv.URL+redirPath, req.URL.String())
+ // Check if method hasn't changed.
+ assert.Equal(via[0].Method, req.Method, "second redirection")
+ // It isn't our business if default API does more redirects, but
+ // return error when there's more than 10 redirects (as Go does).
+ case i > 9:
+ return errors.New("too many redirects")
+ }
+ i++
+ return nil
+ }
+
+ client := http.Client{
+ CheckRedirect: redirCheck,
+ }
+
+ // Response is checked in redirCheck.
+ _, err := client.Post(srv.URL+reqPath, "text/plain", nil)
+ assert.Nil(err)
+}
import (
"encoding/json"
- "fmt"
"net/http"
"regexp"
"strconv"
// reqHandler denotes function that parses HTTP request and returns responseData.
type reqHandler func(*http.Request, map[string]string) responseData
+// Version contains version string of the API.
+const Version = "v1"
+
// API provides HTTP API handlers.
type API struct {
- r *httptreemux.TreeMux
+ r *httptreemux.Group
reqs Requests
workers Workers
}
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 *util.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
}
}
-// NewAPI takes router and registers HTTP API in it. httptreemux.PanicHandler
+// NewAPI takes router and registers HTTP API in it. htttreemux.PanicHandler
// function is set. Also other setting of the router may be modified.
-func NewAPI(router *httptreemux.TreeMux, requestsAPI Requests,
+func NewAPI(router *httptreemux.Group, requestsAPI Requests,
workersAPI Workers) (api *API) {
api = new(API)
api.workers = workersAPI
api.r = router
- api.r.PanicHandler = panicHandler
- root := api.r.NewGroup("/api/v1")
- reqs := root.NewGroup("/reqs")
- workers := root.NewGroup("/workers")
+ reqs := api.r.NewGroup("/reqs")
+ workers := api.r.NewGroup("/workers")
// Requests API
routerSetHandler(reqs, "/", api.listRequestsHandler, http.StatusOK,
"testing"
. "git.tizen.org/tools/boruta"
- util "git.tizen.org/tools/boruta/http"
"git.tizen.org/tools/boruta/mocks"
"github.com/dimfeld/httptreemux"
"github.com/golang/mock/gomock"
os.Exit(m.Run())
}
-func initTest(t *testing.T) (*assert.Assertions, *allMocks, *API) {
+func initTest(t *testing.T) (*assert.Assertions, *allMocks, *httptreemux.TreeMux) {
+ r := httptreemux.New()
ctrl := gomock.NewController(t)
m := &allMocks{
ctrl: ctrl,
rq: mocks.NewMockRequests(ctrl),
wm: mocks.NewMockWorkers(ctrl),
}
- return assert.New(t), m, NewAPI(httptreemux.New(), m.rq, m.wm)
+ _ = NewAPI(r.NewGroup("/api/"+Version), m.rq, m.wm)
+ return assert.New(t), m, r
}
func (m *allMocks) finish() {
return
}
-func runTests(assert *assert.Assertions, api *API, tests []requestTest) {
- srv := httptest.NewServer(api.r)
+func runTests(assert *assert.Assertions, r *httptreemux.TreeMux, tests []requestTest) {
+ srv := httptest.NewServer(r)
defer srv.Close()
var req *http.Request
var err error
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: &util.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)
- }
-}
-
func TestParseReqID(t *testing.T) {
assert := assert.New(t)
reqid, err := parseReqID("1")
)
func TestNewRequestHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
prefix := "new-req-"
malformedJSONTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestCloseRequestHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
methods := []string{http.MethodPost}
notFoundTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestUpdateRequestHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
methods := []string{http.MethodPost}
notFoundTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestGetRequestInfoHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
methods := []string{http.MethodGet, http.MethodHead}
invalidIDTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestListRequestsHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
deadline, err := time.Parse(dateLayout, future)
malformedJSONTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestAcquireWorkerHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
methods := []string{http.MethodPost}
invalidIDTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestProlongAccessHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
methods := []string{http.MethodPost}
invalidIDTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestListWorkersHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
armCaps := make(Capabilities)
malformedJSONTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestGetWorkerInfoHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
prefix := "worker-info-"
notFoundTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestSetWorkerStateHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
prefix := "worker-set-state-"
malformedJSONTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestSetWorkerGroupsHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
path := "/api/v1/workers/%s/setgroups"
malformedJSONTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}
func TestDeregisterWorkerHandler(t *testing.T) {
- assert, m, api := initTest(t)
+ assert, m, r := initTest(t)
defer m.finish()
prefix := "worker-deregister-"
notFoundTest,
}
- runTests(assert, api, tests)
+ runTests(assert, r, tests)
}