diff --git a/resources/postman/Switcher GitOps.postman_collection.json b/resources/postman/Switcher GitOps.postman_collection.json index e2a59c1..964c7b8 100644 --- a/resources/postman/Switcher GitOps.postman_collection.json +++ b/resources/postman/Switcher GitOps.postman_collection.json @@ -12,6 +12,16 @@ { "name": "Create", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "POST", "header": [], "body": { @@ -38,6 +48,16 @@ { "name": "Update", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "PUT", "header": [], "body": { @@ -64,6 +84,16 @@ { "name": "Update (token)", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "PUT", "header": [], "body": { @@ -90,6 +120,16 @@ { "name": "Update (force sync)", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "PUT", "header": [], "body": { @@ -119,6 +159,16 @@ "disableBodyPruning": true }, "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "GET", "header": [], "body": { @@ -146,6 +196,16 @@ { "name": "Fetch By Domain Id / Env", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "GET", "header": [], "url": { @@ -165,6 +225,16 @@ { "name": "Delete By Domain Id / Env", "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{gitopsToken}}", + "type": "string" + } + ] + }, "method": "DELETE", "header": [], "body": { diff --git a/resources/swagger.yaml b/resources/swagger.yaml index 6cf2626..e385b99 100644 --- a/resources/swagger.yaml +++ b/resources/swagger.yaml @@ -15,6 +15,11 @@ servers: description: Local - url: https://localhost:8000 description: Remote +tags: + - name: API + description: API status & docs + - name: Account API + description: Account management paths: /api/check: get: @@ -64,6 +69,8 @@ paths: - Account API summary: Create a new account description: Create a new account and starts handler when active + security: + - bearerAuth: [] requestBody: required: true content: @@ -94,6 +101,8 @@ paths: - Account API summary: Update an existing account description: Update an existing account and starts handler when active + security: + - bearerAuth: [] requestBody: required: true content: @@ -125,6 +134,8 @@ paths: - Account API summary: Get All accounts by domain ID description: Get all accounts by domain ID + security: + - bearerAuth: [] parameters: - name: domainId in: path @@ -166,6 +177,8 @@ paths: - Account API summary: Get account by domain ID and environment description: Get account by domain ID and environment + security: + - bearerAuth: [] parameters: - name: domainId in: path @@ -210,6 +223,8 @@ paths: - Account API summary: Delete account by domain ID and environment description: Delete account by domain ID and environment + security: + - bearerAuth: [] parameters: - name: domainId in: path @@ -240,6 +255,11 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT schemas: AccountRequest: type: object diff --git a/src/controller/account.go b/src/controller/account.go index f729513..40a96ee 100644 --- a/src/controller/account.go +++ b/src/controller/account.go @@ -31,18 +31,21 @@ func NewAccountController(repo repository.AccountRepository, coreHandler *core.C } func (controller *AccountController) RegisterRoutes(r *mux.Router) http.Handler { - r.NewRoute().Path(controller.routeAccountPath).Name("CreateAccount").HandlerFunc(controller.CreateAccountHandler).Methods(http.MethodPost) - r.NewRoute().Path(controller.routeAccountPath).Name("UpdateAccount").HandlerFunc(controller.UpdateAccountHandler).Methods(http.MethodPut) - r.NewRoute().Path(controller.routeAccountPath + "/{domainId}").Name("GelAllAccountsByDomainId").HandlerFunc(controller.FetchAllAccountsByDomainIdHandler).Methods(http.MethodGet) - r.NewRoute().Path(controller.routeAccountPath + "/{domainId}/{enviroment}").Name("GetAccount").HandlerFunc(controller.FetchAccountHandler).Methods(http.MethodGet) - r.NewRoute().Path(controller.routeAccountPath + "/{domainId}/{enviroment}").Name("DeleteAccount").HandlerFunc(controller.DeleteAccountHandler).Methods(http.MethodDelete) + r.NewRoute().Path(controller.routeAccountPath).Name("CreateAccount").Handler( + ValidateToken(http.HandlerFunc(controller.CreateAccountHandler))).Methods(http.MethodPost) + r.NewRoute().Path(controller.routeAccountPath).Name("UpdateAccount").Handler( + ValidateToken(http.HandlerFunc(controller.UpdateAccountHandler))).Methods(http.MethodPut) + r.NewRoute().Path(controller.routeAccountPath + "/{domainId}").Name("GelAllAccountsByDomainId").Handler( + ValidateToken(http.HandlerFunc(controller.FetchAllAccountsByDomainIdHandler))).Methods(http.MethodGet) + r.NewRoute().Path(controller.routeAccountPath + "/{domainId}/{enviroment}").Name("GetAccount").Handler( + ValidateToken(http.HandlerFunc(controller.FetchAccountHandler))).Methods(http.MethodGet) + r.NewRoute().Path(controller.routeAccountPath + "/{domainId}/{enviroment}").Name("DeleteAccount").Handler( + ValidateToken(http.HandlerFunc(controller.DeleteAccountHandler))).Methods(http.MethodDelete) return r } func (controller *AccountController) CreateAccountHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - var accountRequest model.Account err := json.NewDecoder(r.Body).Decode(&accountRequest) if err != nil { @@ -71,8 +74,6 @@ func (controller *AccountController) CreateAccountHandler(w http.ResponseWriter, } func (controller *AccountController) FetchAccountHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - domainId := mux.Vars(r)["domainId"] enviroment := mux.Vars(r)["enviroment"] @@ -88,8 +89,6 @@ func (controller *AccountController) FetchAccountHandler(w http.ResponseWriter, } func (controller *AccountController) FetchAllAccountsByDomainIdHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - domainId := mux.Vars(r)["domainId"] accounts := controller.accountRepository.FetchAllByDomainId(domainId) @@ -109,8 +108,6 @@ func (controller *AccountController) FetchAllAccountsByDomainIdHandler(w http.Re } func (controller *AccountController) UpdateAccountHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - var accountRequest model.Account err := json.NewDecoder(r.Body).Decode(&accountRequest) if err != nil { @@ -136,8 +133,6 @@ func (controller *AccountController) UpdateAccountHandler(w http.ResponseWriter, } func (controller *AccountController) DeleteAccountHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - domainId := mux.Vars(r)["domainId"] enviroment := mux.Vars(r)["enviroment"] diff --git a/src/controller/account_test.go b/src/controller/account_test.go index a01c9f0..210f5c8 100644 --- a/src/controller/account_test.go +++ b/src/controller/account_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/switcherapi/switcher-gitops/src/config" @@ -15,13 +16,20 @@ import ( "github.com/switcherapi/switcher-gitops/src/utils" ) +func TestToken(t *testing.T) { + token := generateToken("test", time.Minute) + assert.NotEmpty(t, token) +} + func TestCreateAccountHandler(t *testing.T) { + token := generateToken("test", time.Minute) + t.Run("Should create an account", func(t *testing.T) { // Test accountV1.Domain.ID = "123-controller-create-account" payload, _ := json.Marshal(accountV1) req, _ := http.NewRequest(http.MethodPost, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert var accountResponse model.Account @@ -37,7 +45,7 @@ func TestCreateAccountHandler(t *testing.T) { // Test payload := []byte("") req, _ := http.NewRequest(http.MethodPost, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusBadRequest, response.Code) @@ -51,7 +59,7 @@ func TestCreateAccountHandler(t *testing.T) { // Test payload, _ := json.Marshal(accountV1) req, _ := http.NewRequest(http.MethodPost, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusInternalServerError, response.Code) @@ -60,6 +68,8 @@ func TestCreateAccountHandler(t *testing.T) { } func TestFetchAccountHandler(t *testing.T) { + token := generateToken("test", time.Minute) + t.Run("Should fetch an account by domain ID / environment", func(t *testing.T) { // Create an account accountV1.Domain.ID = "123-controller-fetch-account" @@ -68,7 +78,7 @@ func TestFetchAccountHandler(t *testing.T) { // Test payload := []byte("") req, _ := http.NewRequest(http.MethodGet, accountController.routeAccountPath+"/"+accountV1.Domain.ID+"/"+accountV1.Environment, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert var accountResponse model.Account @@ -84,7 +94,7 @@ func TestFetchAccountHandler(t *testing.T) { // Test payload := []byte("") req, _ := http.NewRequest(http.MethodGet, accountController.routeAccountPath+"/not-found/default", bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusNotFound, response.Code) @@ -101,7 +111,7 @@ func TestFetchAccountHandler(t *testing.T) { // Test payload := []byte("") req, _ := http.NewRequest(http.MethodGet, accountController.routeAccountPath+"/"+accountV1.Domain.ID, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert var accountsResponse []model.Account @@ -118,7 +128,7 @@ func TestFetchAccountHandler(t *testing.T) { // Test payload := []byte("") req, _ := http.NewRequest(http.MethodGet, accountController.routeAccountPath+"/not-found", bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusNotFound, response.Code) @@ -127,6 +137,8 @@ func TestFetchAccountHandler(t *testing.T) { } func TestUpdateAccountHandler(t *testing.T) { + token := generateToken("test", time.Minute) + t.Run("Should update an account", func(t *testing.T) { // Create an account accountV1.Domain.ID = "123-controller-update-account" @@ -141,7 +153,7 @@ func TestUpdateAccountHandler(t *testing.T) { // Test payload, _ := json.Marshal(accountV2) req, _ := http.NewRequest(http.MethodPut, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert var accountResponse model.Account @@ -167,7 +179,7 @@ func TestUpdateAccountHandler(t *testing.T) { payload, _ := json.Marshal(accountRequest) req, _ := http.NewRequest(http.MethodPut, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert var accountResponse model.Account @@ -189,7 +201,7 @@ func TestUpdateAccountHandler(t *testing.T) { // Test payload := []byte("") req, _ := http.NewRequest(http.MethodPut, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusBadRequest, response.Code) @@ -207,7 +219,7 @@ func TestUpdateAccountHandler(t *testing.T) { // Test payload, _ := json.Marshal(accountV1) req, _ := http.NewRequest(http.MethodPut, accountController.routeAccountPath, bytes.NewBuffer(payload)) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusInternalServerError, response.Code) @@ -216,6 +228,8 @@ func TestUpdateAccountHandler(t *testing.T) { } func TestDeleteAccountHandler(t *testing.T) { + token := generateToken("test", time.Minute) + t.Run("Should delete an account by domain ID / environment", func(t *testing.T) { // Create an account accountV1.Domain.ID = "123-controller-delete-account" @@ -223,7 +237,7 @@ func TestDeleteAccountHandler(t *testing.T) { // Test req, _ := http.NewRequest(http.MethodDelete, accountController.routeAccountPath+"/"+accountV1.Domain.ID+"/"+accountV1.Environment, nil) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusNoContent, response.Code) @@ -232,7 +246,7 @@ func TestDeleteAccountHandler(t *testing.T) { t.Run("Should not delete an account by domain ID / environment - not found", func(t *testing.T) { // Test req, _ := http.NewRequest(http.MethodDelete, accountController.routeAccountPath+"/not-found/default", nil) - response := executeRequest(req) + response := executeRequest(req, r, token) // Assert assert.Equal(t, http.StatusInternalServerError, response.Code) @@ -240,6 +254,51 @@ func TestDeleteAccountHandler(t *testing.T) { }) } +func TestUnnauthorizedAccountHandler(t *testing.T) { + t.Run("Should not create an account - unauthorized", func(t *testing.T) { + // Test + payload, _ := json.Marshal(accountV1) + req, _ := http.NewRequest(http.MethodPost, accountController.routeAccountPath, bytes.NewBuffer(payload)) + response := executeRequest(req, r, "") + + // Assert + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Equal(t, "{\"error\":\"Invalid token\"}", response.Body.String()) + }) + + t.Run("Should not fetch an account by domain ID / environment - unauthorized", func(t *testing.T) { + // Test + payload := []byte("") + req, _ := http.NewRequest(http.MethodGet, accountController.routeAccountPath+"/123/default", bytes.NewBuffer(payload)) + response := executeRequest(req, r, "") + + // Assert + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Equal(t, "{\"error\":\"Invalid token\"}", response.Body.String()) + }) + + t.Run("Should not update an account - unauthorized", func(t *testing.T) { + // Test + payload, _ := json.Marshal(accountV1) + req, _ := http.NewRequest(http.MethodPut, accountController.routeAccountPath, bytes.NewBuffer(payload)) + response := executeRequest(req, r, "") + + // Assert + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Equal(t, "{\"error\":\"Invalid token\"}", response.Body.String()) + }) + + t.Run("Should not delete an account by domain ID / environment - unauthorized", func(t *testing.T) { + // Test + req, _ := http.NewRequest(http.MethodDelete, accountController.routeAccountPath+"/123/default", nil) + response := executeRequest(req, r, "") + + // Assert + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Equal(t, "{\"error\":\"Invalid token\"}", response.Body.String()) + }) +} + // Helpers func givenAccountRequest(data model.Account) (*httptest.ResponseRecorder, *http.Request) { diff --git a/src/controller/api.go b/src/controller/api.go index 2aabd61..4d41614 100644 --- a/src/controller/api.go +++ b/src/controller/api.go @@ -39,12 +39,8 @@ func NewApiController(coreHandler *core.CoreHandler) *ApiController { } } -func ConfigureHeaders(w http.ResponseWriter) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") -} - func (controller *ApiController) RegisterRoutes(r *mux.Router) http.Handler { + r.Use(DefaultHeaders) r.NewRoute().Path(controller.routeCheckApiPath).Name("CheckApi").HandlerFunc(controller.CheckApiHandler).Methods(http.MethodGet) r.NewRoute().Path(controller.routeApiDocsPath).Name("ApiDocs").HandlerFunc(controller.ApiDocsHandler).Methods(http.MethodGet) @@ -52,8 +48,6 @@ func (controller *ApiController) RegisterRoutes(r *mux.Router) http.Handler { } func (controller *ApiController) CheckApiHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - utils.ResponseJSON(w, ApiCheckResponse{ Status: "All good", Version: "1.0.1", @@ -69,8 +63,6 @@ func (controller *ApiController) CheckApiHandler(w http.ResponseWriter, r *http. } func (controller *ApiController) ApiDocsHandler(w http.ResponseWriter, r *http.Request) { - ConfigureHeaders(w) - w.WriteHeader(http.StatusOK) http.ServeFile(w, r, "resources/swagger.yaml") } diff --git a/src/controller/api_test.go b/src/controller/api_test.go index a77ae94..5acb68d 100644 --- a/src/controller/api_test.go +++ b/src/controller/api_test.go @@ -9,7 +9,7 @@ import ( func TestCheckApiHandler(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/api/check", nil) - response := executeRequest(req) + response := executeRequest(req, r, "") assert.Equal(t, http.StatusOK, response.Code) assert.Contains(t, response.Body.String(), "All good") @@ -17,6 +17,6 @@ func TestCheckApiHandler(t *testing.T) { func TestApiDocsHandler(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/api/docs", nil) - response := executeRequest(req) + response := executeRequest(req, r, "") assert.Equal(t, http.StatusOK, response.Code) } diff --git a/src/controller/controller_test.go b/src/controller/controller_test.go index 0636504..33fe0ba 100644 --- a/src/controller/controller_test.go +++ b/src/controller/controller_test.go @@ -6,7 +6,9 @@ import ( "net/http/httptest" "os" "testing" + "time" + "github.com/golang-jwt/jwt" "github.com/gorilla/mux" "github.com/switcherapi/switcher-gitops/src/config" "github.com/switcherapi/switcher-gitops/src/core" @@ -50,13 +52,38 @@ func shutdown() { mongoDb.Client().Disconnect(context.Background()) } -func executeRequest(req *http.Request) *httptest.ResponseRecorder { +// Helpers + +func executeRequest(req *http.Request, router *mux.Router, token string) *httptest.ResponseRecorder { + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) + router.ServeHTTP(rr, req) return rr } +func generateToken(subject string, duration time.Duration) string { + apiKey := config.GetEnv("SWITCHER_API_JWT_SECRET") + + // Define the claims for the JWT token + claims := jwt.MapClaims{ + "iss": "Switcher API", + "subject": subject, + "exp": time.Now().Add(duration).Unix(), + } + + // Create the JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign the token with the API key + signedToken, _ := token.SignedString([]byte(apiKey)) + + return signedToken +} + // Fixtures var accountV1 = model.Account{ diff --git a/src/controller/middleware.go b/src/controller/middleware.go new file mode 100644 index 0000000..fdc3d75 --- /dev/null +++ b/src/controller/middleware.go @@ -0,0 +1,55 @@ +package controller + +import ( + "net/http" + "strings" + + "github.com/golang-jwt/jwt" + "github.com/switcherapi/switcher-gitops/src/config" + "github.com/switcherapi/switcher-gitops/src/utils" +) + +func DefaultHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") + next.ServeHTTP(w, r) + }) +} + +func ValidateToken(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if validateToken(r) { + next.ServeHTTP(w, r) + } else { + utils.ResponseJSON(w, ErrorResponse{ + Error: "Invalid token", + }, http.StatusUnauthorized) + } + }) +} + +func validateToken(r *http.Request) bool { + authToken := r.Header.Get("Authorization") + if authToken == "" { + return false + } + + parts := strings.Split(authToken, " ") + if len(parts) != 2 { + return false + } + + tokenStr := parts[1] + aoiKey := config.GetEnv("SWITCHER_API_JWT_SECRET") + + token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(aoiKey), nil + }) + + if err != nil { + return false + } + + return token.Valid +} diff --git a/src/controller/middleware_test.go b/src/controller/middleware_test.go new file mode 100644 index 0000000..1d42915 --- /dev/null +++ b/src/controller/middleware_test.go @@ -0,0 +1,84 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +var mr = mux.NewRouter() + +func TestTokenMiddleware(t *testing.T) { + + t.Run("Should return success when token is valid", func(t *testing.T) { + fakeServer := givenFakeServer(http.StatusOK) + defer fakeServer.Close() + + token := generateToken("test", time.Minute) + req, _ := http.NewRequest(http.MethodGet, fakeServer.URL+"/api/fake", nil) + response := executeRequest(req, mr, token) + + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("Should return unauthorized - expired token", func(t *testing.T) { + fakeServer := givenFakeServer(http.StatusOK) + defer fakeServer.Close() + + token := generateToken("test", -time.Minute) + req, _ := http.NewRequest(http.MethodGet, fakeServer.URL+"/api/fake", nil) + response := executeRequest(req, mr, token) + + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Contains(t, response.Body.String(), "Invalid token") + }) + + t.Run("Should return unauthorized - invalid token", func(t *testing.T) { + fakeServer := givenFakeServer(http.StatusUnauthorized) + defer fakeServer.Close() + + req, _ := http.NewRequest(http.MethodGet, fakeServer.URL+"/api/fake", nil) + response := executeRequest(req, mr, "invalid-token") + + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Contains(t, response.Body.String(), "Invalid token") + }) + + t.Run("Should return unauthorized - missing token", func(t *testing.T) { + fakeServer := givenFakeServer(http.StatusOK) + defer fakeServer.Close() + + req, _ := http.NewRequest(http.MethodGet, fakeServer.URL+"/api/fake", nil) + response := executeRequest(req, mr, "") + + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Contains(t, response.Body.String(), "Invalid token") + }) + + t.Run("Should return unauthorized - invalid token format", func(t *testing.T) { + fakeServer := givenFakeServer(http.StatusOK) + defer fakeServer.Close() + + req, _ := http.NewRequest(http.MethodGet, fakeServer.URL+"/api/fake", nil) + req.Header.Set("Authorization", "invalid-token") + response := executeRequest(req, mr, "") + + assert.Equal(t, http.StatusUnauthorized, response.Code) + assert.Contains(t, response.Body.String(), "Invalid token") + }) +} + +// Helpers + +func givenFakeServer(status int) *httptest.Server { + mr.Path("/api/fake").Handler(ValidateToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(status) + w.Header().Set("Content-Type", "application/json") + }))) + + return httptest.NewServer(mr) +}