diff --git a/.circleci/config.yml b/.circleci/config.yml index d42d576..925d765 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,6 +60,17 @@ jobs: name: Run tests command: make test-ci + shared: + docker: + - image: circleci/golang:1.12 + working_directory: ~/offen/shared + steps: + - checkout: + path: ~/offen + - run: + name: Run tests + command: make test + vault: docker: - image: circleci/node:10-browsers @@ -150,3 +161,4 @@ workflows: - script - auditorium - packages + - shared diff --git a/docker-compose.yml b/docker-compose.yml index 0990790..e354e11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,11 +3,11 @@ version: '3' services: kms: image: golang:1.12 - working_dir: /kms + working_dir: /code/kms volumes: - - ./kms:/kms - - ./local.offen.dev.pem:/kms/local.offen.dev.pem - - ./local.offen.dev-key.pem:/kms/local.offen.dev-key.pem + - .:/code + - ./local.offen.dev.pem:/code/kms/local.offen.dev.pem + - ./local.offen.dev-key.pem:/code/kms/local.offen.dev-key.pem - $GOPATH/pkg/mod:/go/pkg/mod environment: - GOPATH=/go @@ -18,11 +18,11 @@ services: server: image: golang:1.12 - working_dir: /server + working_dir: /code/server volumes: - - ./server:/server - - ./local.offen.dev.pem:/server/local.offen.dev.pem - - ./local.offen.dev-key.pem:/server/local.offen.dev-key.pem + - .:/code + - ./local.offen.dev.pem:/code/server/local.offen.dev.pem + - ./local.offen.dev-key.pem:/code/server/local.offen.dev-key.pem - $GOPATH/pkg/mod:/go/pkg/mod environment: - GOPATH=/go diff --git a/shared/Makefile b/shared/Makefile new file mode 100644 index 0000000..c56d5e2 --- /dev/null +++ b/shared/Makefile @@ -0,0 +1,2 @@ +test: + @go test -race -cover ./... diff --git a/shared/README.md b/shared/README.md new file mode 100644 index 0000000..c124d27 --- /dev/null +++ b/shared/README.md @@ -0,0 +1,15 @@ +# shared + +`shared` contains Go packages shared across applications. Consumers need to symlink the folder into their app directory. + +--- + +## Development + +### Commands + +#### Run the tests + +``` +make test +``` diff --git a/shared/http/errors.go b/shared/http/errors.go new file mode 100644 index 0000000..9534104 --- /dev/null +++ b/shared/http/errors.go @@ -0,0 +1,22 @@ +package http + +import ( + "encoding/json" + "net/http" +) + +type errorResponse struct { + Error string `json:"error"` + Status int `json:"status"` +} + +func jsonError(err error, status int) []byte { + r := errorResponse{err.Error(), status} + b, _ := json.Marshal(r) + return b +} + +func RespondWithJSONError(w http.ResponseWriter, err error, status int) { + w.WriteHeader(status) + w.Write(jsonError(err, status)) +} diff --git a/shared/http/errors_test.go b/shared/http/errors_test.go new file mode 100644 index 0000000..6923218 --- /dev/null +++ b/shared/http/errors_test.go @@ -0,0 +1,19 @@ +package http + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func TestRespondWithJSONError(t *testing.T) { + w := httptest.NewRecorder() + RespondWithJSONError(w, errors.New("does not work"), http.StatusInternalServerError) + if w.Code != http.StatusInternalServerError { + t.Errorf("Unexpected status code %d", w.Code) + } + if w.Body.String() != `{"error":"does not work","status":500}` { + t.Errorf("Unexpected response body %s", w.Body.String()) + } +} diff --git a/shared/http/middleware.go b/shared/http/middleware.go new file mode 100644 index 0000000..94d34e4 --- /dev/null +++ b/shared/http/middleware.go @@ -0,0 +1,29 @@ +package http + +import "net/http" + +func CorsMiddleware(next http.Handler, origin string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Methods", "POST,GET") + w.Header().Set("Access-Control-Allow-Origin", origin) + next.ServeHTTP(w, r) + }) +} + +func ContentTypeMiddleware(next http.Handler, contentType string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", contentType) + next.ServeHTTP(w, r) + }) +} + +func DoNotTrackMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if dnt := r.Header.Get("DNT"); dnt == "1" { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/shared/http/middleware_test.go b/shared/http/middleware_test.go new file mode 100644 index 0000000..6100238 --- /dev/null +++ b/shared/http/middleware_test.go @@ -0,0 +1,82 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestCorsMiddleware(t *testing.T) { + t.Run("default", func(t *testing.T) { + wrapped := CorsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + }), "https://www.example.net") + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + wrapped.ServeHTTP(w, r) + if h := w.Header().Get("Access-Control-Allow-Origin"); h != "https://www.example.net" { + t.Errorf("Unexpected header value %v", h) + } + }) +} + +func TestContentTypeMiddleware(t *testing.T) { + t.Run("default", func(t *testing.T) { + wrapped := ContentTypeMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + }), "application/json") + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + wrapped.ServeHTTP(w, r) + if h := w.Header().Get("Content-Type"); h != "application/json" { + t.Errorf("Unexpected header value %v", h) + } + }) +} + +func TestDoNotTrackMiddleware(t *testing.T) { + wrapped := DoNotTrackMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hey there")) + })) + t.Run("with header", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("DNT", "1") + wrapped.ServeHTTP(w, r) + + if w.Code != http.StatusNoContent { + t.Errorf("Unexpected status code %d", w.Code) + } + + if w.Body.String() != "" { + t.Errorf("Unexpected response body %s", w.Body.String()) + } + }) + t.Run("without header", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + wrapped.ServeHTTP(w, r) + + if w.Code != http.StatusOK { + t.Errorf("Unexpected status code %d", w.Code) + } + + if w.Body.String() != "hey there" { + t.Errorf("Unexpected response body %s", w.Body.String()) + } + }) + t.Run("with header allowing", func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("DNT", "0") + wrapped.ServeHTTP(w, r) + + if w.Code != http.StatusOK { + t.Errorf("Unexpected status code %d", w.Code) + } + + if w.Body.String() != "hey there" { + t.Errorf("Unexpected response body %s", w.Body.String()) + } + }) +}