659 lines
25 KiB
Go
659 lines
25 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"goyco/internal/database"
|
|
"goyco/internal/handlers"
|
|
"goyco/internal/middleware"
|
|
"goyco/internal/repositories"
|
|
"goyco/internal/services"
|
|
"goyco/internal/testutils"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
func TestIntegration_Handlers(t *testing.T) {
|
|
ctx := setupTestContext(t)
|
|
authService := ctx.AuthService
|
|
emailSender := ctx.Suite.EmailSender
|
|
userRepo := ctx.Suite.UserRepo
|
|
postRepo := ctx.Suite.PostRepo
|
|
|
|
t.Run("Auth_Handler_Complete_Workflow", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
registerResponse := makePostRequestWithJSON(t, ctx.Router, "/api/auth/register", map[string]any{
|
|
"username": "handler_user",
|
|
"email": "handler@example.com",
|
|
"password": "SecurePass123!",
|
|
})
|
|
if registerResponse.Code != http.StatusCreated {
|
|
t.Errorf("Expected status 201, got %d", registerResponse.Code)
|
|
}
|
|
|
|
registerPayload := assertJSONResponse(t, registerResponse, http.StatusCreated)
|
|
if success, _ := registerPayload["success"].(bool); !success {
|
|
t.Fatalf("Expected register response success, got %v", registerPayload)
|
|
}
|
|
|
|
user, err := userRepo.GetByUsername("handler_user")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get user after registration: %v", err)
|
|
}
|
|
|
|
mockToken := "test-verification-token"
|
|
|
|
hashedToken := testutils.HashVerificationToken(mockToken)
|
|
|
|
user.EmailVerificationToken = hashedToken
|
|
if err := userRepo.Update(user); err != nil {
|
|
t.Fatalf("Failed to update user with mock token: %v", err)
|
|
}
|
|
|
|
confirmResponse := makeGetRequest(t, ctx.Router, "/api/auth/confirm?token="+url.QueryEscape(mockToken))
|
|
if confirmResponse.Code != http.StatusOK {
|
|
t.Fatalf("Expected 200 when confirming email via handler, got %d", confirmResponse.Code)
|
|
}
|
|
|
|
loginSeed := createAuthenticatedUser(t, authService, userRepo, "auth_handler_login", "auth_handler_login@example.com")
|
|
|
|
loginAuth, err := authService.Login(loginSeed.User.Username, "SecurePass123!")
|
|
if err != nil {
|
|
t.Fatalf("Service login failed for seeded user: %v", err)
|
|
}
|
|
|
|
meResponse := makeAuthenticatedGetRequest(t, ctx.Router, "/api/auth/me", &authenticatedUser{User: loginSeed.User, Token: loginAuth.AccessToken}, nil)
|
|
if meResponse.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", meResponse.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Auth_Handler_Security_Validation", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
weakResponse := makePostRequestWithJSON(t, ctx.Router, "/api/auth/register", map[string]any{
|
|
"username": "weak_user",
|
|
"email": "weak@example.com",
|
|
"password": "123",
|
|
})
|
|
if weakResponse.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status 400 for weak password, got %d", weakResponse.Code)
|
|
}
|
|
|
|
weakErrorResponse := assertJSONResponse(t, weakResponse, http.StatusBadRequest)
|
|
if success, _ := weakErrorResponse["success"].(bool); success {
|
|
t.Error("Expected error response to have success=false")
|
|
}
|
|
if errorMsg, ok := weakErrorResponse["error"].(string); !ok || errorMsg == "" {
|
|
t.Error("Expected error response to contain validation error message")
|
|
}
|
|
|
|
invalidResponse := makePostRequestWithJSON(t, ctx.Router, "/api/auth/register", map[string]any{
|
|
"username": "invalid_user",
|
|
"email": "not-an-email",
|
|
"password": "SecurePass123!",
|
|
})
|
|
if invalidResponse.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status 400 for invalid email, got %d", invalidResponse.Code)
|
|
}
|
|
|
|
invalidEmailErrorResponse := assertJSONResponse(t, invalidResponse, http.StatusBadRequest)
|
|
if success, _ := invalidEmailErrorResponse["success"].(bool); success {
|
|
t.Error("Expected error response to have success=false")
|
|
}
|
|
if errorMsg, ok := invalidEmailErrorResponse["error"].(string); !ok || errorMsg == "" {
|
|
t.Error("Expected error response to contain validation error message")
|
|
}
|
|
|
|
incompleteResponse := makePostRequestWithJSON(t, ctx.Router, "/api/auth/register", map[string]any{
|
|
"username": "incomplete_user",
|
|
})
|
|
if incompleteResponse.Code != http.StatusBadRequest {
|
|
t.Errorf("Expected status 400 for missing fields, got %d", incompleteResponse.Code)
|
|
}
|
|
|
|
incompleteErrorResponse := assertJSONResponse(t, incompleteResponse, http.StatusBadRequest)
|
|
if success, _ := incompleteErrorResponse["success"].(bool); success {
|
|
t.Error("Expected error response to have success=false")
|
|
}
|
|
if errorMsg, ok := incompleteErrorResponse["error"].(string); !ok || errorMsg == "" {
|
|
t.Error("Expected error response to contain validation error message")
|
|
}
|
|
})
|
|
|
|
t.Run("Post_Handler_Complete_Workflow", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
user := createAuthenticatedUser(t, authService, userRepo, "post_user", "post@example.com")
|
|
|
|
postResponse := makePostRequest(t, ctx.Router, "/api/posts", map[string]any{
|
|
"title": "Handler Test Post",
|
|
"url": "https://example.com/handler-test",
|
|
"content": "This is a handler test post",
|
|
}, user, nil)
|
|
if postResponse.Code != http.StatusCreated {
|
|
t.Errorf("Expected status 201, got %d", postResponse.Code)
|
|
}
|
|
|
|
postResult := assertJSONResponse(t, postResponse, http.StatusCreated)
|
|
postDetails, ok := getDataFromResponse(postResult)
|
|
if !ok {
|
|
t.Fatalf("Expected data object in post response, got %v", postResult)
|
|
}
|
|
postID, ok := postDetails["id"].(float64)
|
|
if !ok {
|
|
t.Fatal("Expected post ID in response")
|
|
}
|
|
|
|
getResponse := makeGetRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d", int(postID)))
|
|
if getResponse.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", getResponse.Code)
|
|
}
|
|
|
|
postsResponse := makeGetRequest(t, ctx.Router, "/api/posts")
|
|
if postsResponse.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", postsResponse.Code)
|
|
}
|
|
|
|
searchResponse := makeGetRequest(t, ctx.Router, "/api/posts/search?q=handler")
|
|
if searchResponse.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", searchResponse.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Post_Handler_Security_Validation", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
postResponse := makePostRequestWithJSON(t, ctx.Router, "/api/posts", map[string]any{
|
|
"title": "Unauthorized Post",
|
|
"url": "https://example.com/unauthorized",
|
|
"content": "This should fail",
|
|
})
|
|
authErrorResponse := assertJSONResponse(t, postResponse, http.StatusUnauthorized)
|
|
if success, _ := authErrorResponse["success"].(bool); success {
|
|
t.Error("Expected error response to have success=false")
|
|
}
|
|
if errorMsg, ok := authErrorResponse["error"].(string); !ok || errorMsg == "" {
|
|
t.Error("Expected error response to contain authentication error message")
|
|
}
|
|
|
|
user := createAuthenticatedUser(t, authService, userRepo, "security_user", "security@example.com")
|
|
|
|
invalidResponse := makePostRequest(t, ctx.Router, "/api/posts", map[string]any{
|
|
"title": "",
|
|
"url": "not-a-url",
|
|
"content": "Invalid post",
|
|
}, user, nil)
|
|
postValidationErrorResponse := assertJSONResponse(t, invalidResponse, http.StatusBadRequest)
|
|
if success, _ := postValidationErrorResponse["success"].(bool); success {
|
|
t.Error("Expected error response to have success=false")
|
|
}
|
|
if errorMsg, ok := postValidationErrorResponse["error"].(string); !ok || errorMsg == "" {
|
|
t.Error("Expected error response to contain validation error message")
|
|
}
|
|
})
|
|
|
|
t.Run("Vote_Handler_Complete_Workflow", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
user := createAuthenticatedUser(t, authService, userRepo, "vote_handler_user", "vote_handler@example.com")
|
|
post := testutils.CreatePostWithRepo(t, postRepo, user.User.ID, "Vote Handler Test Post", "https://example.com/vote-handler")
|
|
|
|
voteResponse := makePostRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d/vote", post.ID), map[string]any{"type": "up"}, user, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
assertStatus(t, voteResponse, http.StatusOK)
|
|
|
|
getVoteResponse := makeAuthenticatedGetRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d/vote", post.ID), user, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
assertStatus(t, getVoteResponse, http.StatusOK)
|
|
|
|
getPostVotesResponse := makeAuthenticatedGetRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d/votes", post.ID), user, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
assertStatus(t, getPostVotesResponse, http.StatusOK)
|
|
|
|
removeVoteResponse := makeDeleteRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d/vote", post.ID), user, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
assertStatus(t, removeVoteResponse, http.StatusOK)
|
|
})
|
|
|
|
t.Run("User_Handler_Complete_Workflow", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
user := createAuthenticatedUser(t, authService, userRepo, "user_handler_user", "user_handler@example.com")
|
|
|
|
usersResponse := makeAuthenticatedGetRequest(t, ctx.Router, "/api/users", user, nil)
|
|
assertStatus(t, usersResponse, http.StatusOK)
|
|
|
|
getUserResponse := makeAuthenticatedGetRequest(t, ctx.Router, fmt.Sprintf("/api/users/%d", user.User.ID), user, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
|
assertStatus(t, getUserResponse, http.StatusOK)
|
|
|
|
getUserPostsResponse := makeAuthenticatedGetRequest(t, ctx.Router, fmt.Sprintf("/api/users/%d/posts", user.User.ID), user, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
|
assertStatus(t, getUserPostsResponse, http.StatusOK)
|
|
})
|
|
|
|
t.Run("Error_Handling_Invalid_Requests", func(t *testing.T) {
|
|
middleware.StopAllRateLimiters()
|
|
ctx.Suite.EmailSender.Reset()
|
|
|
|
invalidJSONResponse := makeRequest(t, ctx.Router, "POST", "/api/auth/register", []byte("invalid json"), map[string]string{"Content-Type": "application/json"})
|
|
jsonErrorResponse := assertJSONResponse(t, invalidJSONResponse, http.StatusBadRequest)
|
|
if success, _ := jsonErrorResponse["success"].(bool); success {
|
|
t.Error("Expected error response to have success=false")
|
|
}
|
|
if errorMsg, ok := jsonErrorResponse["error"].(string); !ok || errorMsg == "" {
|
|
t.Error("Expected error response to contain JSON parsing error message")
|
|
}
|
|
|
|
missingCTData := map[string]string{
|
|
"username": uniqueTestUsername(t, "missing_ct"),
|
|
"email": uniqueTestEmail(t, "missing_ct"),
|
|
"password": "SecurePass123!",
|
|
}
|
|
missingCTBody, _ := json.Marshal(missingCTData)
|
|
missingCTRequest := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(missingCTBody))
|
|
missingCTResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(missingCTResponse, missingCTRequest)
|
|
if missingCTResponse.Code == http.StatusTooManyRequests {
|
|
var rateLimitResponse map[string]any
|
|
if err := json.Unmarshal(missingCTResponse.Body.Bytes(), &rateLimitResponse); err != nil {
|
|
t.Errorf("Rate limited but response is not valid JSON: %v", err)
|
|
} else {
|
|
t.Logf("Rate limit hit (expected in full test suite run), but request was processed correctly (not rejected as invalid JSON)")
|
|
}
|
|
} else if missingCTResponse.Code != http.StatusCreated {
|
|
t.Errorf("Expected status 201, got %d", missingCTResponse.Code)
|
|
}
|
|
|
|
invalidEndpointRequest := httptest.NewRequest("GET", "/api/invalid/endpoint", nil)
|
|
invalidEndpointResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(invalidEndpointResponse, invalidEndpointRequest)
|
|
if invalidEndpointResponse.Code == http.StatusOK {
|
|
t.Error("Expected error for invalid endpoint")
|
|
}
|
|
})
|
|
|
|
t.Run("Security_Authentication_Bypass", func(t *testing.T) {
|
|
meRequest := httptest.NewRequest("GET", "/api/auth/me", nil)
|
|
meResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(meResponse, meRequest)
|
|
if meResponse.Code == http.StatusOK {
|
|
t.Error("Expected error for unauthenticated request")
|
|
}
|
|
|
|
invalidTokenRequest := httptest.NewRequest("GET", "/api/auth/me", nil)
|
|
invalidTokenRequest.Header.Set("Authorization", "Bearer invalid-token")
|
|
invalidTokenResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(invalidTokenResponse, invalidTokenRequest)
|
|
if invalidTokenResponse.Code == http.StatusOK {
|
|
t.Error("Expected error for invalid token")
|
|
}
|
|
|
|
malformedTokenRequest := httptest.NewRequest("GET", "/api/auth/me", nil)
|
|
malformedTokenRequest.Header.Set("Authorization", "InvalidFormat token")
|
|
malformedTokenResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(malformedTokenResponse, malformedTokenRequest)
|
|
if malformedTokenResponse.Code == http.StatusOK {
|
|
t.Error("Expected error for malformed token")
|
|
}
|
|
})
|
|
|
|
t.Run("Security_Input_Sanitization", func(t *testing.T) {
|
|
user := createAuthenticatedUser(t, authService, userRepo, "xss_user", "xss@example.com")
|
|
|
|
xssResponse := makePostRequest(t, ctx.Router, "/api/posts", map[string]any{
|
|
"title": "<script>alert('xss')</script>",
|
|
"url": "https://example.com/xss",
|
|
"content": "<script>alert('xss')</script>",
|
|
}, user, nil)
|
|
if xssResponse.Code != http.StatusCreated {
|
|
t.Errorf("Expected status 201 for XSS sanitization, got %d", xssResponse.Code)
|
|
}
|
|
|
|
xssResult := assertJSONResponse(t, xssResponse, http.StatusCreated)
|
|
if success, _ := xssResult["success"].(bool); !success {
|
|
t.Error("Expected XSS response to have success=true")
|
|
}
|
|
|
|
data, ok := getDataFromResponse(xssResult)
|
|
if !ok {
|
|
t.Fatalf("Expected data object in XSS response, got %T", xssResult["data"])
|
|
}
|
|
|
|
title, ok := data["title"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected title string in XSS response, got %T", data["title"])
|
|
}
|
|
|
|
if strings.Contains(title, "<script>") {
|
|
t.Errorf("Expected script tags to be HTML-escaped in title, got: %s", title)
|
|
}
|
|
if !strings.Contains(title, "<script>") {
|
|
t.Errorf("Expected script tags to be HTML-escaped (<script>), got: %s", title)
|
|
}
|
|
if !strings.Contains(title, "alert(") {
|
|
t.Errorf("Expected JavaScript code to be present but escaped, got: %s", title)
|
|
}
|
|
|
|
content, ok := data["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected content string in XSS response, got %T", data["content"])
|
|
}
|
|
|
|
if strings.Contains(content, "<script>") {
|
|
t.Errorf("Expected script tags to be HTML-escaped in content, got: %s", content)
|
|
}
|
|
|
|
sqlResponse := makePostRequest(t, ctx.Router, "/api/posts", map[string]any{
|
|
"title": "'; DROP TABLE posts; --",
|
|
"url": "https://example.com/sql",
|
|
"content": "SQL injection test",
|
|
}, user, nil)
|
|
if sqlResponse.Code != http.StatusCreated {
|
|
t.Errorf("Expected status 201 for SQL injection sanitization, got %d", sqlResponse.Code)
|
|
}
|
|
|
|
sqlResult := assertJSONResponse(t, sqlResponse, http.StatusCreated)
|
|
if success, _ := sqlResult["success"].(bool); !success {
|
|
t.Error("Expected SQL response to have success=true")
|
|
}
|
|
|
|
sqlResponseData, ok := getDataFromResponse(sqlResult)
|
|
if !ok {
|
|
t.Fatalf("Expected data object in SQL response, got %T", sqlResult["data"])
|
|
}
|
|
|
|
sqlResponseTitle, ok := sqlResponseData["title"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected title string in SQL response, got %T", sqlResponseData["title"])
|
|
}
|
|
|
|
if strings.Contains(sqlResponseTitle, "'; DROP TABLE posts; --") {
|
|
t.Errorf("Expected SQL injection payload to be HTML-escaped in title, got: %s", sqlResponseTitle)
|
|
}
|
|
if !strings.Contains(sqlResponseTitle, "'") {
|
|
t.Errorf("Expected single quotes to be HTML-escaped ('), got: %s", sqlResponseTitle)
|
|
}
|
|
if !strings.Contains(sqlResponseTitle, "DROP TABLE") {
|
|
t.Errorf("Expected SQL commands to be present but escaped, got: %s", sqlResponseTitle)
|
|
}
|
|
|
|
sqlResponseContent, ok := sqlResponseData["content"].(string)
|
|
if !ok {
|
|
t.Fatalf("Expected content string in SQL response, got %T", sqlResponseData["content"])
|
|
}
|
|
|
|
if strings.Contains(sqlResponseContent, "'; DROP TABLE posts; --") {
|
|
t.Errorf("Expected SQL injection payload to be HTML-escaped in content, got: %s", sqlResponseContent)
|
|
}
|
|
})
|
|
|
|
t.Run("Authorization_User_Access_Control", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
firstUser := createAuthenticatedUser(t, authService, userRepo, "auth_user1", "auth1@example.com")
|
|
secondUser := createAuthenticatedUser(t, authService, userRepo, "auth_user2", "auth2@example.com")
|
|
|
|
post := testutils.CreatePostWithRepo(t, postRepo, firstUser.User.ID, "Private Post", "https://example.com/private")
|
|
|
|
getPostResponse := makeAuthenticatedGetRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d", post.ID), secondUser, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
testutils.AssertHTTPStatus(t, getPostResponse, http.StatusOK)
|
|
|
|
updateResponse := makePutRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d", post.ID), map[string]any{"title": "Updated Title"}, secondUser, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
testutils.AssertHTTPStatus(t, updateResponse, http.StatusForbidden)
|
|
|
|
deleteResponse := makeDeleteRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d", post.ID), secondUser, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
testutils.AssertHTTPStatus(t, deleteResponse, http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("Authorization_Vote_Access_Control", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
firstUser := createAuthenticatedUser(t, authService, userRepo, "vote_auth_user1", "vote_auth1@example.com")
|
|
secondUser := createAuthenticatedUser(t, authService, userRepo, "vote_auth_user2", "vote_auth2@example.com")
|
|
|
|
post := testutils.CreatePostWithRepo(t, postRepo, firstUser.User.ID, "Vote Auth Post", "https://example.com/vote-auth")
|
|
|
|
voteResponse := makePostRequest(t, ctx.Router, fmt.Sprintf("/api/posts/%d/vote", post.ID), map[string]any{"type": "up"}, secondUser, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
|
if voteResponse.Code != http.StatusOK {
|
|
t.Errorf("Users should be able to vote on any post, got %d", voteResponse.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Authorization_Token_Expiration", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
user := createAuthenticatedUser(t, authService, userRepo, "expire_auth_user", "expire_auth@example.com")
|
|
|
|
now := time.Now()
|
|
claims := services.TokenClaims{
|
|
UserID: user.User.ID,
|
|
Username: user.User.Username,
|
|
SessionVersion: user.User.SessionVersion,
|
|
TokenType: services.TokenTypeAccess,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: testutils.AppTestConfig.JWT.Issuer,
|
|
Audience: []string{testutils.AppTestConfig.JWT.Audience},
|
|
IssuedAt: jwt.NewNumericDate(now.Add(-25 * time.Hour)),
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(-1 * time.Hour)),
|
|
Subject: fmt.Sprint(user.User.ID),
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
expiredToken, err := token.SignedString([]byte(testutils.AppTestConfig.JWT.Secret))
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate expired token: %v", err)
|
|
}
|
|
|
|
meResponse := makeRequest(t, ctx.Router, "GET", "/api/auth/me", nil, map[string]string{"Authorization": "Bearer " + expiredToken})
|
|
testutils.AssertHTTPStatus(t, meResponse, http.StatusUnauthorized)
|
|
})
|
|
|
|
t.Run("Authorization_Token_Tampering", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
user := createAuthenticatedUser(t, authService, userRepo, "tamper_user", "tamper@example.com")
|
|
|
|
tamperedToken := user.Token[:len(user.Token)-5] + "XXXXX"
|
|
|
|
meRequest := httptest.NewRequest("GET", "/api/auth/me", nil)
|
|
meRequest.Header.Set("Authorization", "Bearer "+tamperedToken)
|
|
meResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(meResponse, meRequest)
|
|
testutils.AssertHTTPStatus(t, meResponse, http.StatusUnauthorized)
|
|
})
|
|
|
|
t.Run("Authorization_Session_Version_Mismatch", func(t *testing.T) {
|
|
emailSender.Reset()
|
|
user := createAuthenticatedUser(t, authService, userRepo, "session_user", "session@example.com")
|
|
|
|
now := time.Now()
|
|
claims := services.TokenClaims{
|
|
UserID: user.User.ID,
|
|
Username: user.User.Username,
|
|
SessionVersion: user.User.SessionVersion + 1,
|
|
TokenType: services.TokenTypeAccess,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: testutils.AppTestConfig.JWT.Issuer,
|
|
Audience: []string{testutils.AppTestConfig.JWT.Audience},
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
|
Subject: fmt.Sprint(user.User.ID),
|
|
},
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
invalidToken, err := token.SignedString([]byte(testutils.AppTestConfig.JWT.Secret))
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate invalid token: %v", err)
|
|
}
|
|
|
|
meRequest := httptest.NewRequest("GET", "/api/auth/me", nil)
|
|
meRequest.Header.Set("Authorization", "Bearer "+invalidToken)
|
|
meResponse := httptest.NewRecorder()
|
|
|
|
ctx.Router.ServeHTTP(meResponse, meRequest)
|
|
testutils.AssertHTTPStatus(t, meResponse, http.StatusUnauthorized)
|
|
})
|
|
|
|
}
|
|
|
|
func TestIntegration_DatabaseMonitoring(t *testing.T) {
|
|
db := testutils.NewTestDB(t)
|
|
defer func() {
|
|
sqlDB, _ := db.DB()
|
|
sqlDB.Close()
|
|
}()
|
|
|
|
monitor := middleware.NewInMemoryDBMonitor()
|
|
|
|
monitoringPlugin := database.NewGormDBMonitor(monitor)
|
|
if err := db.Use(monitoringPlugin); err != nil {
|
|
t.Fatalf("Failed to add monitoring plugin: %v", err)
|
|
}
|
|
|
|
userRepo := repositories.NewUserRepository(db)
|
|
postRepo := repositories.NewPostRepository(db)
|
|
voteRepo := repositories.NewVoteRepository(db)
|
|
deletionRepo := repositories.NewAccountDeletionRepository(db)
|
|
refreshTokenRepo := repositories.NewRefreshTokenRepository(db)
|
|
emailSender := &testutils.MockEmailSender{}
|
|
|
|
_, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, userRepo, postRepo, deletionRepo, refreshTokenRepo, emailSender)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create auth service: %v", err)
|
|
}
|
|
|
|
voteService := services.NewVoteService(voteRepo, postRepo, db)
|
|
apiHandler := handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, postRepo, userRepo, voteService, db, monitor)
|
|
|
|
t.Run("Health endpoint includes database monitoring", func(t *testing.T) {
|
|
user := &database.User{
|
|
Username: "monitoring_user",
|
|
Email: "monitoring@example.com",
|
|
Password: "password123",
|
|
EmailVerified: true,
|
|
}
|
|
userRepo.Create(user)
|
|
|
|
request := httptest.NewRequest("GET", "/health", nil)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
apiHandler.GetHealth(recorder, request)
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusOK)
|
|
|
|
var response map[string]any
|
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if response["success"] != true {
|
|
t.Error("Expected success to be true")
|
|
}
|
|
|
|
data, ok := getDataFromResponse(response)
|
|
if !ok {
|
|
t.Fatal("Expected data to be a map")
|
|
}
|
|
|
|
if pingTime, exists := data["ping_time"]; exists {
|
|
t.Logf("Database ping time: %v", pingTime)
|
|
}
|
|
|
|
if dbStats, exists := data["database_stats"]; exists {
|
|
t.Logf("Database stats present: %v", dbStats)
|
|
}
|
|
})
|
|
|
|
t.Run("Metrics endpoint includes database monitoring", func(t *testing.T) {
|
|
|
|
user := &database.User{
|
|
Username: "metrics_user",
|
|
Email: "metrics@example.com",
|
|
Password: "password123",
|
|
EmailVerified: true,
|
|
}
|
|
userRepo.Create(user)
|
|
|
|
post := &database.Post{
|
|
Title: "Test Post",
|
|
Content: "Test content",
|
|
URL: "https://example.com",
|
|
AuthorID: &user.ID,
|
|
}
|
|
postRepo.Create(post)
|
|
|
|
request := httptest.NewRequest("GET", "/metrics", nil)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
apiHandler.GetMetrics(recorder, request)
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusOK)
|
|
|
|
var response map[string]any
|
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if response["success"] != true {
|
|
t.Error("Expected success to be true")
|
|
}
|
|
|
|
data, ok := getDataFromResponse(response)
|
|
if !ok {
|
|
t.Fatal("Expected data to be a map")
|
|
}
|
|
|
|
if dbData, exists := data["database"]; exists {
|
|
t.Logf("Database monitoring data present: %v", dbData)
|
|
if dbMap, ok := dbData.(map[string]any); ok {
|
|
if totalQueries, exists := dbMap["total_queries"]; exists {
|
|
t.Logf("Total queries tracked: %v", totalQueries)
|
|
}
|
|
}
|
|
}
|
|
|
|
if perfData, exists := data["performance"]; exists {
|
|
t.Logf("Performance data present: %v", perfData)
|
|
}
|
|
})
|
|
|
|
t.Run("Database operations are tracked", func(t *testing.T) {
|
|
|
|
monitor = middleware.NewInMemoryDBMonitor()
|
|
monitoringPlugin = database.NewGormDBMonitor(monitor)
|
|
db.Use(monitoringPlugin)
|
|
|
|
apiHandler = handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, postRepo, userRepo, voteService, db, monitor)
|
|
|
|
user := &database.User{
|
|
Username: "tracking_user",
|
|
Email: "tracking@example.com",
|
|
Password: "password123",
|
|
EmailVerified: true,
|
|
}
|
|
userRepo.Create(user)
|
|
|
|
post := &database.Post{
|
|
Title: "Tracking Post",
|
|
Content: "Tracking content",
|
|
URL: "https://example.com",
|
|
AuthorID: &user.ID,
|
|
}
|
|
postRepo.Create(post)
|
|
|
|
stats := monitor.GetStats()
|
|
|
|
t.Logf("Database operations tracked: %d queries", stats.TotalQueries)
|
|
t.Logf("Slow queries: %d", stats.SlowQueries)
|
|
t.Logf("Average duration: %v", stats.AverageDuration)
|
|
t.Logf("Error count: %d", stats.ErrorCount)
|
|
|
|
if stats.TotalQueries > 0 {
|
|
t.Logf("✅ Database monitoring is working - tracked %d queries", stats.TotalQueries)
|
|
} else {
|
|
t.Logf("⚠️ Database monitoring plugin may not be tracking all operations (this is a known limitation)")
|
|
}
|
|
})
|
|
}
|