diff --git a/README.rst b/README.rst
index 43d4890..49c0219 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,10 @@
Welcome to *Kapow!*
===================
+.. image:: https://goreportcard.com/badge/github.com/bbva/kapow
+ :target: https://goreportcard.com/report/github.com/bbva/kapow
+
+
With *Kapow!* you can publish simple **shell scripts** as **HTTP services** easily.
*Kapow!* with an example
@@ -38,5 +42,3 @@ This is the only line you'll need:
Mention license and contributing
-
-
diff --git a/doc/README.md b/doc/README.md
deleted file mode 100644
index acc8529..0000000
--- a/doc/README.md
+++ /dev/null
@@ -1,226 +0,0 @@
-# Installing Kapow!
-
-Kapow! has a reference implementation in Go that is under active development right
-now. If you want to start using Kapow! you can:
-* Download a binary (linux, at this moment) from our
-[releases](https://github.com/BBVA/kapow/releases) section
-* Install the package with the `get` command (you need the Go runtime installed
-and [configured](https://golang.org/cmd/go/))
-```sh
-go get -u github.com/BBVA/kapow
-```
-
-
-# Examples
-
-Below are some examples on how to define and invoke routes in Kapow!
-
-As you will see `kapow` binary is both a server and a CLI that you can use to configure
-a running server. The server exposes an [API](/spec#http-control-api) that you
-can use directly if you want.
-
-In order to get information from the request that fired the script execution and
-to help you compose the response, the server exposes
-some [resources](/spec#handlers) to interact with from the script.
-
-
-## The mandatory Hello World (for WWW fans)
-
-First, you create a pow file named `greet.pow` with the following contents:
-```sh
-kapow route add /greet -c 'name=$(kapow get /request/params/name); echo Hello ${name:-World} | kapow set /response/body'
-```
-
-note that you have to escape it as the command will run on a shell itself. Then, you
-execute:
-```sh
-kapow server greet.pow
-```
-
-to start a Kapow! server exposing your service. Now you can check that it works
-as intended with good ole’ `curl`:
-```sh
-curl localhost:8080/greet
-Hello World
-
-curl localhost:8080/greet?name=friend
-Hello friend
-```
-
-If you want to work with JSON you can use this version of the pow
-`greet-json.pow`
-```sh
-kapow route add -X POST /greet -c 'who=$(kapow get /request/body | jq -r .name); kapow set /response/status 201; jq --arg value "${who:-World}" -n \{name:\$value\} | kapow set /response/body'
-```
-
-that uses [jq](https://stedolan.github.io/jq/) to allow you to work with JSON
-from the command line. Check that it works with
-```sh
-curl -X POST -H 'Content-Type: application/json' -d '{"name": "friend"}' localhost:8080/greet
-{"name": "friend" }
-
-curl -X POST -H 'Content-Type: application/json' -d '' localhost:8080/greet
-{"name": "World"}
-```
-
-
-## The mandatory Echo (for UNIX fans)
-
-First, you create a pow file named `echo.pow` with the following contents:
-```sh
-kapow route add -X POST /echo -c 'kapow get /request/body | kapow set /response/body'
-```
-
-then, you execute:
-```sh
-kapow server echo.pow
-```
-
-and you can check that it works as intended with good ole’ `curl`:
-```sh
-curl -X POST -d '1,2,3... testing' localhost:8080/echo
-1, 2, 3, 4, 5, 6, 7, 8, 9, testing
-```
-
-If you send a big file and want to see the content back as a real-time stream
-you can use this version `echo-stream.pow`
-```sh
-kapow route add -X POST /echo -c 'kapow get /request/body | kapow set /response/stream'
-```
-
-
-## The multiline fun
-
-Unless you're a hardcore Perl golfer, you'll probably need to write your stuff
-over more than one line in order to avoid the mess we saw on our JSON greet
-version.
-
-Don't worry, we need to write several lines, too. Bash, in its magnificent
-UNIX® style, provides us with the
-[here-documents](https://www.gnu.org/software/bash/manual/bash.html#Here-Documents)
-mechanism that we can leverage precisely for this purpose.
-
-Imagine that we want to return both the standard output and a generated file from a
-command execution. Let's write a `log-and-stuff.pow` file with the following content:
-```sh
-kapow route add /log_and_stuff - <<-'EOF'
- echo this is a quite long sentence and other stuff | tee log.txt | kapow set /response/body
- cat log.txt | kapow set /response/body
-EOF
-```
-
-then we serve it with `kapow`:
-```sh
-kapow server log-and-stuff.pow
-```
-
-Yup. As simple as that. You can check it.
-```sh
-curl localhost:8080/log_and_stuff
-this is a quite long sentence and other stuff
-this is a quite long sentence and other stuff
-```
-
-
-## Interact with other systems
-
-You can leverage all the power of the shell in your scripts and interact with
-other systems by using all the available tools. Write a
-`log-and-stuff-callback.pow` file with the following content:
-```sh
-kapow route add /log_and_stuff - <<-'EOF'
- callback_url="$(kapow get /request/params/callback)"
- echo this is a quite long sentence and other stuff | tee log.txt | kapow set /response/body
- echo sending to $callback_url | kapow set /response/body
- curl -X POST --data-binary @log.txt $callback_url | kapow set /response/body
-EOF
-```
-
-serve it with `kapow`:
-```sh
-kapow server log-and-stuff-callback.pow
-```
-
-and finally check it.
-```sh
-curl localhost:8080/log_and_stuff?callback=nowhere.com
-this is a quite long sentence and other stuff
-sending to nowhere.com
-
-
405 Not Allowed
-
-
405 Not Allowed
-
nginx
-
-
-```
-
-You must be aware that you must have all the dependencies you use in your
-scripts installed in the host that will run the Kapow! server.
-
-In addition, a pow file can contain as many routes as you like, so you can start
-a server with several routes configured in one shot.
-
-
-# Sample Docker usage
-
-## Clone the project
-
-```sh
-git clone https://github.com/BBVA/kapow.git
-```
-
-
-## Build the kapow! docker image
-
-```sh
-make docker
-```
-
-Now you have a container image with all the above pow files copied in /tmp so
-you can start each example by running
-```sh
-docker run --rm -p 8080:8080 docker server example.pow
-```
-
-
-## Build a docker image for running the nmap example
-```sh
-cd /path/to/kapow/poc/examples/nmap; docker build -t kapow-nmap .
-```
-
-
-## Run kapow
-```sh
-docker run \
- -d \
- -p 8080:8080 \
- kapow-nmap
-```
-which will output something like this:
-```sh
-e7da20c7d9a39624b5c56157176764671e5d2d8f1bf306b3ede898d66fe3f4bf
-```
-
-
-## Test /list endpoint
-
-In another terminal, try running:
-```sh
-curl http://localhost:8080/list/github.com
-```
-
-which will respond something like:
-```sh
-Starting Nmap 7.70 ( https://nmap.org ) at 2019-05-10 14:01 UTC
-Nmap scan report for github.com (140.82.118.3)
-rDNS record for 140.82.118.3: lb-140-82-118-3-ams.github.com
-Nmap done: 1 IP address (0 hosts up) scanned in 0.04 seconds
-```
-
-et voilà !
-
-
-# License
-
-This project is distributed under the [Apache License 2.0](/LICENSE).
diff --git a/internal/client/get.go b/internal/client/get.go
index d392fe0..f0816ee 100644
--- a/internal/client/get.go
+++ b/internal/client/get.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/get_test.go b/internal/client/get_test.go
index 60c4b6d..4d4d21e 100644
--- a/internal/client/get_test.go
+++ b/internal/client/get_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/route_add.go b/internal/client/route_add.go
index e008203..f1cdffe 100644
--- a/internal/client/route_add.go
+++ b/internal/client/route_add.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/route_add_test.go b/internal/client/route_add_test.go
index 6c74e28..e66779f 100644
--- a/internal/client/route_add_test.go
+++ b/internal/client/route_add_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/route_list.go b/internal/client/route_list.go
index b2c2053..a22f86f 100644
--- a/internal/client/route_list.go
+++ b/internal/client/route_list.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/route_list_test.go b/internal/client/route_list_test.go
index bb6a80f..e5eadb5 100644
--- a/internal/client/route_list_test.go
+++ b/internal/client/route_list_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/route_remove.go b/internal/client/route_remove.go
index d26f599..53329ed 100644
--- a/internal/client/route_remove.go
+++ b/internal/client/route_remove.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/route_remove_test.go b/internal/client/route_remove_test.go
index ac42b15..21fa460 100644
--- a/internal/client/route_remove_test.go
+++ b/internal/client/route_remove_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/set.go b/internal/client/set.go
index 0df76d1..745c1ab 100644
--- a/internal/client/set.go
+++ b/internal/client/set.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client
import (
diff --git a/internal/client/set_test.go b/internal/client/set_test.go
index f18d76c..d13f5b9 100644
--- a/internal/client/set_test.go
+++ b/internal/client/set_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package client_test
import (
diff --git a/internal/cmd/get.go b/internal/cmd/get.go
index 1fdbe56..497a859 100644
--- a/internal/cmd/get.go
+++ b/internal/cmd/get.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package cmd
import (
diff --git a/internal/cmd/route.go b/internal/cmd/route.go
index 51d9499..3925517 100644
--- a/internal/cmd/route.go
+++ b/internal/cmd/route.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package cmd
import (
diff --git a/internal/cmd/server.go b/internal/cmd/server.go
index f176b91..14719c8 100644
--- a/internal/cmd/server.go
+++ b/internal/cmd/server.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package cmd
import (
diff --git a/internal/cmd/set.go b/internal/cmd/set.go
index 1f81a36..f3e2f95 100644
--- a/internal/cmd/set.go
+++ b/internal/cmd/set.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package cmd
import (
diff --git a/internal/cmd/validations.go b/internal/cmd/validations.go
index 2e99b98..19012c8 100644
--- a/internal/cmd/validations.go
+++ b/internal/cmd/validations.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package cmd
import (
diff --git a/internal/http/reason.go b/internal/http/reason.go
index 464f3dc..89d8732 100644
--- a/internal/http/reason.go
+++ b/internal/http/reason.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package http
import (
diff --git a/internal/http/reason_test.go b/internal/http/reason_test.go
index d71ff82..aa17b93 100644
--- a/internal/http/reason_test.go
+++ b/internal/http/reason_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package http
import (
diff --git a/internal/http/request.go b/internal/http/request.go
index 970a8aa..a01063d 100644
--- a/internal/http/request.go
+++ b/internal/http/request.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package http
import (
diff --git a/internal/http/request_test.go b/internal/http/request_test.go
index b0785dc..86e9921 100644
--- a/internal/http/request_test.go
+++ b/internal/http/request_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package http
import (
diff --git a/internal/server/control/control.go b/internal/server/control/control.go
index 216e8ff..d29d790 100644
--- a/internal/server/control/control.go
+++ b/internal/server/control/control.go
@@ -13,26 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package control
import (
"encoding/json"
"io/ioutil"
- "log"
"net/http"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/BBVA/kapow/internal/server/model"
+ "github.com/BBVA/kapow/internal/server/srverrors"
"github.com/BBVA/kapow/internal/server/user"
)
-// Run must start the control server in a specific address
-func Run(bindAddr string) {
- log.Fatal(http.ListenAndServe(bindAddr, configRouter()))
-}
-
+// configRouter Populates the server mux with all the supported routes. The
+// server exposes list, get, delete and add route endpoints.
func configRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/routes/{id}", removeRoute).
@@ -46,20 +44,27 @@ func configRouter() *mux.Router {
return r
}
+// funcRemove Method used to ask the route model module to delete a route
var funcRemove func(id string) error = user.Routes.Delete
+// removeRoute Handler that removes the requested route. If it doesn't exist,
+// returns 404 and an error entity
func removeRoute(res http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id := vars["id"]
if err := funcRemove(id); err != nil {
- res.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, "Route Not Found", res)
return
}
+
res.WriteHeader(http.StatusNoContent)
}
+// funcList Method used to ask the route model module for the list of routes
var funcList func() []model.Route = user.Routes.List
+// listRoutes Handler that retrieves a list of the existing routes. An empty
+// list is returned when no routes exist
func listRoutes(res http.ResponseWriter, req *http.Request) {
list := funcList()
@@ -69,58 +74,71 @@ func listRoutes(res http.ResponseWriter, req *http.Request) {
_, _ = res.Write(listBytes)
}
+// funcAdd Method used to ask the route model module to append a new route
var funcAdd func(model.Route) model.Route = user.Routes.Append
+
+// idGenerator UUID generator for new routes
var idGenerator = uuid.NewUUID
+// pathValidator Validates that a path complies with the gorilla mux
+// requirements
var pathValidator func(string) error = func(path string) error {
return mux.NewRouter().NewRoute().BuildOnly().Path(path).GetError()
}
+// addRoute Handler that adds a new route. Makes all parameter validation and
+// creates the a new is for the route
func addRoute(res http.ResponseWriter, req *http.Request) {
var route model.Route
payload, _ := ioutil.ReadAll(req.Body)
err := json.Unmarshal(payload, &route)
if err != nil {
- res.WriteHeader(http.StatusBadRequest)
+ srverrors.WriteErrorResponse(http.StatusBadRequest, "Malformed JSON", res)
return
}
+
if route.Method == "" {
- res.WriteHeader(http.StatusUnprocessableEntity)
+ srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, "Invalid Route", res)
return
}
+
if route.Pattern == "" {
- res.WriteHeader(http.StatusUnprocessableEntity)
+ srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, "Invalid Route", res)
return
}
err = pathValidator(route.Pattern)
if err != nil {
- res.WriteHeader(http.StatusUnprocessableEntity)
+ srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, "Invalid Route", res)
return
}
id, err := idGenerator()
if err != nil {
- res.WriteHeader(http.StatusInternalServerError)
+ srverrors.WriteErrorResponse(http.StatusInternalServerError, "Internal Server Error", res)
return
}
+
route.ID = id.String()
created := funcAdd(route)
createdBytes, _ := json.Marshal(created)
- res.WriteHeader(http.StatusCreated)
res.Header().Set("Content-Type", "application/json")
+ res.WriteHeader(http.StatusCreated)
_, _ = res.Write(createdBytes)
}
+// funcGet Method used to ask the route model module for the details of a route
var funcGet func(string) (model.Route, error) = user.Routes.Get
+// getRoute Handler that retrieves the details of a route. If the route doesn't
+// exists returns 404 and an error entity
func getRoute(res http.ResponseWriter, req *http.Request) {
id := mux.Vars(req)["id"]
if r, err := funcGet(id); err != nil {
- res.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, "Route Not Found", res)
} else {
res.Header().Set("Content-Type", "application/json")
rBytes, _ := json.Marshal(r)
diff --git a/internal/server/control/control_test.go b/internal/server/control/control_test.go
index 6f792c6..9d1738d 100644
--- a/internal/server/control/control_test.go
+++ b/internal/server/control/control_test.go
@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package control
import (
"encoding/json"
"errors"
+ "fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
@@ -29,9 +31,33 @@ import (
"github.com/gorilla/mux"
"github.com/BBVA/kapow/internal/server/model"
+ "github.com/BBVA/kapow/internal/server/srverrors"
"github.com/BBVA/kapow/internal/server/user"
)
+func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason string) []error {
+ errList := make([]error, 0)
+
+ if r.StatusCode != expectedErrcode {
+ errList = append(errList, fmt.Errorf("HTTP status mismatch. Expected: %d, got: %d", expectedErrcode, r.StatusCode))
+ }
+
+ if v := r.Header.Get("Content-Type"); v != "application/json; charset=utf-8" {
+ errList = append(errList, fmt.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v))
+ }
+
+ errMsg := srverrors.ServerErrMessage{}
+ if bodyBytes, err := ioutil.ReadAll(r.Body); err != nil {
+ errList = append(errList, fmt.Errorf("Unexpected error reading response body: %v", err))
+ } else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil {
+ errList = append(errList, fmt.Errorf("Response body contains invalid JSON entity: %v", err))
+ } else if errMsg.Reason != expectedReason {
+ errList = append(errList, fmt.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason))
+ }
+
+ return errList
+}
+
func TestConfigRouterHasRoutesWellConfigured(t *testing.T) {
testCases := []struct {
pattern, method string
@@ -100,17 +126,15 @@ func TestAddRouteReturnsBadRequestWhenMalformedJSONBody(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder()
- handler := http.HandlerFunc(addRoute)
- handler.ServeHTTP(resp, req)
- if resp.Code != http.StatusBadRequest {
- t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusBadRequest, resp.Code)
+ addRoute(resp, req)
+
+ for _, e := range checkErrorResponse(resp.Result(), http.StatusBadRequest, "Malformed JSON") {
+ t.Error(e)
}
-
}
func TestAddRouteReturns422ErrorWhenMandatoryFieldsMissing(t *testing.T) {
- handler := http.HandlerFunc(addRoute)
tc := []struct {
payload, testCase string
testMustFail bool
@@ -166,18 +190,19 @@ func TestAddRouteReturns422ErrorWhenMandatoryFieldsMissing(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(test.payload))
resp := httptest.NewRecorder()
- handler.ServeHTTP(resp, req)
+ addRoute(resp, req)
+ r := resp.Result()
if test.testMustFail {
- if resp.Code != http.StatusUnprocessableEntity {
- t.Errorf("HTTP status mismatch in case %s. Expected: %d, got: %d", test.testCase, http.StatusUnprocessableEntity, resp.Code)
+ for _, e := range checkErrorResponse(r, http.StatusUnprocessableEntity, "Invalid Route") {
+ t.Error(e)
}
} else if !test.testMustFail {
- if resp.Code != http.StatusCreated {
- t.Errorf("HTTP status mismatch in case %s. Expected: %d, got: %d", test.testCase, http.StatusUnprocessableEntity, resp.Code)
+ if r.StatusCode != http.StatusCreated {
+ t.Errorf("HTTP status mismatch in case %s. Expected: %d, got: %d", test.testCase, http.StatusUnprocessableEntity, r.StatusCode)
}
- if ct := resp.Header().Get("Content-Type"); ct != "application/json" {
- t.Errorf("Incorrect content type in response. Expected: application/json, got: %s", ct)
+ if ct := r.Header.Get("Content-Type"); ct != "application/json" {
+ t.Errorf("Incorrect content type in response. Expected: application/json, got: %q", ct)
}
}
}
@@ -192,7 +217,6 @@ func TestAddRouteGeneratesRouteID(t *testing.T) {
}`
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder()
- handler := http.HandlerFunc(addRoute)
var genID string
funcAdd = func(input model.Route) model.Route {
genID = input.ID
@@ -203,7 +227,7 @@ func TestAddRouteGeneratesRouteID(t *testing.T) {
defer func() { pathValidator = origPathValidator }()
pathValidator = func(path string) error { return nil }
- handler.ServeHTTP(resp, req)
+ addRoute(resp, req)
if _, err := uuid.Parse(genID); err != nil {
t.Error("ID not generated properly")
@@ -219,7 +243,6 @@ func TestAddRoute500sWhenIDGeneratorFails(t *testing.T) {
}`
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder()
- handler := http.HandlerFunc(addRoute)
origPathValidator := pathValidator
defer func() { pathValidator = origPathValidator }()
@@ -229,14 +252,13 @@ func TestAddRoute500sWhenIDGeneratorFails(t *testing.T) {
defer func() { idGenerator = idGenOrig }()
idGenerator = func() (uuid.UUID, error) {
var uuid uuid.UUID
- return uuid, errors.New(
- "End of Time reached; Try again before, or in the next Big Bang cycle")
+ return uuid, errors.New("End of Time reached; Try again before, or in the next Big Bang cycle")
}
- handler.ServeHTTP(resp, req)
+ addRoute(resp, req)
- if resp.Result().StatusCode != http.StatusInternalServerError {
- t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusInternalServerError, resp.Result().StatusCode)
+ for _, e := range checkErrorResponse(resp.Result(), http.StatusInternalServerError, "Internal Server Error") {
+ t.Error(e)
}
}
@@ -250,7 +272,6 @@ func TestAddRouteReturnsCreated(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder()
- handler := http.HandlerFunc(addRoute)
var genID string
funcAdd = func(input model.Route) model.Route {
expected := model.Route{ID: input.ID, Method: "GET", Pattern: "/hello", Entrypoint: "/bin/sh -c", Command: "echo Hello World | kapow set /response/body"}
@@ -266,7 +287,7 @@ func TestAddRouteReturnsCreated(t *testing.T) {
defer func() { pathValidator = origPathValidator }()
pathValidator = func(path string) error { return nil }
- handler.ServeHTTP(resp, req)
+ addRoute(resp, req)
if resp.Code != http.StatusCreated {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusCreated, resp.Code)
@@ -296,15 +317,14 @@ func TestAddRoute422sWhenInvalidRoute(t *testing.T) {
}`
req := httptest.NewRequest(http.MethodPost, "/routes", strings.NewReader(reqPayload))
resp := httptest.NewRecorder()
- handler := http.HandlerFunc(addRoute)
origPathValidator := pathValidator
defer func() { pathValidator = origPathValidator }()
pathValidator = func(path string) error { return errors.New("Invalid route") }
- handler.ServeHTTP(resp, req)
+ addRoute(resp, req)
- if resp.Code != http.StatusUnprocessableEntity {
- t.Error("Invalid route registered")
+ for _, e := range checkErrorResponse(resp.Result(), http.StatusUnprocessableEntity, "Invalid Route") {
+ t.Error(e)
}
}
@@ -314,7 +334,6 @@ func TestRemoveRouteReturnsNotFound(t *testing.T) {
handler := mux.NewRouter()
handler.HandleFunc("/routes/{id}", removeRoute).
Methods("DELETE")
-
funcRemove = func(id string) error {
if id == "ROUTE_XXXXXXXXXXXXXXXXXX" {
return errors.New(id)
@@ -324,8 +343,9 @@ func TestRemoveRouteReturnsNotFound(t *testing.T) {
}
handler.ServeHTTP(resp, req)
- if resp.Code != http.StatusNotFound {
- t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNotFound, resp.Code)
+
+ for _, e := range checkErrorResponse(resp.Result(), http.StatusNotFound, "Route Not Found") {
+ t.Error(e)
}
}
@@ -344,6 +364,7 @@ func TestRemoveRouteReturnsNoContent(t *testing.T) {
}
handler.ServeHTTP(resp, req)
+
if resp.Code != http.StatusNoContent {
t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNoContent, resp.Code)
}
@@ -414,13 +435,12 @@ func TestGetRouteReturns404sWhenRouteDoesntExist(t *testing.T) {
handler.ServeHTTP(w, r)
- resp := w.Result()
- if resp.StatusCode != http.StatusNotFound {
- t.Errorf("HTTP status mismatch. Expected: %d, got: %d", http.StatusNotFound, resp.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Route Not Found") {
+ t.Error(e)
}
}
-func TestGetRouteSetsCorrctContentType(t *testing.T) {
+func TestGetRouteSetsCorrectContentType(t *testing.T) {
handler := mux.NewRouter()
handler.HandleFunc("/routes/{id}", getRoute).
Methods("GET")
diff --git a/internal/server/control/server.go b/internal/server/control/server.go
new file mode 100644
index 0000000..5afafea
--- /dev/null
+++ b/internal/server/control/server.go
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
+ *
+ * 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 control
+
+import (
+ "log"
+ "net/http"
+)
+
+// Run Starts the control server listening in bindAddr
+func Run(bindAddr string) {
+ log.Fatal(http.ListenAndServe(bindAddr, configRouter()))
+}
diff --git a/internal/server/data/decorator.go b/internal/server/data/decorator.go
index 62695e7..bd708be 100644
--- a/internal/server/data/decorator.go
+++ b/internal/server/data/decorator.go
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
"net/http"
"github.com/BBVA/kapow/internal/server/model"
+ "github.com/BBVA/kapow/internal/server/srverrors"
"github.com/gorilla/mux"
)
@@ -38,7 +40,7 @@ func checkHandler(fn resourceHandler) func(http.ResponseWriter, *http.Request) {
if h, ok := Handlers.Get(handlerID); ok {
fn(w, r, h)
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, "Handler ID Not Found", w)
}
}
}
diff --git a/internal/server/data/decorator_test.go b/internal/server/data/decorator_test.go
index b66fd8a..1c75cdd 100644
--- a/internal/server/data/decorator_test.go
+++ b/internal/server/data/decorator_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
@@ -126,9 +127,8 @@ func TestCheckHandlerReturnsAFunctionsThat404sWhenHandlerDoesNotExist(t *testing
fn(w, r)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected 404. Got %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Handler ID Not Found") {
+ t.Error(e)
}
}
diff --git a/internal/server/data/resource.go b/internal/server/data/resource.go
index d4a8a70..d55fc72 100644
--- a/internal/server/data/resource.go
+++ b/internal/server/data/resource.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
@@ -23,15 +24,22 @@ import (
"strconv"
"github.com/BBVA/kapow/internal/server/model"
+ "github.com/BBVA/kapow/internal/server/srverrors"
"github.com/gorilla/mux"
)
+const (
+ ResourceItemNotFound = "Resource Item Not Found"
+ NonIntegerValue = "Non Integer Value"
+ InvalidStatusCode = "Invalid Status Code"
+)
+
func getRequestBody(w http.ResponseWriter, r *http.Request, h *model.Handler) {
w.Header().Add("Content-Type", "application/octet-stream")
n, err := io.Copy(w, h.Request.Body)
if err != nil {
if n == 0 {
- w.WriteHeader(http.StatusInternalServerError)
+ srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
} else {
// Only way to abort current connection as of go 1.13
// https://github.com/golang/go/issues/16542
@@ -63,7 +71,7 @@ func getRequestMatches(w http.ResponseWriter, r *http.Request, h *model.Handler)
if value, ok := vars[name]; ok {
_, _ = w.Write([]byte(value))
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -73,7 +81,7 @@ func getRequestParams(w http.ResponseWriter, r *http.Request, h *model.Handler)
if values, ok := h.Request.URL.Query()[name]; ok {
_, _ = w.Write([]byte(values[0]))
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -83,7 +91,7 @@ func getRequestHeaders(w http.ResponseWriter, r *http.Request, h *model.Handler)
if values, ok := h.Request.Header[textproto.CanonicalMIMEHeaderKey(name)]; ok {
_, _ = w.Write([]byte(values[0]))
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -93,7 +101,7 @@ func getRequestCookies(w http.ResponseWriter, r *http.Request, h *model.Handler)
if cookie, err := h.Request.Cookie(name); err == nil {
_, _ = w.Write([]byte(cookie.Value))
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -109,11 +117,11 @@ func getRequestForm(w http.ResponseWriter, r *http.Request, h *model.Handler) {
// We tried to exercise this execution path but didn't know how.
err := h.Request.ParseForm()
if err != nil {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
} else if values, ok := h.Request.Form[name]; ok {
_, _ = w.Write([]byte(values[0]))
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -124,7 +132,7 @@ func getRequestFileName(w http.ResponseWriter, r *http.Request, h *model.Handler
if err == nil {
_, _ = w.Write([]byte(header.Filename))
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -135,7 +143,7 @@ func getRequestFileContent(w http.ResponseWriter, r *http.Request, h *model.Hand
if err == nil {
_, _ = io.Copy(w, file)
} else {
- w.WriteHeader(http.StatusNotFound)
+ srverrors.WriteErrorResponse(http.StatusNotFound, ResourceItemNotFound, w)
}
}
@@ -144,17 +152,16 @@ func getRequestFileContent(w http.ResponseWriter, r *http.Request, h *model.Hand
func setResponseStatus(w http.ResponseWriter, r *http.Request, h *model.Handler) {
sb, err := ioutil.ReadAll(r.Body)
if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
+ srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
return
}
- si, err := strconv.Atoi(string(sb))
- if http.StatusText(si) == "" {
- w.WriteHeader(http.StatusBadRequest)
- } else if err == nil {
- h.Writer.WriteHeader(int(si))
+ if si, err := strconv.Atoi(string(sb)); err != nil {
+ srverrors.WriteErrorResponse(http.StatusUnprocessableEntity, NonIntegerValue, w)
+ } else if http.StatusText(si) == "" {
+ srverrors.WriteErrorResponse(http.StatusBadRequest, InvalidStatusCode, w)
} else {
- w.WriteHeader(http.StatusBadRequest)
+ h.Writer.WriteHeader(int(si))
}
}
@@ -162,7 +169,7 @@ func setResponseHeaders(w http.ResponseWriter, r *http.Request, h *model.Handler
name := mux.Vars(r)["name"]
vb, err := ioutil.ReadAll(r.Body)
if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
+ srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
return
}
@@ -178,7 +185,7 @@ func setResponseCookies(w http.ResponseWriter, r *http.Request, h *model.Handler
name := mux.Vars(r)["name"]
vb, err := ioutil.ReadAll(r.Body)
if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
+ srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
return
}
@@ -191,6 +198,6 @@ func setResponseBody(w http.ResponseWriter, r *http.Request, h *model.Handler) {
if n > 0 {
panic("Truncated body")
}
- w.WriteHeader(http.StatusInternalServerError)
+ srverrors.WriteErrorResponse(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError), w)
}
}
diff --git a/internal/server/data/resource_test.go b/internal/server/data/resource_test.go
index 1d5e387..7ed8fae 100644
--- a/internal/server/data/resource_test.go
+++ b/internal/server/data/resource_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
@@ -120,9 +121,8 @@ func TestGetRequestBody500sWhenHandlerRequestErrors(t *testing.T) {
getRequestBody(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusInternalServerError {
- t.Error("status not 500")
+ for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
+ t.Error(e)
}
}
@@ -369,9 +369,8 @@ func TestGetRequestMatchesReturnsNotFoundWhenMatchDoesntExists(t *testing.T) {
getRequestMatches(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404. Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -433,9 +432,8 @@ func TestGetRequestParams404sWhenParamDoesntExist(t *testing.T) {
getRequestParams(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404. Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -478,14 +476,15 @@ func TestGetRequestHeadersSetsOctectStreamContentType(t *testing.T) {
Request: httptest.NewRequest("GET", "/", nil),
Writer: httptest.NewRecorder(),
}
+ h.Request.Header.Set("bar", "BAZ")
r := createMuxRequest("/handlers/HANDLERID/request/headers/{name}", "/handlers/HANDLERID/request/headers/bar", "GET", nil)
w := httptest.NewRecorder()
getRequestHeaders(w, r, &h)
res := w.Result()
- if res.Header.Get("Content-Type") != "application/octet-stream" {
- t.Error("Content Type mismatch")
+ if v := res.Header.Get("Content-Type"); v != "application/octet-stream" {
+ t.Errorf("Content Type mismatch. Expected: application/octet-stream. Got: %q", v)
}
}
@@ -567,9 +566,8 @@ func TestGetRequestHeaders404sWhenHeaderDoesntExist(t *testing.T) {
getRequestHeaders(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Error("Status code mismatch")
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -652,9 +650,8 @@ func TestGetRequestCookies404sIfCookieDoesntExist(t *testing.T) {
getRequestCookies(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -754,9 +751,8 @@ func TestGetRequestForm404sWhenFieldDoesntExist(t *testing.T) {
getRequestForm(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -810,9 +806,8 @@ func TestGetRequestForm404sWhenFormDoesntExist(t *testing.T) {
getRequestForm(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -885,9 +880,8 @@ func TestGetRequestFileName404sWhenFileDoesntExist(t *testing.T) {
getRequestFileName(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -902,9 +896,8 @@ func TestGetRequestFileName404sWhenFormDoesntExist(t *testing.T) {
getRequestFileName(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -966,9 +959,8 @@ func TestGetRequestFileContent404sWhenFileDoesntExist(t *testing.T) {
getRequestFileContent(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -983,9 +975,8 @@ func TestGetRequestFileContent404sWhenFormDoesntExist(t *testing.T) {
getRequestFileContent(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusNotFound {
- t.Errorf("Status code mismatch. Expected: 404, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusNotFound, "Resource Item Not Found") {
+ t.Error(e)
}
}
@@ -1009,9 +1000,8 @@ func TestGetRequestFileContent500sWhenHandlerRequestErrors(t *testing.T) {
getRequestFileContent(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusInternalServerError {
- t.Error("status not 500", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
+ t.Error(e)
}
}
@@ -1058,9 +1048,8 @@ func TestSetResponseStatus400sWhenNonparseableStatusCode(t *testing.T) {
setResponseStatus(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusBadRequest {
- t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusUnprocessableEntity, "Non Integer Value") {
+ t.Error(e)
}
}
@@ -1074,9 +1063,8 @@ func TestSetResponseStatus500sWhenErrorReadingRequest(t *testing.T) {
setResponseStatus(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusInternalServerError {
- t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
+ t.Error(e)
}
}
@@ -1092,9 +1080,8 @@ func TestSetResponseStatus400sWhenStatusCodeNotSupportedByGo(t *testing.T) {
setResponseStatus(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusBadRequest {
- t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Status Code") {
+ t.Error(e)
}
}
@@ -1162,9 +1149,8 @@ func TestSetResponseHeaders500sWhenErrorReadingRequest(t *testing.T) {
setResponseHeaders(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusInternalServerError {
- t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
+ t.Error(e)
}
}
@@ -1181,9 +1167,8 @@ func TestSetResponseHeaders400sOnInvalidHeaderKey(t *testing.T) {
setResponseHeaders(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusBadRequest {
- t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Header Name") {
+ t.Error(e)
}
}
@@ -1200,9 +1185,8 @@ func TestSetResponseHeaders400sOnInvalidHeaderValue(t *testing.T) {
setResponseHeaders(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusBadRequest {
- t.Errorf("Status code mismatch. Expected: 400, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Header Value") {
+ t.Error(e)
}
}
@@ -1250,9 +1234,8 @@ func TestSetResponseCookies500sWhenErrorReadingRequest(t *testing.T) {
setResponseCookies(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusInternalServerError {
- t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
+ t.Error(e)
}
}
@@ -1322,9 +1305,8 @@ func TestSetResponseBody500sWhenReaderFailsInFirstRead(t *testing.T) {
setResponseBody(w, r, &h)
- res := w.Result()
- if res.StatusCode != http.StatusInternalServerError {
- t.Errorf("Status code mismatch. Expected: 500, Got: %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) {
+ t.Error(e)
}
}
diff --git a/internal/server/data/server.go b/internal/server/data/server.go
index 6139428..140b984 100644
--- a/internal/server/data/server.go
+++ b/internal/server/data/server.go
@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
"log"
"net/http"
+ "github.com/BBVA/kapow/internal/server/srverrors"
"github.com/gorilla/mux"
)
@@ -35,7 +37,9 @@ func configRouter(rs []routeSpec) (r *mux.Router) {
}
r.HandleFunc(
"/handlers/{handlerID}/{resource:.*}",
- func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })
+ func(w http.ResponseWriter, r *http.Request) {
+ srverrors.WriteErrorResponse(http.StatusBadRequest, "Invalid Resource Path", w)
+ })
return r
}
diff --git a/internal/server/data/server_test.go b/internal/server/data/server_test.go
index eaeb932..7481e5a 100644
--- a/internal/server/data/server_test.go
+++ b/internal/server/data/server_test.go
@@ -13,16 +13,44 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/BBVA/kapow/internal/server/model"
+ "github.com/BBVA/kapow/internal/server/srverrors"
)
+func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason string) []error {
+ errList := make([]error, 0)
+
+ if r.StatusCode != expectedErrcode {
+ errList = append(errList, fmt.Errorf("HTTP status mismatch. Expected: %d, got: %d", expectedErrcode, r.StatusCode))
+ }
+
+ if v := r.Header.Get("Content-Type"); v != "application/json; charset=utf-8" {
+ errList = append(errList, fmt.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v))
+ }
+
+ errMsg := srverrors.ServerErrMessage{}
+ if bodyBytes, err := ioutil.ReadAll(r.Body); err != nil {
+ errList = append(errList, fmt.Errorf("Unexpected error reading response body: %v", err))
+ } else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil {
+ errList = append(errList, fmt.Errorf("Response body contains invalid JSON entity: %v", err))
+ } else if errMsg.Reason != expectedReason {
+ errList = append(errList, fmt.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason))
+ }
+
+ return errList
+}
+
func TestConfigRouterReturnsRouterWithDecoratedRoutes(t *testing.T) {
var handlerID string
rs := []routeSpec{
@@ -49,8 +77,7 @@ func TestConfigRouterReturnsRouterThat400sOnUnconfiguredResources(t *testing.T)
m.ServeHTTP(w, httptest.NewRequest("GET", "/handlers/FOO/dummy", nil))
- res := w.Result()
- if res.StatusCode != http.StatusBadRequest {
- t.Errorf("Status code mismatch. Expected 400. Got %d", res.StatusCode)
+ for _, e := range checkErrorResponse(w.Result(), http.StatusBadRequest, "Invalid Resource Path") {
+ t.Error(e)
}
}
diff --git a/internal/server/data/state.go b/internal/server/data/state.go
index e95a294..94c4cfc 100644
--- a/internal/server/data/state.go
+++ b/internal/server/data/state.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
diff --git a/internal/server/data/state_test.go b/internal/server/data/state_test.go
index 7ca5e15..f082c3a 100644
--- a/internal/server/data/state_test.go
+++ b/internal/server/data/state_test.go
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package data
import (
diff --git a/internal/server/model/handler.go b/internal/server/model/handler.go
index b96ac44..8d79b35 100644
--- a/internal/server/model/handler.go
+++ b/internal/server/model/handler.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package model
import (
diff --git a/internal/server/model/route.go b/internal/server/model/route.go
index 900fd59..f630dce 100644
--- a/internal/server/model/route.go
+++ b/internal/server/model/route.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package model
// Route contains the data needed to represent a Kapow! user route.
diff --git a/internal/server/server.go b/internal/server/server.go
index 7c8da7e..766ec3c 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package server
import (
diff --git a/internal/server/srverrors/error.go b/internal/server/srverrors/error.go
new file mode 100644
index 0000000..77c7083
--- /dev/null
+++ b/internal/server/srverrors/error.go
@@ -0,0 +1,19 @@
+package srverrors
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+type ServerErrMessage struct {
+ Reason string `json:"reason"`
+}
+
+func WriteErrorResponse(statusCode int, reasonMsg string, res http.ResponseWriter) {
+ respBody := ServerErrMessage{}
+ respBody.Reason = reasonMsg
+ bb, _ := json.Marshal(respBody)
+ res.Header().Set("Content-Type", "application/json; charset=utf-8")
+ res.WriteHeader(statusCode)
+ _, _ = res.Write(bb)
+}
diff --git a/internal/server/srverrors/error_test.go b/internal/server/srverrors/error_test.go
new file mode 100644
index 0000000..adda7b5
--- /dev/null
+++ b/internal/server/srverrors/error_test.go
@@ -0,0 +1,47 @@
+package srverrors_test
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/BBVA/kapow/internal/server/srverrors"
+)
+
+func TestWriteErrorResponseSetsAppJsonContentType(t *testing.T) {
+ w := httptest.NewRecorder()
+
+ srverrors.WriteErrorResponse(0, "Not Important Here", w)
+
+ if v := w.Result().Header.Get("Content-Type"); v != "application/json; charset=utf-8" {
+ t.Errorf("Content-Type header mismatch. Expected: %q, got: %q", "application/json; charset=utf-8", v)
+ }
+}
+
+func TestWriteErrorResponseSetsRequestedStatusCode(t *testing.T) {
+ w := httptest.NewRecorder()
+
+ srverrors.WriteErrorResponse(http.StatusGone, "Not Important Here", w)
+
+ if v := w.Result().StatusCode; v != http.StatusGone {
+ t.Errorf("Status code mismatch. Expected: %d, got: %d", http.StatusGone, v)
+ }
+}
+
+func TestWriteErrorResponseSetsBodyCorrectly(t *testing.T) {
+ expectedReason := "Something Not Found"
+ w := httptest.NewRecorder()
+
+ srverrors.WriteErrorResponse(http.StatusNotFound, expectedReason, w)
+
+ errMsg := srverrors.ServerErrMessage{}
+ if bodyBytes, err := ioutil.ReadAll(w.Result().Body); err != nil {
+ t.Errorf("Unexpected error reading response body: %v", err)
+ } else if err := json.Unmarshal(bodyBytes, &errMsg); err != nil {
+ t.Errorf("Response body contains invalid JSON entity: %v", err)
+ } else if errMsg.Reason != expectedReason {
+ t.Errorf("Unexpected reason in response. Expected: %q, got: %q", expectedReason, errMsg.Reason)
+ }
+}
diff --git a/internal/server/user/mux/gorillize.go b/internal/server/user/mux/gorillize.go
index a089f07..9ebcc2d 100644
--- a/internal/server/user/mux/gorillize.go
+++ b/internal/server/user/mux/gorillize.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package mux
import (
diff --git a/internal/server/user/mux/gorillize_test.go b/internal/server/user/mux/gorillize_test.go
index 69ba2f4..f531f2d 100644
--- a/internal/server/user/mux/gorillize_test.go
+++ b/internal/server/user/mux/gorillize_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package mux
import (
diff --git a/internal/server/user/mux/handlerbuilder.go b/internal/server/user/mux/handlerbuilder.go
index 64fe7d7..09dbdfb 100644
--- a/internal/server/user/mux/handlerbuilder.go
+++ b/internal/server/user/mux/handlerbuilder.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package mux
import (
diff --git a/internal/server/user/mux/handlerbuilder_test.go b/internal/server/user/mux/handlerbuilder_test.go
index 3e2d7f3..da5dde1 100644
--- a/internal/server/user/mux/handlerbuilder_test.go
+++ b/internal/server/user/mux/handlerbuilder_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package mux
import (
diff --git a/internal/server/user/mux/mux.go b/internal/server/user/mux/mux.go
index 9946f5f..508cbcb 100644
--- a/internal/server/user/mux/mux.go
+++ b/internal/server/user/mux/mux.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package mux
import (
diff --git a/internal/server/user/mux/mux_test.go b/internal/server/user/mux/mux_test.go
index 5cbc04f..771ad78 100644
--- a/internal/server/user/mux/mux_test.go
+++ b/internal/server/user/mux/mux_test.go
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package mux
import (
diff --git a/internal/server/user/server.go b/internal/server/user/server.go
index bf3800a..15f0706 100644
--- a/internal/server/user/server.go
+++ b/internal/server/user/server.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package user
import (
diff --git a/internal/server/user/spawn/spawn.go b/internal/server/user/spawn/spawn.go
index fb13231..50f4f4d 100644
--- a/internal/server/user/spawn/spawn.go
+++ b/internal/server/user/spawn/spawn.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package spawn
import (
diff --git a/internal/server/user/spawn/spawn_test.go b/internal/server/user/spawn/spawn_test.go
index f5ebae3..ef77791 100644
--- a/internal/server/user/spawn/spawn_test.go
+++ b/internal/server/user/spawn/spawn_test.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package spawn
import (
diff --git a/internal/server/user/state.go b/internal/server/user/state.go
index fc7e3a5..ed240b5 100644
--- a/internal/server/user/state.go
+++ b/internal/server/user/state.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package user
import (
diff --git a/internal/server/user/state_test.go b/internal/server/user/state_test.go
index bb0c13e..2ba52f7 100644
--- a/internal/server/user/state_test.go
+++ b/internal/server/user/state_test.go
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package user
import (
diff --git a/main.go b/main.go
index 9a7a88e..6fd8089 100644
--- a/main.go
+++ b/main.go
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package main
import (
diff --git a/master.pow b/master.pow
deleted file mode 100644
index 451a01b..0000000
--- a/master.pow
+++ /dev/null
@@ -1 +0,0 @@
-go run . route add -c 'echo foo' /
\ No newline at end of file
diff --git a/poc/Makefile b/poc/Makefile
index b7a9a04..9c5a4ff 100644
--- a/poc/Makefile
+++ b/poc/Makefile
@@ -6,4 +6,7 @@ sync:
pipenv sync --dev
test: sync
- pipenv run make -C ../spec/test
+ KAPOW_DATAAPI_URL=http://localhost:8081 pipenv run make -C ../spec/test
+
+fix:
+ KAPOW_DATAAPI_URL=http://localhost:8081 pipenv run make -C ../spec/test fix
diff --git a/poc/bin/kapow b/poc/bin/kapow
index e9d6e66..5c371fb 100755
--- a/poc/bin/kapow
+++ b/poc/bin/kapow
@@ -178,14 +178,14 @@ async def get_field(request):
try:
connection = CONNECTIONS[id]
except KeyError:
- response = web.Response(status=404, reason="Handler ID Not Found")
+ response = web.json_response(data=error_body("Handler ID Not Found"), status=404, reason="Not Found")
else:
try:
content = await connection.get(field)
except ValueError:
- return web.Response(status=400, reason="Invalid Resource Path")
+ return web.json_response(data=error_body("Invalid Resource Path"), status=400, reason="Bad Request")
except KeyError:
- return web.Response(status=404, reason="Resource Item Not Found")
+ return web.json_response(data=error_body("Resource Item Not Found"), status=404, reason="Not Found")
if isinstance(content, StreamReader):
response = web.StreamResponse(status=200, reason="OK")
@@ -210,8 +210,10 @@ async def set_field(request):
try:
connection = CONNECTIONS[id]
+ except ValueError:
+ return web.json_response(data=error_body("Invalid Resource Path"), status=400, reason="Bad Request")
except KeyError:
- response = web.Response(status=404, reason="Handler ID Not Found")
+ response = web.json_response(data=error_body("Handler ID Not Found"), status=404, reason="Not Found")
else:
try:
await connection.set(field, request.content)
@@ -275,6 +277,9 @@ def handle_route(entrypoint, command):
########################################################################
+def error_body(reason):
+ return {"reason": reason, "foo": "bar"}
+
def get_routes(app):
async def _get_routes(request):
"""Return the list of registered routes."""
@@ -302,7 +307,7 @@ def get_route(app):
"entrypoint": r.entrypoint,
"command": r.command})
else:
- return web.Response(status=404, reason="Not Found")
+ return web.json_response(data=error_body("Route Not Found"), status=404, reason="Not Found")
return _get_route
@@ -312,7 +317,7 @@ def insert_route(app):
try:
content = await request.json()
except ValueError:
- return web.Response(status=400, reason="Malformed JSON")
+ return web.json_response(data=error_body("Malformed JSON"), status=400, reason="Bad Request")
try:
index = int(content["index"])
@@ -330,7 +335,7 @@ def insert_route(app):
+ [route]
+ app["user_routes"][index:]))
except (InvalidRouteError, KeyError, AssertionError, ValueError) as exc:
- return web.Response(status=422, reason="Invalid Route")
+ return web.json_response(data=error_body("Invalid Route"), status=422, reason="Unprocessable Entity")
else:
app["user_routes"].insert(index, route)
return web.json_response({"id": route.id,
@@ -348,7 +353,7 @@ def append_route(app):
try:
content = await request.json()
except ValueError as exc:
- return web.Response(status=400, reason="Malformed JSON")
+ return web.json_response(data=error_body("Malformed JSON"), status=400, reason="Bad Request")
try:
method = content.get("method", "GET")
@@ -362,7 +367,7 @@ def append_route(app):
handler=handle_route(entrypoint, command))
app.change_routes(app["user_routes"] + [route])
except (InvalidRouteError, KeyError) as exc:
- return web.Response(status=422, reason="Invalid Route")
+ return web.json_response(data=error_body("Invalid Route"), status=422, reason="Unprocessable Entity")
else:
app["user_routes"].append(route)
return web.json_response({"id": route.id,
@@ -381,7 +386,7 @@ def delete_route(app):
id = request.match_info["id"]
routes = [r for r in app["user_routes"] if r.id != id]
if len(routes) == len(app["user_routes"]):
- return web.Response(status=404, reason="Not Found")
+ return web.json_response(data=error_body("Route Not Found"), status=404, reason="Not Found")
else:
app.change_routes(routes)
app["user_routes"] = routes
diff --git a/poc/examples/eval.pow b/poc/examples/eval.pow
deleted file mode 100755
index acc18bd..0000000
--- a/poc/examples/eval.pow
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-kapow route add -X POST '/eval' -c '$($(kapow get /request/body) | kapow set /response/stream)'
diff --git a/poc/examples/eval.testme b/poc/examples/eval.testme
deleted file mode 100755
index a17bfc9..0000000
--- a/poc/examples/eval.testme
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env sh
-
-curl -X POST --data-binary @- http://localhost:8080/eval <
-
-
-
- Nmap
-
-
-
-
-
diff --git a/poc/examples/nmap-web.pow b/poc/examples/nmap-web.pow
deleted file mode 100755
index 6fb0755..0000000
--- a/poc/examples/nmap-web.pow
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-#
-# Nmap produces an XML report, suitable for rendering in a web browser
-#
-
-# Call examples:
-#
-# $ browser http://localhost:8080
-#
-# $ curl -v http://localhost:8080/nmap.xml -d 'target_spec=127.0.0.1&port_ranges=9000'
-#
-
-kapow route add -X GET / - <<-'EOF'
- cat nmap-web.html | kapow set /response/body
-EOF
-
-kapow route add -X GET /nmap.xsl - <<-'EOF'
- curl --silent https://svn.nmap.org/nmap/docs/nmap.xsl \
- | kapow set /response/body
-EOF
-
-kapow route add -X POST /nmap.xml - <<-'EOF'
-
- TARGET_SPEC=$(kapow get /request/form/target_spec)
- : ${TARGET_SPEC:=127.0.0.1}
-
- PORT_RANGES=$(kapow get /request/form/port_ranges)
- : ${PORT_RANGES:=8080}
-
- nmap \
- -Pn \
- -n \
- -p "$PORT_RANGES" \
- -oX - \
- --stylesheet /nmap.xsl \
- "$TARGET_SPEC" \
- | kapow set /response/body
-EOF
diff --git a/poc/examples/nmap/Dockerfile b/poc/examples/nmap/Dockerfile
deleted file mode 100644
index 5fe5224..0000000
--- a/poc/examples/nmap/Dockerfile
+++ /dev/null
@@ -1,7 +0,0 @@
-FROM bbvalabsci/kapow:latest
-
-RUN apk add nmap
-
-COPY nmap.pow /tmp/
-
-CMD ["server", "/tmp/nmap.pow"]
diff --git a/poc/examples/nmap/nmap.pow b/poc/examples/nmap/nmap.pow
deleted file mode 100755
index 37ec075..0000000
--- a/poc/examples/nmap/nmap.pow
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-kapow route add -X GET '/list/{ip}' -c 'nmap -sL $(kapow get /request/matches/ip) | kapow set /response/body'
diff --git a/poc/examples/operator.pow b/poc/examples/operator.pow
deleted file mode 100755
index 27bca22..0000000
--- a/poc/examples/operator.pow
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-kapow route add /list/files -c 'ls -la $(kapow get /request/params/path) | kapow set /response/body'
-
-kapow route add /list/processes -c 'ps -aux | kapow set /response/body'
-
-kapow route add /show/cpuinfo -c 'kapow set /response/body < /proc/cpuinfo'
-
-kapow route add /show/memory -c 'free -m | kapow set /response/body'
-
-kapow route add /show/disk -c 'df -h | kapow set /response/body'
-
-kapow route add /show/connections -c 'ss -pluton | kapow set /response/body'
-
-kapow route add /show/mounts -c 'mount | kapow set /response/body'
-
-kapow route add /tail/dmesg - <<-'EOF'
- kapow set /response/headers/Content-Type text/plain
- dmesg -w | kapow set /response/stream
-EOF
-
-kapow route add /tail/journal - <<-'EOF'
- kapow set /response/headers/Content-Type text/plain
- journalctl -f | kapow set /response/stream
-EOF
diff --git a/poc/examples/pandoc/pandoc.pow b/poc/examples/pandoc/pandoc.pow
deleted file mode 100755
index fb245c5..0000000
--- a/poc/examples/pandoc/pandoc.pow
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-kapow route add -X POST --entrypoint '/bin/zsh -c' '/convert/{from}/{to}' - <<-'EOF'
- pandoc --from=$(kapow get /request/matches/from) \
- --to=$(kapow get /request/matches/to) \
- --output=>(kapow set /response/body) \
- =(kapow get /request/body)
-EOF
-kapow route add -X GET '/formats/input' -c 'pandoc --list-input-formats | kapow set /response/body'
-kapow route add -X GET '/formats/output' -c 'pandoc --list-output-formats | grep -v pdf | kapow set /response/body'
diff --git a/poc/examples/pandoc/testme b/poc/examples/pandoc/testme
deleted file mode 100755
index e2fabd3..0000000
--- a/poc/examples/pandoc/testme
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env sh
-
-curl -X POST --data-binary @- http://localhost:8080/convert/markdown/man <
-
- PDF Editor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/poc/examples/pdfeditor/pdfeditor.pow b/poc/examples/pdfeditor/pdfeditor.pow
deleted file mode 100755
index 06eb815..0000000
--- a/poc/examples/pdfeditor/pdfeditor.pow
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-kapow route add -X POST --entrypoint ./topdf '/editor/pdf'
-kapow route add / -c 'kapow set /response/headers/Content-Type text/html && kapow set /response/body < pdfeditor.html'
diff --git a/poc/examples/pdfeditor/topdf b/poc/examples/pdfeditor/topdf
deleted file mode 100755
index 8e2dddc..0000000
--- a/poc/examples/pdfeditor/topdf
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/zsh
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-tmpfile=$(mktemp --suffix=.pdf)
-pandoc --from=$(kapow get /request/form/from) --to=pdf --output=${tmpfile} -t latex =(kapow get /request/form/content)
-if [ $? -eq 0 ]; then
- kapow set /response/headers/Content-Type application/pdf
- kapow set /response/body < ${tmpfile}
- kapow set /response/status 200
-else
- kapow set /response/status 500
-fi
-rm -f ${tmpfile}
diff --git a/poc/examples/tcpdump/README.md b/poc/examples/tcpdump/README.md
deleted file mode 100644
index de84f51..0000000
--- a/poc/examples/tcpdump/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-Remote tcpdump sniffer with source filtering
-============================================
-
-1. Add any filter you want to the `tcpdump` command inside `tcpdump.pow` to filter
- any traffic you don't want to be sniffed!
-2. For the sake of simplicity, run `sudo -E kapow server tcpdump.pow`. In a
- production environment, `tcpdump` should be run with the appropiate permissions,
- but kapow can (and should) run as an unprivileged user.
-3. In your local machine run:
- ```bash
- curl http://localhost:8080/sniff/ | sudo -E wireshark -k -i -
- ```
- Again, for the sake of simplicity, `Wireshark` is running as root. If you don't want
- to run it this way, follow this guide:
- https://gist.github.com/MinaMikhailcom/0825906230cbbe478faf4d08abe9d11a
-4. Profit!
diff --git a/poc/examples/tcpdump/tcpdump.pow b/poc/examples/tcpdump/tcpdump.pow
deleted file mode 100644
index 36b83d9..0000000
--- a/poc/examples/tcpdump/tcpdump.pow
+++ /dev/null
@@ -1 +0,0 @@
-kapow route add /sniff/{iface} -c 'tcpdump -i "$(kapow get /request/matches/iface)" -U -s0 -w - "not port 8080" | kapow set /response/stream'
diff --git a/poc/examples/torrent.pow b/poc/examples/torrent.pow
deleted file mode 100755
index ee31e7d..0000000
--- a/poc/examples/torrent.pow
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/bin/bash
-
-#
-# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
-#
-# 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.
-#
-
-kapow route add / - <<-'EOF'
- kapow set /response/headers/Content-Type text/html
- kapow set /response/body <<-HTML
-
-
- Add me to your bookmarks!
-
-
- HTML
-EOF
-
-kapow route add /save/magnet -e '/bin/bash -c' - <<-'EOF'
- link=$(kapow get /request/params/link)
- [ -z $link ] && kapow set /response/status 400 && exit 0
-
- watch_folder=/tmp
- cd $watch_folder
- [[ "$link" =~ xt=urn:btih:([^&/]+) ]] || exit;
- echo "d10:magnet-uri${#link}:${link}e" > "meta-${BASH_REMATCH[1]}.torrent"
-
- kapow set /response/status 302
- kapow set /response/headers/Location /torrent/list
-EOF
-
-kapow route add /torrent/list -c 'kapow set /response/body "Not Implemented Yet"'
diff --git a/spec/README.md b/spec/README.md
index 2fb6fe0..aadca97 100644
--- a/spec/README.md
+++ b/spec/README.md
@@ -131,14 +131,19 @@ whole lifetime of the server.
conservative in what you do, be liberal in what you accept from others.
* We reuse conventions of well-established software projects, such as Docker.
* All requests and responses will leverage JSON as the data encoding method.
-* The API calls responses have two parts:
+* The API calls responses have several parts:
* The HTTP status code (e.g., `400`, which is a bad request). The target
audience of this information is the client code. The client can thus use
this information to control the program flow.
- * The body is optional
+ * The body is optional depending on the request method and the status code. For
+ error responses (4xx and 5xx) a json body is included with a reason phrase.
+ The target audience in this case is the human operating the client. The
+ human can use this information to make a decision on how to proceed.
* All successful API calls will return a representation of the *final* state
attained by the objects which have been addressed (either requested, set or
deleted).
+* When several error conditions can happen at the same time, the order of the
+ checks is implementation-defined.
For instance, given this request:
```http
@@ -163,6 +168,15 @@ Content-Length: 189
]
```
+While an error response may look like this:
+```http
+404 Not Found
+Content-Type: application/json
+Content-Length: 25
+
+{"reason": "Not Found"}
+```
+
## API Elements
@@ -242,8 +256,8 @@ A new id is created for the appended route so it can be referenced later.
}
```
* **Error Responses**:
- * **Code**: `400 Bad Request`
- * **Code**: `422 Unprocessable Entity`
+ * **Code**: `400`; **Reason**: `Malformed JSON`
+ * **Code**: `422`; **Reason**: `Invalid Route`
* **Sample Call**:
```sh
$ curl -X POST --data-binary @- $KAPOW_URL/routes <
```sh
$ curl -X PUT --data-binary @- $KAPOW_URL/routes <
```sh
$ curl -X DELETE $KAPOW_URL/routes/ROUTE_1f186c92_f906_4506_9788_a1f541b11d0f
@@ -357,7 +371,7 @@ Retrieves the information about the route identified by `{id}`.
}
```
* **Error Responses**:
- * **Code**: `404 Not Found`
+ * **Code**: `404`; Reason: `Route Not Found`
* **Sample Call**:
```sh
$ curl -X GET $KAPOW_URL/routes/ROUTE_1f186c92_f906_4506_9788_a1f541b11d0f
@@ -381,11 +395,16 @@ response.
* The HTTP status code (e.g., `400`, which is a bad request). The target
audience of this information is the client code. The client can thus use
this information to control the program flow.
- * The HTTP body. On read requests, containing the data retrieved as
- 'application/octet-stream' mime type.
+ * The HTTP reason phrase. The target audience in this case is the human
+ operating the client. The human can use this information to make a
+ decision on how to proceed.
* Regarding HTTP request and response bodies:
- * The response body will be empty in case of error.
- * It will transport binary data in other case.
+ * In case of error the response body will be a json entity containing a reason
+ phrase. The target audience in this case is the human operating the client.
+ The human can use this information to make a decision on how to proceed.
+ * It will transport binary data in any other case.
+* When several error conditions can happen at the same time, the order of the
+ checks is implementation-defined.
## API Elements
@@ -510,12 +529,11 @@ path doesn't exist or is invalid.
**Header**: `Content-Type: application/octet-stream`
**Content**: The value of the resource. Note that it may be empty.
* **Error Responses**:
- * **Code**: `400 Bad Request`
- **Notes**: An invalid resource path has been requested. Check the list of
- valid resource paths at the top of this section.
- * **Code**: `404 Not Found`
+ * **Code**: `400`; Reason: `Invalid Resource Path`
+ **Notes**: Check the list of valid resource paths at the top of this section.
+ * **Code**: `404`; Reason: `Handler ID Not Found`
**Notes**: Refers to the handler resource itself.
- * **Code**: `404 Not Found`
+ * **Code**: `404`; Reason: `Resource Item Not Found`
**Notes**: Refers to the named item in the corresponding data API resource.
* **Sample Call**:
```sh
@@ -534,10 +552,13 @@ path doesn't exist or is invalid.
* **Success Responses**:
* **Code**: `200 OK`
* **Error Responses**:
- * **Code**: `400 Bad Request`
- **Notes**: An invalid resource path has been requested. Check the list of
- valid resource paths at the top of this section.
- * **Code**: `404 Not Found`
+ * **Code**: `400`; Reason: `Invalid Resource Path`
+ **Notes**: Check the list of valid resource paths at the top of this section.
+ * **Code**: `422`; Reason: `Non Integer Value`
+ **Notes**: When setting the status code with a non integer value.
+ * **Code**: `400`; Reason: `Invalid Status Code`
+ **Notes**: When setting a non-supported status code.
+ * **Code**: `404`; Reason: `Handler ID Not Found`
**Notes**: Refers to the handler resource itself.
* **Sample Call**:
```sh
diff --git a/spec/test/Makefile b/spec/test/Makefile
index 571fcb8..62ca762 100644
--- a/spec/test/Makefile
+++ b/spec/test/Makefile
@@ -11,6 +11,6 @@ wip:
test: lint
pipenv run behave --no-capture --tags=~@skip
fix: lint
- KAPOW_DEBUG_TESTS=1 pipenv run behave --stop --no-capture
+ KAPOW_DEBUG_TESTS=1 pipenv run behave --stop --no-capture --tags=~@skip
catalog:
pipenv run behave --format steps.usage --dry-run --no-summary -q
diff --git a/spec/test/features/control/append/error_malformed.feature b/spec/test/features/control/append/error_malformed.feature
index 567a4e3..493375c 100644
--- a/spec/test/features/control/append/error_malformed.feature
+++ b/spec/test/features/control/append/error_malformed.feature
@@ -28,4 +28,10 @@ Feature: Kapow! server reject append requests with malformed JSON bodies.
Hi! I am an invalid JSON document.
"""
Then I get 400 as response code
-# And I get "Malformed JSON" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Malformed JSON"
+ }
+ """
diff --git a/spec/test/features/control/append/error_unprocessable.feature b/spec/test/features/control/append/error_unprocessable.feature
index 92121d9..06c2a0d 100644
--- a/spec/test/features/control/append/error_unprocessable.feature
+++ b/spec/test/features/control/append/error_unprocessable.feature
@@ -31,7 +31,13 @@ Feature: Kapow! server rejects requests with semantic errors.
}
"""
Then I get 422 as response code
-# And I get "Invalid Route" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Invalid Route"
+ }
+ """
Scenario: Error because bad route format.
If a request contains an invalid expression in the
@@ -48,4 +54,10 @@ Feature: Kapow! server rejects requests with semantic errors.
}
"""
Then I get 422 as response code
-# And I get "Invalid Route" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Invalid Route"
+ }
+ """
diff --git a/spec/test/features/control/append/success.feature b/spec/test/features/control/append/success.feature
index f66ef29..bf67e0c 100644
--- a/spec/test/features/control/append/success.feature
+++ b/spec/test/features/control/append/success.feature
@@ -33,7 +33,7 @@ Feature: Append new routes in Kapow! server.
}
"""
Then I get 201 as response code
-# And I get "Created" as response reason phrase
+ And I get "Created" as response reason phrase
And I get the following response body:
"""
{
@@ -64,7 +64,7 @@ Feature: Append new routes in Kapow! server.
}
"""
Then I get 201 as response code
-# And I get "Created" as response reason phrase
+ And I get "Created" as response reason phrase
And I get the following response body:
"""
{
diff --git a/spec/test/features/control/delete/error_notfound.feature b/spec/test/features/control/delete/error_notfound.feature
index 587363f..18d2a90 100644
--- a/spec/test/features/control/delete/error_notfound.feature
+++ b/spec/test/features/control/delete/error_notfound.feature
@@ -24,4 +24,10 @@ Feature: Fail to delete a route in Kapow! server.
Given I have a just started Kapow! server
When I delete the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
Then I get 404 as response code
-# And I get "Not Found" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Route Not Found"
+ }
+ """
diff --git a/spec/test/features/control/delete/success.feature b/spec/test/features/control/delete/success.feature
index 0b7a12a..614f186 100644
--- a/spec/test/features/control/delete/success.feature
+++ b/spec/test/features/control/delete/success.feature
@@ -26,4 +26,4 @@ Feature: Delete routes in Kapow! server.
| GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body |
When I delete the first route
Then I get 204 as response code
-# And I get "No Content" as response reason phrase
+ And I get "No Content" as response reason phrase
diff --git a/spec/test/features/control/get/error_notfound.feature b/spec/test/features/control/get/error_notfound.feature
index 903711c..dfdb951 100644
--- a/spec/test/features/control/get/error_notfound.feature
+++ b/spec/test/features/control/get/error_notfound.feature
@@ -24,4 +24,10 @@ Feature: Fail to retrieve route details in Kapow! server.
Given I have a just started Kapow! server
When I get the route with id "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
Then I get 404 as response code
-# And I get "Not Found" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Route Not Found"
+ }
+ """
diff --git a/spec/test/features/control/get/success.feature b/spec/test/features/control/get/success.feature
index d8a73cf..04d1287 100644
--- a/spec/test/features/control/get/success.feature
+++ b/spec/test/features/control/get/success.feature
@@ -26,7 +26,7 @@ Feature: Retrieve route details in Kapow! server.
| GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body |
When I get the first route
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the following response body:
"""
{
diff --git a/spec/test/features/control/insert/error_malformed.feature b/spec/test/features/control/insert/error_malformed.feature
index 22f9806..20596bc 100644
--- a/spec/test/features/control/insert/error_malformed.feature
+++ b/spec/test/features/control/insert/error_malformed.feature
@@ -37,4 +37,10 @@ Feature: Kapow! server rejects insertion requests with malformed JSON bodies.
}
"""
Then I get 400 as response code
-# And I get "Malformed JSON" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Malformed JSON"
+ }
+ """
diff --git a/spec/test/features/control/insert/error_unprocessable.feature b/spec/test/features/control/insert/error_unprocessable.feature
index 07f2a19..18468da 100644
--- a/spec/test/features/control/insert/error_unprocessable.feature
+++ b/spec/test/features/control/insert/error_unprocessable.feature
@@ -31,7 +31,7 @@ Feature: Kapow! server rejects insertion requests with semantic errors.
}
"""
Then I get 422 as response code
-# And I get "Invalid Route" as response reason phrase
+ And I get "Invalid Route" as response reason phrase
Scenario: Error because wrong route specification.
If a request contains an invalid expression in the
@@ -49,7 +49,7 @@ Feature: Kapow! server rejects insertion requests with semantic errors.
}
"""
Then I get 422 as response code
-# And I get "Invalid Route" as response reason phrase
+ And I get "Invalid Route" as response reason phrase
Scenario: Error because negative index specified.
If a request contains a negative number in the
@@ -67,4 +67,10 @@ Feature: Kapow! server rejects insertion requests with semantic errors.
}
"""
Then I get 422 as response code
-# And I get "Invalid Route" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Invalid Route"
+ }
+ """
diff --git a/spec/test/features/control/insert/success.feature b/spec/test/features/control/insert/success.feature
index 9264106..d93d5e1 100644
--- a/spec/test/features/control/insert/success.feature
+++ b/spec/test/features/control/insert/success.feature
@@ -40,7 +40,7 @@ Feature: Insert new routes in Kapow! server.
}
"""
Then I get 201 as response code
-# And I get "Created" as response reason phrase
+ And I get "Created" as response reason phrase
And I get the following response body:
"""
{
@@ -69,7 +69,7 @@ Feature: Insert new routes in Kapow! server.
}
"""
Then I get 201 as response code
-# And I get "Created" as response reason phrase
+ And I get "Created" as response reason phrase
And I get the following response body:
"""
{
diff --git a/spec/test/features/control/list/success.feature b/spec/test/features/control/list/success.feature
index 4817467..0361043 100644
--- a/spec/test/features/control/list/success.feature
+++ b/spec/test/features/control/list/success.feature
@@ -25,7 +25,7 @@ Feature: Listing routes in a Kapow! server.
Given I have a just started Kapow! server
When I request a routes listing
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the following response body:
"""
[]
@@ -41,7 +41,7 @@ Feature: Listing routes in a Kapow! server.
| GET | /qux/{dirname} | /bin/sh -c | ls -la /request/params/dirname \| kapow set /response/body |
When I request a routes listing
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the following response body:
"""
[
diff --git a/spec/test/features/data/handler/error_handleridnotfound.feature b/spec/test/features/data/handler/error_handleridnotfound.feature
index 782a2dc..464891e 100644
--- a/spec/test/features/data/handler/error_handleridnotfound.feature
+++ b/spec/test/features/data/handler/error_handleridnotfound.feature
@@ -25,14 +25,10 @@ Feature: Fail to retrieve resources from nonexistent handler in Kapow! server.
Given I have a running Kapow! server
When I get the resource "/request/path" for the handler with id "XXXXXXXXXX"
Then I get 404 as response code
-# And I get "Handler ID Not Found" as response reason phrase
-
- Scenario: Try to get an invalid resource from a nonexistent handler.
- A request to retrieve an invalid resource from a nonexistent
- handler will trigger an invalid resource path error
- even if the resource is invalid.
-
- Given I have a running Kapow! server
- When I get the resource "/invalid/path" for the handler with id "XXXXXXXXXX"
- Then I get 400 as response code
-# And I get "Invalid Resource Path" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Handler ID Not Found"
+ }
+ """
diff --git a/spec/test/features/data/handler/error_invalidresource.feature b/spec/test/features/data/handler/error_invalidresource.feature
index 7afa680..fc911f1 100644
--- a/spec/test/features/data/handler/error_invalidresource.feature
+++ b/spec/test/features/data/handler/error_invalidresource.feature
@@ -27,4 +27,10 @@ Feature: Fail to retrieve an invalid resource for a handler in Kapow! server.
When I send a request to the testing route "/foo"
And I get the resource "/invented/path"
Then I get 400 as response code
-# And I get "Invalid Resource Path" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Invalid Resource Path"
+ }
+ """
diff --git a/spec/test/features/data/handler/error_itemnotfound.feature b/spec/test/features/data/handler/error_itemnotfound.feature
index 9b505ab..16dd246 100644
--- a/spec/test/features/data/handler/error_itemnotfound.feature
+++ b/spec/test/features/data/handler/error_itemnotfound.feature
@@ -28,4 +28,10 @@ Feature: Fail to retrieve nonexistent resource items in Kapow! server.
When I send a request to the testing route "/foo"
And I get the resource "/request/params/meloinvento"
Then I get 404 as response code
-# And I get "Resource Item Not Found" as response reason phrase
+ And the response header "Content-Type" contains "application/json"
+ And I get the following response body:
+ """
+ {
+ "reason": "Resource Item Not Found"
+ }
+ """
diff --git a/spec/test/features/data/handler/success.feature b/spec/test/features/data/handler/success.feature
index 5c4bbc5..91c5a36 100644
--- a/spec/test/features/data/handler/success.feature
+++ b/spec/test/features/data/handler/success.feature
@@ -28,7 +28,7 @@ Feature: Retrieve a resource from a handler in Kapow! server.
When I send a request to the testing route "/foo"
And I get the resource "/request/path"
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the following response raw body:
"""
/foo
@@ -44,7 +44,7 @@ Feature: Retrieve a resource from a handler in Kapow! server.
When I send a request to the testing route "/foo?name=bar"
And I get the resource "/request/params/name"
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the following response raw body:
"""
bar
diff --git a/spec/test/features/data/request/success.feature b/spec/test/features/data/request/success.feature
index 3c2fa2a..03ca2a9 100644
--- a/spec/test/features/data/request/success.feature
+++ b/spec/test/features/data/request/success.feature
@@ -34,7 +34,7 @@ Feature: Retrieve request resources from a handler in Kapow! server.
| body | | bodyVal1 |
And I get the resource ""
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the following response raw body:
"""
diff --git a/spec/test/features/data/response/success.feature b/spec/test/features/data/response/success.feature
index 3fc46e7..05cff7e 100644
--- a/spec/test/features/data/response/success.feature
+++ b/spec/test/features/data/response/success.feature
@@ -41,7 +41,7 @@ Feature: Setting values for handler response resources in Kapow! server.
And I set the resource "" with value ""
And I release the testing request
Then I get 200 as response code
-# And I get "OK" as response reason phrase
+ And I get "OK" as response reason phrase
And I get the value "" for the response "" named "" in the testing request
Examples:
diff --git a/spec/test/features/steps/steps.py b/spec/test/features/steps/steps.py
index f280e0f..4fc6ffb 100644
--- a/spec/test/features/steps/steps.py
+++ b/spec/test/features/steps/steps.py
@@ -52,7 +52,7 @@ class Env(EnvironConfig):
#: Where the User Interface is
KAPOW_USER_URL = StringVar(default="http://localhost:8080")
- KAPOW_BOOT_TIMEOUT = IntVar(default=10)
+ KAPOW_BOOT_TIMEOUT = IntVar(default=1000)
KAPOW_DEBUG_TESTS = BooleanVar(default=False)
@@ -185,6 +185,16 @@ def step_impl(context, code):
assert context.testing_response.status_code == int(code), f"Got {context.testing_response.status_code} instead"
+@then('the response header "{header_name}" contains "{value}"')
+def step_impl(context, header_name, value):
+ assert context.response.headers.get(header_name, "").split(';')[0] == value, f"Got {context.response.headers.get(header_name)} instead"
+
+
+@then('the testing response header {header_name} contains {value}')
+def step_impl(context, header_name, value):
+ assert context.testing_response.headers.get(header_name) == value, f"Got {context.testing_response.headers.get(header_name)} instead"
+
+
@then('I get "{reason}" as response reason phrase')
def step_impl(context, reason):
assert context.response.reason == reason, f"Got {context.response.reason} instead"