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": "",
"url": "https://example.com/xss",
"content": "",
}, 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, "