Merge remote-tracking branch 'origin/master' into feature-new-doc

This commit is contained in:
Roberto Abdelkader Martínez Pérez
2019-11-19 13:02:48 +01:00
87 changed files with 487 additions and 801 deletions
+4 -2
View File
@@ -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
-226
View File
@@ -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
<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx</center>
</body>
</html>
```
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).
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client_test
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package http
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package http
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package http
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package http
import (
+32 -14
View File
@@ -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)
+54 -34
View File
@@ -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")
+27
View File
@@ -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()))
}
+3 -1
View File
@@ -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)
}
}
}
+3 -3
View File
@@ -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)
}
}
+26 -19
View File
@@ -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)
}
}
+44 -62
View File
@@ -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)
}
}
+5 -1
View File
@@ -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
}
+30 -3
View File
@@ -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)
}
}
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package data
import (
+1
View File
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package data
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package model
import (
+1
View File
@@ -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.
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package server
import (
+19
View File
@@ -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)
}
+47
View File
@@ -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)
}
}
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mux
import (
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mux
import (
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mux
import (
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mux
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mux
import (
+1
View File
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mux
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package user
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package spawn
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package spawn
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package user
import (
+1
View File
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package user
import (
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
-1
View File
@@ -1 +0,0 @@
go run . route add -c 'echo foo' /
+4 -1
View File
@@ -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
+15 -10
View File
@@ -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
-19
View File
@@ -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)'
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env sh
curl -X POST --data-binary @- http://localhost:8080/eval <<EOF
touch /tmp/kapow_was_here
EOF
echo 'Proof of success:'
ls -l /tmp/kapow_was_here
-35
View File
@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nmap</title>
</head>
<body>
<form id="nmap-params" method="post" action="nmap.xml">
<fieldset>
<legend>Nmap parameters</legend>
<div>
<label for="target_spec">Target Specification:</label>
<input name="target_spec" type="text" placeholder="ip, domain, network, range" value="127.0.0.1" required autofocus>
<p>
Can pass hostnames, IP addresses, networks, etc. e.g.:
scanme.nmap.org, microsoft.com/24, 192.168.0.1;
10.0.0-255.1-254
</p>
</div>
<div>
<label for="port_ranges">Port Ranges:</label>
<input name="port_ranges" type="text" placeholder="port, range, list" value="8080" required>
<p>
Only scan specified ports. e.g.: 22; 1-65535;
U:53,111,137,T:21-25,80,139,8080,S:9
</p>
</div>
<div>
<input name="scan" type="submit" value="Scan">
<input name="reset" type="reset" value="Reset">
</div>
</fieldset>
</form>
</body>
</html>
-55
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
FROM bbvalabsci/kapow:latest
RUN apk add nmap
COPY nmap.pow /tmp/
CMD ["server", "/tmp/nmap.pow"]
-19
View File
@@ -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'
-41
View File
@@ -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
-26
View File
@@ -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'
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env sh
curl -X POST --data-binary @- http://localhost:8080/convert/markdown/man <<EOF
# This is not a pipe
1. hello
1. goodbye
EOF
-41
View File
@@ -1,41 +0,0 @@
<html>
<head>
<title>PDF Editor</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/siimple/dist/siimple.min.css">
<head>
<body>
<div class="siimple-content siimple-content--extra-large">
<div class="siimple-grid">
<div class="siimple-grid-row">
<div class="siimple-grid-col siimple-grid-col--12">
<div class="siimple-grid-col siimple-grid-col--6">
<div class="siimple-form">
<form action="/editor/pdf" method="post" target="result" id="editor">
<div class="siimple-form-title">AWYSIWYG PDF Editor</div>
<div class="siimple-form-field">
<div class="siimple-form-field-label">InputFormat</div>
<select name="from">
<option value="markdown">Markdown</option>
<option value="rst">ReStructuredText</option>
</select>
</div>
<div class="siimple-form-field">
<div class="siimple-form-field-label">InputFormat</div>
<textarea class="siimple-textarea siimple-textarea--fluid" rows="25" name="content">Example text</textarea>
</div>
<div class="siimple-form-field">
<div class="siimple-btn siimple-btn--blue" onclick="document.getElementById('editor').submit();">Preview!</div>
</div>
</form>
</div>
</div>
<div class="siimple-grid-col siimple-grid-col--6">
<iframe name="result" src="" style="height: 100%; width: 100%;"></iframe>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
-20
View File
@@ -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'
-28
View File
@@ -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}
-16
View File
@@ -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/<network-interface> | 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!
-1
View File
@@ -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'
-43
View File
@@ -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
<html>
<body>
<a href='javascript: Array.from(document.querySelectorAll("a")).filter(x => x.href.indexOf("magnet") != -1 ).map(x => x.href = "http://localhost:8080/save/magnet?link="+encodeURI(x.href))'>Add me to your bookmarks!</a>
</body>
</html>
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"'
+42 -21
View File
@@ -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**:<br />
```sh
$ curl -X POST --data-binary @- $KAPOW_URL/routes <<EOF
@@ -295,8 +309,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**:<br />
```sh
$ curl -X PUT --data-binary @- $KAPOW_URL/routes <<EOF`
@@ -329,7 +343,7 @@ Removes the route identified by `{id}`.
* **Success Responses**:
* **Code**: `204 No Content`
* **Error Responses**:
* **Code**: `404 Not Found`
* **Code**: `404`; Reason: `Route Not Found`
* **Sample Call**:<br />
```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**:<br />
```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`<br />
**Content**: The value of the resource. Note that it may be empty.
* **Error Responses**:
* **Code**: `400 Bad Request`<br />
**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`<br />
* **Code**: `400`; Reason: `Invalid Resource Path`<br />
**Notes**: Check the list of valid resource paths at the top of this section.
* **Code**: `404`; Reason: `Handler ID Not Found`<br />
**Notes**: Refers to the handler resource itself.
* **Code**: `404 Not Found`<br />
* **Code**: `404`; Reason: `Resource Item Not Found`<br />
**Notes**: Refers to the named item in the corresponding data API resource.
* **Sample Call**:<br />
```sh
@@ -534,10 +552,13 @@ path doesn't exist or is invalid.
* **Success Responses**:
* **Code**: `200 OK`
* **Error Responses**:
* **Code**: `400 Bad Request`<br />
**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`<br />
* **Code**: `400`; Reason: `Invalid Resource Path`<br />
**Notes**: Check the list of valid resource paths at the top of this section.
* **Code**: `422`; Reason: `Non Integer Value`<br />
**Notes**: When setting the status code with a non integer value.
* **Code**: `400`; Reason: `Invalid Status Code`<br />
**Notes**: When setting a non-supported status code.
* **Code**: `404`; Reason: `Handler ID Not Found`<br />
**Notes**: Refers to the handler resource itself.
* **Sample Call**:<br />
```sh
+1 -1
View File
@@ -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
@@ -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"
}
"""
@@ -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"
}
"""
@@ -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:
"""
{
@@ -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"
}
"""
@@ -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
@@ -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"
}
"""
@@ -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:
"""
{
@@ -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"
}
"""
@@ -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"
}
"""
@@ -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:
"""
{
@@ -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:
"""
[
@@ -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"
}
"""
@@ -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"
}
"""
@@ -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"
}
"""
@@ -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
@@ -34,7 +34,7 @@ Feature: Retrieve request resources from a handler in Kapow! server.
| body | | bodyVal1 |
And I get the resource "<resourcePath>"
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:
"""
<value>
@@ -41,7 +41,7 @@ Feature: Setting values for handler response resources in Kapow! server.
And I set the resource "<resourcePath>" with value "<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 "<value>" for the response "<fieldType>" named "<elementName>" in the testing request
Examples:
+11 -1
View File
@@ -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"