package g8 import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" ) const ( FirstTestProviderClientPermission = "permission-1" SecondTestProviderClientPermission = "permission-2" TestProviderClientToken = "client-token-from-provider" TestProviderClientData = "client-data-from-provider" ) var ( mockClientProvider = NewClientProvider(func(token string) *Client { // We'll pretend that there's only one token that's valid in the client provider, every other token // returns nil if token == TestProviderClientToken { return &Client{ Token: TestProviderClientToken, Data: TestProviderClientData, Permissions: []string{FirstTestProviderClientPermission, SecondTestProviderClientPermission}, } } return nil }) ) type testHandler struct { } func (handler *testHandler) ServeHTTP(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusOK) } func testHandlerFunc(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusOK) } func TestUsability(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token")) var handler http.Handler = &testHandler{} handlerFunc := func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusOK) } router := http.NewServeMux() router.Handle("/handle", handler) router.Handle("/handle-protected", gate.Protect(handler)) router.HandleFunc("/handlefunc", handlerFunc) router.HandleFunc("/handlefunc-protected", gate.ProtectFunc(handlerFunc)) } func TestNewGate(t *testing.T) { gate := NewGate(nil) if gate == nil { t.Error("gate should not be nil") } } func TestUnprotectedHandler(t *testing.T) { request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "bad-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", &testHandler{}) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithInvalidToken(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token")) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "bad-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_ProtectWithValidToken(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token")) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "good-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectMultipleTimes(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token")) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "good-token")) badRequest, _ := http.NewRequest("GET", "/handle", http.NoBody) badRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "bad-token")) router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) for i := 0; i < 100; i++ { responseRecorder := httptest.NewRecorder() router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } responseRecorder = httptest.NewRecorder() router.ServeHTTP(responseRecorder, badRequest) if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", badRequest.Method, badRequest.URL, http.StatusOK, responseRecorder.Code) } } } func TestGate_ProtectWithValidTokenExposedThroughClientProvider(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClientProvider(mockClientProvider)) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", TestProviderClientToken)) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithValidTokenExposedThroughClientProviderWithCache(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClientProvider(mockClientProvider.WithCache(60*time.Minute, 70000))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", TestProviderClientToken)) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithInvalidTokenWhenUsingClientProvider(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClientProvider(mockClientProvider)) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "bad-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_ProtectWithPermissionsWhenValidTokenAndSufficientPermissionsWhileUsingClientProvider(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClientProvider(mockClientProvider)) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", TestProviderClientToken)) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectWithPermissions(&testHandler{}, []string{SecondTestProviderClientPermission})) router.ServeHTTP(responseRecorder, request) // Since the client returned from the mockClientProvider has FirstTestProviderClientPermission and // SecondTestProviderClientPermission and the testHandler is protected by SecondTestProviderClientPermission, // the request should be authorized if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithPermissionsWhenValidTokenAndInsufficientPermissionsWhileUsingClientProvider(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClientProvider(mockClientProvider)) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", TestProviderClientToken)) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectWithPermissions(&testHandler{}, []string{"unrelated-permission"})) router.ServeHTTP(responseRecorder, request) // Since the client returned from the mockClientProvider has FirstTestProviderClientPermission and // SecondTestProviderClientPermission and the testHandler is protected by a permission that the client does not // have, the request should be not be authorized if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_ProtectWithPermissionsWhenClientHasSufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClient("token").WithPermission("admin"))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectWithPermissions(&testHandler{}, []string{"admin"})) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "admin" and the testHandler // is protected by the permission "admin", the request should be authorized if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithPermissionsWhenClientHasInsufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClientWithPermissions("token", []string{"mod"}))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectWithPermissions(&testHandler{}, []string{"admin"})) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "mod" and the // testHandler is protected by the permission "admin", the request should be not be authorized if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_ProtectWithPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClient("mytoken").WithPermissions([]string{"create", "read", "update", "delete"}))) router := http.NewServeMux() router.Handle("/create", gate.ProtectWithPermissions(&testHandler{}, []string{"create"})) router.Handle("/read", gate.ProtectWithPermissions(&testHandler{}, []string{"read"})) router.Handle("/update", gate.ProtectWithPermissions(&testHandler{}, []string{"update"})) router.Handle("/delete", gate.ProtectWithPermissions(&testHandler{}, []string{"delete"})) router.Handle("/crud", gate.ProtectWithPermissions(&testHandler{}, []string{"create", "read", "update", "delete"})) router.Handle("/backup", gate.ProtectWithPermissions(&testHandler{}, []string{"read", "backup"})) checkRouterOutput := func(t *testing.T, router *http.ServeMux, url string, expectedResponseCode int) { t.Run(strings.TrimPrefix(url, "/"), func(t *testing.T) { request, _ := http.NewRequest("GET", url, http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "mytoken")) responseRecorder := httptest.NewRecorder() router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != expectedResponseCode { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, expectedResponseCode, responseRecorder.Code) } }) } checkRouterOutput(t, router, "/create", http.StatusOK) checkRouterOutput(t, router, "/read", http.StatusOK) checkRouterOutput(t, router, "/update", http.StatusOK) checkRouterOutput(t, router, "/delete", http.StatusOK) checkRouterOutput(t, router, "/crud", http.StatusOK) checkRouterOutput(t, router, "/backup", http.StatusUnauthorized) } func TestGate_ProtectWithPermissionWhenClientHasSufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClient("token").WithPermission("admin"))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectWithPermission(&testHandler{}, "admin")) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "admin" and the testHandler // is protected by the permission "admin", the request should be authorized if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithPermissionWhenClientHasInsufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClientWithPermissions("token", []string{"mod"}))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectWithPermission(&testHandler{}, "admin")) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "mod" and the // testHandler is protected by the permission "admin", the request should be not be authorized if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_PermissionMiddlewareWhenClientHasSufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClient("token").WithPermission("admin"))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.PermissionMiddleware("admin")(&testHandler{})) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "admin" and the testHandler // is protected by the permission "admin", the request should be authorized if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_PermissionMiddlewareWhenClientHasInsufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClientWithPermissions("token", []string{"mod"}))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.PermissionMiddleware("admin")(&testHandler{})) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "mod" and the // testHandler is protected by the permission "admin", the request should be not be authorized if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_ProtectFuncWithInvalidToken(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token")) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "bad-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectFunc(testHandlerFunc)) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_ProtectFuncWithValidToken(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithToken("good-token")) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "good-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectFunc(testHandlerFunc)) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectFuncWithPermissionWhenClientHasSufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClient("token").WithPermission("admin"))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.HandleFunc("/handle", gate.ProtectFuncWithPermission(testHandlerFunc, "admin")) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "admin" and the testHandler // is protected by the permission "admin", the request should be authorized if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectFuncWithPermissionWhenClientHasInsufficientPermissions(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService().WithClient(NewClientWithPermissions("token", []string{"mod"}))) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.HandleFunc("/handle", gate.ProtectFuncWithPermission(testHandlerFunc, "admin")) router.ServeHTTP(responseRecorder, request) // Since the client registered directly in the AuthorizationService has the permission "mod" and the // testHandler is protected by the permission "admin", the request should be not be authorized if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } } func TestGate_WithCustomUnauthorizedResponseBody(t *testing.T) { gate := New().WithAuthorizationService(NewAuthorizationService()).WithCustomUnauthorizedResponseBody([]byte("test")) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "bad-token")) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusUnauthorized { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusUnauthorized, responseRecorder.Code) } if responseBody, _ := io.ReadAll(responseRecorder.Body); string(responseBody) != "test" { t.Errorf("%s %s should have returned %s, but returned %s instead", request.Method, request.URL, "test", string(responseBody)) } } func TestGate_ProtectWithNoAuthorizationService(t *testing.T) { gate := New() request, _ := http.NewRequest("GET", "/handle", http.NoBody) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_ProtectWithRateLimit(t *testing.T) { gate := New().WithRateLimit(2) request, _ := http.NewRequest("GET", "/handle", http.NoBody) router := http.NewServeMux() router.Handle("/handle", gate.Protect(&testHandler{})) responseRecorder := httptest.NewRecorder() router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } responseRecorder = httptest.NewRecorder() router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } responseRecorder = httptest.NewRecorder() router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusTooManyRequests { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusTooManyRequests, responseRecorder.Code) } // Wait for rate limit time window to pass time.Sleep(time.Second) responseRecorder = httptest.NewRecorder() router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGate_WithCustomTokenExtractor(t *testing.T) { authorizationService := NewAuthorizationService().WithClientProvider(mockClientProvider) customTokenExtractorFunc := func(request *http.Request) string { sessionCookie, err := request.Cookie("session") if err != nil { return "" } return sessionCookie.Value } gate := New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.AddCookie(&http.Cookie{Name: "session", Value: TestProviderClientToken}) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) { if r.Context().Value(TokenContextKey) != TestProviderClientToken { t.Errorf("token should have been passed to the request context") } if r.Context().Value(DataContextKey) != TestProviderClientData { t.Errorf("data should have been passed to the request context") } w.WriteHeader(http.StatusOK) })) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } } func TestGateWithCustomHeader(t *testing.T) { authorizationService := NewAuthorizationService().WithClientProvider(mockClientProvider) customTokenExtractorFunc := func(request *http.Request) string { return request.Header.Get("X-API-Token") } gate := New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc) request, _ := http.NewRequest("GET", "/handle", http.NoBody) request.Header.Add("X-API-Token", TestProviderClientToken) responseRecorder := httptest.NewRecorder() router := http.NewServeMux() router.Handle("/handle", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) { if r.Context().Value(TokenContextKey) != TestProviderClientToken { t.Errorf("token should have been passed to the request context") } if r.Context().Value(DataContextKey) != TestProviderClientData { t.Errorf("data should have been passed to the request context") } w.WriteHeader(http.StatusOK) })) router.ServeHTTP(responseRecorder, request) if responseRecorder.Code != http.StatusOK { t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code) } }