To gitea and beyond, let's go(-yco)
This commit is contained in:
163
internal/integration/caching_integration_test.go
Normal file
163
internal/integration/caching_integration_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"goyco/internal/handlers"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/server"
|
||||
"goyco/internal/services"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func setupCachingTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
|
||||
metadataService := services.NewURLMetadataService()
|
||||
|
||||
authHandler := handlers.NewAuthHandler(authService, suite.UserRepo)
|
||||
postHandler := handlers.NewPostHandler(suite.PostRepo, metadataService, voteService)
|
||||
voteHandler := handlers.NewVoteHandler(voteService)
|
||||
userHandler := handlers.NewUserHandler(suite.UserRepo, authService)
|
||||
apiHandler := handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService, suite.DB, middleware.NewInMemoryDBMonitor())
|
||||
|
||||
staticDir := t.TempDir()
|
||||
|
||||
router := server.NewRouter(server.RouterConfig{
|
||||
AuthHandler: authHandler,
|
||||
PostHandler: postHandler,
|
||||
VoteHandler: voteHandler,
|
||||
UserHandler: userHandler,
|
||||
APIHandler: apiHandler,
|
||||
AuthService: authService,
|
||||
PageHandler: nil,
|
||||
StaticDir: staticDir,
|
||||
Debug: false,
|
||||
DisableCache: false,
|
||||
DisableCompression: false,
|
||||
DBMonitor: middleware.NewInMemoryDBMonitor(),
|
||||
RateLimitConfig: testutils.AppTestConfig.RateLimit,
|
||||
})
|
||||
|
||||
return &testContext{
|
||||
Router: router,
|
||||
Suite: suite,
|
||||
AuthService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_Caching(t *testing.T) {
|
||||
ctx := setupCachingTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Cache_Hit_On_Repeated_Requests", func(t *testing.T) {
|
||||
req1 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
req2 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
if rec1.Code != rec2.Code {
|
||||
t.Error("Cached responses should have same status code")
|
||||
}
|
||||
|
||||
if rec1.Body.String() != rec2.Body.String() {
|
||||
t.Error("Cached responses should have same body")
|
||||
}
|
||||
|
||||
if rec2.Header().Get("X-Cache") != "HIT" {
|
||||
t.Log("Cache may not be enabled for this path or response may not be cacheable")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cache_Invalidation_On_POST", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "cache_post_user", "cache_post@example.com")
|
||||
|
||||
req1 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": "Cache Test Post",
|
||||
"url": "https://example.com/cache-test",
|
||||
"content": "Test content",
|
||||
}
|
||||
body, _ := json.Marshal(postBody)
|
||||
req2 := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req2 = testutils.WithUserContext(req2, middleware.UserIDKey, user.User.ID)
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
req3 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec3 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec3, req3)
|
||||
|
||||
if rec1.Body.String() == rec3.Body.String() && rec1.Code == http.StatusOK && rec3.Code == http.StatusOK {
|
||||
t.Log("Cache invalidation may not be working or cache may not be enabled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cache_Headers_Present", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("Cache-Control") == "" && rec.Header().Get("X-Cache") == "" {
|
||||
t.Log("Cache headers may not be present for all responses")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cache_Invalidation_On_DELETE", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "cache_delete_user", "cache_delete@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Cache Delete Post", "https://example.com/cache-delete")
|
||||
|
||||
req1 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
req2 := httptest.NewRequest("DELETE", "/api/posts/"+fmt.Sprintf("%d", post.ID), nil)
|
||||
req2.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req2 = testutils.WithUserContext(req2, middleware.UserIDKey, user.User.ID)
|
||||
req2 = testutils.WithURLParams(req2, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
req3 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec3 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec3, req3)
|
||||
|
||||
if rec1.Body.String() == rec3.Body.String() && rec1.Code == http.StatusOK && rec3.Code == http.StatusOK {
|
||||
t.Log("Cache invalidation may not be working or cache may not be enabled")
|
||||
}
|
||||
})
|
||||
}
|
||||
406
internal/integration/complete_api_endpoints_integration_test.go
Normal file
406
internal/integration/complete_api_endpoints_integration_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_CompleteAPIEndpoints(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("Auth_Logout_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "logout_user", "logout@example.com")
|
||||
|
||||
reqBody := map[string]string{}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/logout", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Auth_Revoke_Token_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "revoke_user", "revoke@example.com")
|
||||
|
||||
loginResult, err := ctx.AuthService.Login("revoke_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %v", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"refresh_token": loginResult.RefreshToken,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/revoke", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Auth_Revoke_All_Tokens_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "revoke_all_user", "revoke_all@example.com")
|
||||
|
||||
reqBody := map[string]string{}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/revoke-all", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Auth_Resend_Verification_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"email": "resend@example.com",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/resend-verification", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Auth_Confirm_Email_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "confirm_api_user", "confirm_api@example.com")
|
||||
|
||||
token := ctx.Suite.EmailSender.VerificationToken()
|
||||
if token == "" {
|
||||
token = "test-token"
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/auth/confirm?token="+url.QueryEscape(token), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("Auth_Update_Email_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "update_email_api_user", "update_email_api@example.com")
|
||||
|
||||
reqBody := map[string]string{
|
||||
"email": "newemail@example.com",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if email, ok := data["email"].(string); ok && email != "newemail@example.com" {
|
||||
t.Errorf("Expected email to be updated, got %s", email)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Auth_Update_Username_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "update_username_api_user", "update_username_api@example.com")
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username": "new_username",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", "/api/auth/username", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if username, ok := data["username"].(string); ok && username != "new_username" {
|
||||
t.Errorf("Expected username to be updated, got %s", username)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Users_List_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "users_list_user", "users_list@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/users", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["users"]; !exists {
|
||||
t.Error("Expected users in response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Users_Get_By_ID_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "users_get_user", "users_get@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/users/%d", user.User.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if userData, ok := data["user"].(map[string]any); ok {
|
||||
if id, ok := userData["id"].(float64); ok && uint(id) != user.User.ID {
|
||||
t.Errorf("Expected user ID %d, got %.0f", user.User.ID, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Users_Get_Posts_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "users_posts_user", "users_posts@example.com")
|
||||
|
||||
testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "User Posts Test", "https://example.com/user-posts")
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/users/%d/posts", user.User.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if posts, ok := data["posts"].([]any); ok {
|
||||
if len(posts) == 0 {
|
||||
t.Error("Expected at least one post in response")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected posts array in response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Users_Create_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "users_create_admin", "users_create_admin@example.com")
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username": "created_user",
|
||||
"email": "created@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/users", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusCreated)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["user"]; !exists {
|
||||
t.Error("Expected user in response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Posts_Update_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "posts_update_user", "posts_update@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Update Test Post", "https://example.com/update-test")
|
||||
|
||||
reqBody := map[string]string{
|
||||
"title": "Updated Title",
|
||||
"content": "Updated content",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%d", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if postData, ok := data["post"].(map[string]any); ok {
|
||||
if title, ok := postData["title"].(string); ok && title != "Updated Title" {
|
||||
t.Errorf("Expected title 'Updated Title', got '%s'", title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Posts_Delete_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "posts_delete_user", "posts_delete@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Delete Test Post", "https://example.com/delete-test")
|
||||
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
getReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getRec, getReq)
|
||||
assertStatus(t, getRec, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Votes_Get_All_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "votes_get_all_user", "votes_get_all@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Votes Test Post", "https://example.com/votes-test")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
voteBodyBytes, _ := json.Marshal(voteBody)
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(voteBodyBytes))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(voteRec, voteReq)
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/votes", post.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if votes, ok := data["votes"].([]any); ok {
|
||||
if len(votes) == 0 {
|
||||
t.Error("Expected at least one vote in response")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected votes array in response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Votes_Remove_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "votes_remove_user", "votes_remove@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Vote Remove Test", "https://example.com/vote-remove")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
voteBodyBytes, _ := json.Marshal(voteBody)
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(voteBodyBytes))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(voteRec, voteReq)
|
||||
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d/vote", post.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("API_Info_Endpoint", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["endpoints"]; !exists {
|
||||
t.Error("Expected endpoints in API info")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Swagger_Documentation_Endpoint", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/swagger/index.html", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_Compression(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Response_Compression_Gzip", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("Content-Encoding") == "gzip" {
|
||||
reader, err := gzip.NewReader(rec.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create gzip reader: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decompress: %v", err)
|
||||
}
|
||||
|
||||
if len(decompressed) == 0 {
|
||||
t.Error("Expected decompressed content")
|
||||
}
|
||||
} else {
|
||||
t.Log("Compression may not be applied to small responses")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Compression_Headers_Present", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("Vary") == "" {
|
||||
t.Log("Vary header may not always be present")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_StaticFiles(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Robots_Txt_Served", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/robots.txt", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
if !strings.Contains(rec.Body.String(), "User-agent") {
|
||||
t.Error("Expected robots.txt content")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Static_Files_Security_Headers", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/robots.txt", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("X-Content-Type-Options") == "" {
|
||||
t.Log("Security headers may not be applied to all static files")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_URLMetadata(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("URL_Metadata_Fetch_On_Post_Creation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "metadata_user", "metadata@example.com")
|
||||
|
||||
ctx.Suite.TitleFetcher.SetTitle("Fetched Title")
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": "Test Post",
|
||||
"url": "https://example.com/metadata-test",
|
||||
"content": "Test content",
|
||||
}
|
||||
body, _ := json.Marshal(postBody)
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusCreated)
|
||||
})
|
||||
|
||||
t.Run("URL_Metadata_Endpoint", func(t *testing.T) {
|
||||
ctx.Suite.TitleFetcher.SetTitle("Endpoint Title")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/posts/title?url=https://example.com/test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["title"]; !exists {
|
||||
t.Error("Expected title in metadata response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_CrossComponentAuthorization(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("Post_Owner_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
owner := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "post_owner", "post_owner@example.com")
|
||||
otherUser := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "post_other", "post_other@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, owner.User.ID, "Owner Post", "https://example.com/owner")
|
||||
|
||||
updateBody := map[string]string{
|
||||
"title": "Updated Title",
|
||||
"content": "Updated content",
|
||||
}
|
||||
body, _ := json.Marshal(updateBody)
|
||||
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%d", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+otherUser.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, otherUser.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusForbidden)
|
||||
|
||||
req = httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%d", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+owner.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, owner.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Post_Delete_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
owner := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "delete_owner", "delete_owner@example.com")
|
||||
otherUser := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "delete_other", "delete_other@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, owner.User.ID, "Delete Post", "https://example.com/delete")
|
||||
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+otherUser.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, otherUser.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusForbidden)
|
||||
|
||||
req = httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+owner.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, owner.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("User_Profile_Access_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user1 := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "profile_user1", "profile_user1@example.com")
|
||||
user2 := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "profile_user2", "profile_user2@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/users/%d", user1.User.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user2.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user2.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", user1.User.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if userData, ok := data["user"].(map[string]any); ok {
|
||||
if id, ok := userData["id"].(float64); ok && uint(id) != user1.User.ID {
|
||||
t.Errorf("Expected user ID %d, got %.0f", user1.User.ID, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User_Settings_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "settings_auth_user", "settings_auth@example.com")
|
||||
otherUser := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "settings_auth_other", "settings_auth_other@example.com")
|
||||
|
||||
updateBody := map[string]string{
|
||||
"email": "newemail@example.com",
|
||||
}
|
||||
body, _ := json.Marshal(updateBody)
|
||||
|
||||
req := httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+otherUser.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, otherUser.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if userData, ok := data["user"].(map[string]any); ok {
|
||||
if email, ok := userData["email"].(string); ok && email == "newemail@example.com" {
|
||||
if id, ok := userData["id"].(float64); ok && uint(id) != otherUser.User.ID {
|
||||
t.Error("Expected email update to affect the authenticated user, not another user")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateBody2 := map[string]string{
|
||||
"email": "anothernewemail@example.com",
|
||||
}
|
||||
body2, _ := json.Marshal(updateBody2)
|
||||
|
||||
req = httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(body2))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Vote_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "vote_auth_user", "vote_auth@example.com")
|
||||
postOwner := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "vote_auth_owner", "vote_auth_owner@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, postOwner.User.ID, "Vote Auth Post", "https://example.com/vote-auth")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
body, _ := json.Marshal(voteBody)
|
||||
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
req = httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Protected_Endpoint_Without_Auth", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Protected_Endpoint_With_Invalid_Token", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer invalid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("User_List_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "list_auth_user", "list_auth@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/users", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
req = httptest.NewRequest("GET", "/api/users", nil)
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Authorization", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "refresh_auth_user", "refresh_auth@example.com")
|
||||
|
||||
loginResult, err := ctx.AuthService.Login("refresh_auth_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %v", err)
|
||||
}
|
||||
|
||||
refreshBody := map[string]string{
|
||||
"refresh_token": loginResult.RefreshToken,
|
||||
}
|
||||
body, _ := json.Marshal(refreshBody)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/auth/refresh", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["access_token"]; !exists {
|
||||
t.Error("Expected access_token in refresh response")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected data field in refresh response")
|
||||
}
|
||||
|
||||
refreshBody = map[string]string{
|
||||
"refresh_token": "invalid-refresh-token",
|
||||
}
|
||||
body, _ = json.Marshal(refreshBody)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/auth/refresh", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
223
internal/integration/csrf_integration_test.go
Normal file
223
internal/integration/csrf_integration_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIntegration_CSRF_Protection(t *testing.T) {
|
||||
ctx := setupPageHandlerTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("CSRF_Blocks_Form_Without_Token", func(t *testing.T) {
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "testuser")
|
||||
reqBody.Set("email", "test@example.com")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
|
||||
req := httptest.NewRequest("POST", "/register", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("Expected status 403, got %d. Body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "Invalid CSRF token") {
|
||||
t.Error("Expected CSRF error message")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CSRF_Allows_Form_With_Valid_Token", func(t *testing.T) {
|
||||
getReq := httptest.NewRequest("GET", "/register", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
|
||||
cookies := getRec.Result().Cookies()
|
||||
var csrfCookie *http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "csrf_token" {
|
||||
csrfCookie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if csrfCookie == nil {
|
||||
t.Fatal("Expected CSRF cookie to be set")
|
||||
}
|
||||
|
||||
csrfToken := csrfCookie.Value
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "csrf_user")
|
||||
reqBody.Set("email", "csrf@example.com")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/register", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(csrfCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusForbidden {
|
||||
t.Error("Expected form submission with valid CSRF token to succeed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CSRF_Allows_API_Requests", func(t *testing.T) {
|
||||
reqBody := map[string]string{
|
||||
"username": "api_user",
|
||||
"email": "api@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusForbidden {
|
||||
t.Error("Expected API requests to bypass CSRF protection")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CSRF_Blocks_Mismatched_Token", func(t *testing.T) {
|
||||
getReq := httptest.NewRequest("GET", "/register", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
|
||||
cookies := getRec.Result().Cookies()
|
||||
var csrfCookie *http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "csrf_token" {
|
||||
csrfCookie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if csrfCookie == nil {
|
||||
t.Fatal("Expected CSRF cookie to be set")
|
||||
}
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "mismatch_user")
|
||||
reqBody.Set("email", "mismatch@example.com")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
reqBody.Set("csrf_token", "wrong-token")
|
||||
|
||||
req := httptest.NewRequest("POST", "/register", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(csrfCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("Expected status 403, got %d. Body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "Invalid CSRF token") {
|
||||
t.Error("Expected CSRF error message")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CSRF_Allows_GET_Requests", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/register", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusForbidden {
|
||||
t.Error("Expected GET requests to bypass CSRF protection")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CSRF_Token_In_Header", func(t *testing.T) {
|
||||
getReq := httptest.NewRequest("GET", "/register", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
|
||||
cookies := getRec.Result().Cookies()
|
||||
var csrfCookie *http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "csrf_token" {
|
||||
csrfCookie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if csrfCookie == nil {
|
||||
t.Fatal("Expected CSRF cookie to be set")
|
||||
}
|
||||
|
||||
csrfToken := csrfCookie.Value
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "header_user")
|
||||
reqBody.Set("email", "header@example.com")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
|
||||
req := httptest.NewRequest("POST", "/register", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-CSRF-Token", csrfToken)
|
||||
req.AddCookie(csrfCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusForbidden {
|
||||
t.Error("Expected CSRF token in header to be accepted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CSRF_With_PageHandler_Forms", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "csrf_form_user", "csrf_form@example.com")
|
||||
|
||||
getReq := httptest.NewRequest("GET", "/posts/new", nil)
|
||||
getReq.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
|
||||
cookies := getRec.Result().Cookies()
|
||||
var csrfCookie *http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "csrf_token" {
|
||||
csrfCookie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if csrfCookie == nil {
|
||||
t.Fatal("Expected CSRF cookie to be set")
|
||||
}
|
||||
|
||||
csrfToken := csrfCookie.Value
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("title", "CSRF Test Post")
|
||||
reqBody.Set("url", "https://example.com/csrf-test")
|
||||
reqBody.Set("content", "Test content")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/posts", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
req.AddCookie(csrfCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusForbidden {
|
||||
t.Error("Expected post creation with valid CSRF token to succeed")
|
||||
}
|
||||
})
|
||||
}
|
||||
346
internal/integration/data_consistency_integration_test.go
Normal file
346
internal/integration/data_consistency_integration_test.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_DataConsistency(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("Post_Creation_Consistency", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "consistency_user", "consistency@example.com")
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": "Consistency Test Post",
|
||||
"url": "https://example.com/consistency",
|
||||
"content": "Test content",
|
||||
}
|
||||
body, _ := json.Marshal(postBody)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
createResponse := assertJSONResponse(t, rec, http.StatusCreated)
|
||||
if createResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
postData, ok := createResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Response missing data")
|
||||
}
|
||||
|
||||
postID, ok := postData["id"].(float64)
|
||||
if !ok {
|
||||
t.Fatal("Response missing post id")
|
||||
}
|
||||
|
||||
createdTitle := postData["title"]
|
||||
createdURL := postData["url"]
|
||||
createdContent := postData["content"]
|
||||
|
||||
getReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%.0f", postID), nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getRec, getReq)
|
||||
|
||||
getResponse := assertJSONResponse(t, getRec, http.StatusOK)
|
||||
if getResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
getPostData, ok := getResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Get response missing data")
|
||||
}
|
||||
|
||||
if getPostData["title"] != createdTitle {
|
||||
t.Errorf("Title mismatch: created=%v, retrieved=%v", createdTitle, getPostData["title"])
|
||||
}
|
||||
|
||||
if getPostData["url"] != createdURL {
|
||||
t.Errorf("URL mismatch: created=%v, retrieved=%v", createdURL, getPostData["url"])
|
||||
}
|
||||
|
||||
if getPostData["content"] != createdContent {
|
||||
t.Errorf("Content mismatch: created=%v, retrieved=%v", createdContent, getPostData["content"])
|
||||
}
|
||||
|
||||
if getPostData["author_id"] == nil {
|
||||
t.Error("Expected author_id to be set")
|
||||
} else if authorID, ok := getPostData["author_id"].(float64); ok {
|
||||
if uint(authorID) != user.User.ID {
|
||||
t.Errorf("Author ID mismatch: expected=%d, got=%.0f", user.User.ID, authorID)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Author ID type mismatch: expected float64, got %T", getPostData["author_id"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Vote_Consistency", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "vote_consistency_user", "vote_consistency@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Vote Consistency Post", "https://example.com/vote-consistency")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
body, _ := json.Marshal(voteBody)
|
||||
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(voteRec, voteReq)
|
||||
|
||||
assertStatus(t, voteRec, http.StatusOK)
|
||||
|
||||
getVotesReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/votes", post.ID), nil)
|
||||
getVotesReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getVotesReq = testutils.WithUserContext(getVotesReq, middleware.UserIDKey, user.User.ID)
|
||||
getVotesReq = testutils.WithURLParams(getVotesReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getVotesRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getVotesRec, getVotesReq)
|
||||
|
||||
votesResponse := assertJSONResponse(t, getVotesRec, http.StatusOK)
|
||||
if votesResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
votesData, ok := votesResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Votes response missing data")
|
||||
}
|
||||
|
||||
votes, ok := votesData["votes"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("Votes response missing votes array")
|
||||
}
|
||||
|
||||
if len(votes) == 0 {
|
||||
t.Error("Expected at least one vote")
|
||||
}
|
||||
|
||||
foundUserVote := false
|
||||
for _, vote := range votes {
|
||||
if voteMap, ok := vote.(map[string]any); ok {
|
||||
var userIDVal any
|
||||
var exists bool
|
||||
if userIDVal, exists = voteMap["user_id"]; !exists {
|
||||
userIDVal, exists = voteMap["UserID"]
|
||||
}
|
||||
if exists && userIDVal != nil {
|
||||
if userID, ok := userIDVal.(float64); ok && uint(userID) == user.User.ID {
|
||||
var voteType string
|
||||
if vt, ok := voteMap["type"].(string); ok {
|
||||
voteType = vt
|
||||
} else if vt, ok := voteMap["Type"].(string); ok {
|
||||
voteType = vt
|
||||
}
|
||||
if voteType != "" && voteType != "up" {
|
||||
t.Errorf("Expected vote type 'up', got '%s'", voteType)
|
||||
}
|
||||
foundUserVote = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundUserVote {
|
||||
t.Error("User vote not found in votes list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Post_Update_Consistency", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "update_consistency_user", "update_consistency@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Original Title", "https://example.com/original")
|
||||
|
||||
updateBody := map[string]string{
|
||||
"title": "Updated Title",
|
||||
"content": "Updated content",
|
||||
}
|
||||
body, _ := json.Marshal(updateBody)
|
||||
|
||||
updateReq := httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%d", post.ID), bytes.NewBuffer(body))
|
||||
updateReq.Header.Set("Content-Type", "application/json")
|
||||
updateReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
updateReq = testutils.WithUserContext(updateReq, middleware.UserIDKey, user.User.ID)
|
||||
updateReq = testutils.WithURLParams(updateReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
updateRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(updateRec, updateReq)
|
||||
|
||||
assertStatus(t, updateRec, http.StatusOK)
|
||||
|
||||
getReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getRec, getReq)
|
||||
|
||||
getResponse := assertJSONResponse(t, getRec, http.StatusOK)
|
||||
if getResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
getPostData, ok := getResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Get response missing data")
|
||||
}
|
||||
|
||||
if getPostData["title"] != "Updated Title" {
|
||||
t.Errorf("Title not updated: expected 'Updated Title', got %v", getPostData["title"])
|
||||
}
|
||||
|
||||
if getPostData["content"] != "Updated content" {
|
||||
t.Errorf("Content not updated: expected 'Updated content', got %v", getPostData["content"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User_Posts_Consistency", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "user_posts_consistency", "user_posts_consistency@example.com")
|
||||
|
||||
post1 := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Post 1", "https://example.com/post1")
|
||||
post2 := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Post 2", "https://example.com/post2")
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/users/%d/posts", user.User.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, ok := response["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Response missing data")
|
||||
}
|
||||
|
||||
posts, ok := data["posts"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("Response missing posts array")
|
||||
}
|
||||
|
||||
if len(posts) < 2 {
|
||||
t.Errorf("Expected at least 2 posts, got %d", len(posts))
|
||||
}
|
||||
|
||||
foundPost1 := false
|
||||
foundPost2 := false
|
||||
for _, post := range posts {
|
||||
if postMap, ok := post.(map[string]any); ok {
|
||||
if postID, ok := postMap["id"].(float64); ok {
|
||||
if uint(postID) == post1.ID {
|
||||
foundPost1 = true
|
||||
}
|
||||
if uint(postID) == post2.ID {
|
||||
foundPost2 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundPost1 {
|
||||
t.Error("Post 1 not found in user posts")
|
||||
}
|
||||
|
||||
if !foundPost2 {
|
||||
t.Error("Post 2 not found in user posts")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Post_Deletion_Consistency", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "delete_consistency_user", "delete_consistency@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Delete Consistency Post", "https://example.com/delete-consistency")
|
||||
|
||||
deleteReq := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
deleteReq = testutils.WithUserContext(deleteReq, middleware.UserIDKey, user.User.ID)
|
||||
deleteReq = testutils.WithURLParams(deleteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
deleteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(deleteRec, deleteReq)
|
||||
|
||||
assertStatus(t, deleteRec, http.StatusOK)
|
||||
|
||||
getReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getRec, getReq)
|
||||
|
||||
assertStatus(t, getRec, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Vote_Removal_Consistency", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "vote_remove_consistency", "vote_remove_consistency@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Vote Remove Consistency", "https://example.com/vote-remove-consistency")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
body, _ := json.Marshal(voteBody)
|
||||
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(voteRec, voteReq)
|
||||
|
||||
assertStatus(t, voteRec, http.StatusOK)
|
||||
|
||||
removeVoteReq := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d/vote", post.ID), nil)
|
||||
removeVoteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
removeVoteReq = testutils.WithUserContext(removeVoteReq, middleware.UserIDKey, user.User.ID)
|
||||
removeVoteReq = testutils.WithURLParams(removeVoteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
removeVoteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(removeVoteRec, removeVoteReq)
|
||||
|
||||
assertStatus(t, removeVoteRec, http.StatusOK)
|
||||
|
||||
getVotesReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/votes", post.ID), nil)
|
||||
getVotesReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getVotesReq = testutils.WithUserContext(getVotesReq, middleware.UserIDKey, user.User.ID)
|
||||
getVotesReq = testutils.WithURLParams(getVotesReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getVotesRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getVotesRec, getVotesReq)
|
||||
|
||||
votesResponse := assertJSONResponse(t, getVotesRec, http.StatusOK)
|
||||
if votesResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if data, ok := votesResponse["data"].(map[string]any); ok {
|
||||
if votes, ok := data["votes"].([]any); ok {
|
||||
for _, vote := range votes {
|
||||
if voteMap, ok := vote.(map[string]any); ok {
|
||||
if userID, ok := voteMap["user_id"].(float64); ok && uint(userID) == user.User.ID {
|
||||
t.Error("User vote still exists after removal")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
201
internal/integration/edge_cases_integration_test.go
Normal file
201
internal/integration/edge_cases_integration_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_EdgeCases(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("Expired_Token_Handling", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "expired_user", "expired@example.com")
|
||||
|
||||
expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDAwMDAwMDB9.expired"
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+expiredToken)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Concurrent_Vote_Operations", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user1 := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "vote_user1", "vote1@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user1.User.ID, "Concurrent Vote Post", "https://example.com/concurrent")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 10)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
body, _ := json.Marshal(voteBody)
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user1.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user1.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
errors <- fmt.Errorf("unexpected status: %d", rec.Code)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
for err := range errors {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Large_Payload_Handling", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "large_user", "large@example.com")
|
||||
|
||||
largeContent := make([]byte, 10001)
|
||||
for i := range largeContent {
|
||||
largeContent[i] = 'a'
|
||||
}
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": "Large Post",
|
||||
"url": "https://example.com/large",
|
||||
"content": string(largeContent),
|
||||
}
|
||||
body, _ := json.Marshal(postBody)
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
|
||||
smallContent := make([]byte, 1000)
|
||||
for i := range smallContent {
|
||||
smallContent[i] = 'a'
|
||||
}
|
||||
|
||||
postBody2 := map[string]string{
|
||||
"title": "Small Post",
|
||||
"url": "https://example.com/small",
|
||||
"content": string(smallContent),
|
||||
}
|
||||
body2, _ := json.Marshal(postBody2)
|
||||
req2 := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body2))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req2 = testutils.WithUserContext(req2, middleware.UserIDKey, user.User.ID)
|
||||
rec2 := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec2, req2)
|
||||
|
||||
assertStatus(t, rec2, http.StatusCreated)
|
||||
})
|
||||
|
||||
t.Run("Malformed_JSON_Payloads", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "malformed_user", "malformed@example.com")
|
||||
|
||||
malformedPayloads := []string{
|
||||
`{"title": "test"`,
|
||||
`{"title": "test",}`,
|
||||
`{title: "test"}`,
|
||||
`{"title": 'test'}`,
|
||||
`{"title": "test" "url": ""}`,
|
||||
}
|
||||
|
||||
for _, payload := range malformedPayloads {
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBufferString(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Race_Condition_Vote_Removal", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "race_user", "race@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Race Post", "https://example.com/race")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
body, _ := json.Marshal(voteBody)
|
||||
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(voteRec, voteReq)
|
||||
assertStatus(t, voteRec, http.StatusOK)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 3; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d/vote", post.ID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
getVotesReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/votes", post.ID), nil)
|
||||
getVotesReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getVotesReq = testutils.WithUserContext(getVotesReq, middleware.UserIDKey, user.User.ID)
|
||||
getVotesReq = testutils.WithURLParams(getVotesReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getVotesRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getVotesRec, getVotesReq)
|
||||
|
||||
votesResponse := assertJSONResponse(t, getVotesRec, http.StatusOK)
|
||||
if votesResponse != nil {
|
||||
if data, ok := votesResponse["data"].(map[string]any); ok {
|
||||
if votes, ok := data["votes"].([]any); ok {
|
||||
userVoteCount := 0
|
||||
for _, vote := range votes {
|
||||
if voteMap, ok := vote.(map[string]any); ok {
|
||||
if userID, ok := voteMap["user_id"].(float64); ok && uint(userID) == user.User.ID {
|
||||
userVoteCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
if userVoteCount > 1 {
|
||||
t.Errorf("Expected at most 1 vote from user, got %d", userVoteCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
139
internal/integration/email_integration_test.go
Normal file
139
internal/integration/email_integration_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_EmailService(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Registration_Email_Sent", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username": "email_reg_user",
|
||||
"email": "email_reg@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusCreated)
|
||||
|
||||
token := ctx.Suite.EmailSender.VerificationToken()
|
||||
if token == "" {
|
||||
t.Error("Expected verification email to be sent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PasswordReset_Email_Sent", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "email_reset_user",
|
||||
Email: "email_reset@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := ctx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username_or_email": "email_reset_user",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/forgot-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
token := ctx.Suite.EmailSender.PasswordResetToken()
|
||||
if token == "" {
|
||||
t.Error("Expected password reset email to be sent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AccountDeletion_Email_Sent", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "email_del_user", "email_del@example.com")
|
||||
|
||||
reqBody := map[string]string{}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("DELETE", "/api/auth/account", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
token := ctx.Suite.EmailSender.DeletionToken()
|
||||
if token == "" {
|
||||
t.Error("Expected account deletion email to be sent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmailChange_Verification_Sent", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "email_change_user", "email_change@example.com")
|
||||
|
||||
reqBody := map[string]string{
|
||||
"email": "newemail@example.com",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", "/api/auth/email", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
token := ctx.Suite.EmailSender.VerificationToken()
|
||||
if token == "" {
|
||||
t.Error("Expected email change verification to be sent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Email_Template_Content", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username": "template_user",
|
||||
"email": "template@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
token := ctx.Suite.EmailSender.VerificationToken()
|
||||
if token == "" {
|
||||
t.Fatal("Expected verification token")
|
||||
}
|
||||
|
||||
if len(token) < 10 {
|
||||
t.Error("Expected token to have reasonable format")
|
||||
}
|
||||
})
|
||||
}
|
||||
356
internal/integration/end_to_end_journeys_integration_test.go
Normal file
356
internal/integration/end_to_end_journeys_integration_test.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_EndToEndUserJourneys(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("Complete_Registration_To_Post_Creation_Journey", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
registerBody := map[string]string{
|
||||
"username": "journey_user",
|
||||
"email": "journey@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(registerBody)
|
||||
registerReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
registerReq.Header.Set("Content-Type", "application/json")
|
||||
registerRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(registerRec, registerReq)
|
||||
|
||||
assertStatus(t, registerRec, http.StatusCreated)
|
||||
|
||||
verificationToken := ctx.Suite.EmailSender.VerificationToken()
|
||||
if verificationToken == "" {
|
||||
t.Fatal("Verification token not sent")
|
||||
}
|
||||
|
||||
confirmReq := httptest.NewRequest("GET", "/api/auth/confirm?token="+url.QueryEscape(verificationToken), nil)
|
||||
confirmRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(confirmRec, confirmReq)
|
||||
|
||||
assertStatus(t, confirmRec, http.StatusOK)
|
||||
|
||||
loginBody := map[string]string{
|
||||
"username": "journey_user",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
loginBodyBytes, _ := json.Marshal(loginBody)
|
||||
loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(loginBodyBytes))
|
||||
loginReq.Header.Set("Content-Type", "application/json")
|
||||
loginRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(loginRec, loginReq)
|
||||
|
||||
loginResponse := assertJSONResponse(t, loginRec, http.StatusOK)
|
||||
if loginResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, ok := loginResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Login response missing data")
|
||||
}
|
||||
|
||||
var token string
|
||||
if accessToken, ok := data["access_token"].(string); ok && accessToken != "" {
|
||||
token = accessToken
|
||||
} else if tokenVal, ok := data["token"].(string); ok && tokenVal != "" {
|
||||
token = tokenVal
|
||||
} else {
|
||||
t.Fatal("Login response missing access_token or token")
|
||||
}
|
||||
|
||||
var userID uint
|
||||
if userData, ok := data["user"].(map[string]any); ok {
|
||||
if id, ok := userData["id"].(float64); ok {
|
||||
userID = uint(id)
|
||||
} else if id, ok := userData["ID"].(float64); ok {
|
||||
userID = uint(id)
|
||||
}
|
||||
}
|
||||
if userID == 0 {
|
||||
if id, ok := data["user_id"].(float64); ok {
|
||||
userID = uint(id)
|
||||
}
|
||||
}
|
||||
if userID == 0 {
|
||||
t.Fatalf("Login response missing user.id. Data: %+v", data)
|
||||
}
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": "Journey Test Post",
|
||||
"url": "https://example.com/journey",
|
||||
"content": "Test content",
|
||||
}
|
||||
postBodyBytes, _ := json.Marshal(postBody)
|
||||
postReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(postBodyBytes))
|
||||
postReq.Header.Set("Content-Type", "application/json")
|
||||
postReq.Header.Set("Authorization", "Bearer "+token)
|
||||
postReq = testutils.WithUserContext(postReq, middleware.UserIDKey, uint(userID))
|
||||
postRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(postRec, postReq)
|
||||
|
||||
postResponse := assertJSONResponse(t, postRec, http.StatusCreated)
|
||||
if postResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
postData, ok := postResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Post response missing data")
|
||||
}
|
||||
|
||||
postID, ok := postData["id"].(float64)
|
||||
if !ok {
|
||||
t.Fatal("Post response missing id")
|
||||
}
|
||||
|
||||
getPostReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%.0f", postID), nil)
|
||||
getPostRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getPostRec, getPostReq)
|
||||
|
||||
assertStatus(t, getPostRec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Complete_Password_Reset_Journey", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "reset_journey_user", "reset_journey@example.com")
|
||||
|
||||
resetBody := map[string]string{
|
||||
"username_or_email": "reset_journey@example.com",
|
||||
}
|
||||
resetBodyBytes, _ := json.Marshal(resetBody)
|
||||
resetReq := httptest.NewRequest("POST", "/api/auth/forgot-password", bytes.NewBuffer(resetBodyBytes))
|
||||
resetReq.Header.Set("Content-Type", "application/json")
|
||||
resetRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(resetRec, resetReq)
|
||||
|
||||
assertStatus(t, resetRec, http.StatusOK)
|
||||
|
||||
resetToken := ctx.Suite.EmailSender.GetLastPasswordResetToken()
|
||||
if resetToken == "" {
|
||||
t.Fatal("Password reset token not sent")
|
||||
}
|
||||
|
||||
newPasswordBody := map[string]string{
|
||||
"token": resetToken,
|
||||
"new_password": "NewSecurePass123!",
|
||||
}
|
||||
newPasswordBodyBytes, _ := json.Marshal(newPasswordBody)
|
||||
newPasswordReq := httptest.NewRequest("POST", "/api/auth/reset-password", bytes.NewBuffer(newPasswordBodyBytes))
|
||||
newPasswordReq.Header.Set("Content-Type", "application/json")
|
||||
newPasswordRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(newPasswordRec, newPasswordReq)
|
||||
|
||||
assertStatus(t, newPasswordRec, http.StatusOK)
|
||||
|
||||
loginBody := map[string]string{
|
||||
"username": "reset_journey_user",
|
||||
"password": "NewSecurePass123!",
|
||||
}
|
||||
loginBodyBytes, _ := json.Marshal(loginBody)
|
||||
loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(loginBodyBytes))
|
||||
loginReq.Header.Set("Content-Type", "application/json")
|
||||
loginRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(loginRec, loginReq)
|
||||
|
||||
assertStatus(t, loginRec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Complete_Vote_And_Unvote_Journey", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "vote_journey_user", "vote_journey@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Vote Journey Post", "https://example.com/vote-journey")
|
||||
|
||||
voteBody := map[string]string{"type": "up"}
|
||||
voteBodyBytes, _ := json.Marshal(voteBody)
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(voteBodyBytes))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(voteRec, voteReq)
|
||||
|
||||
assertStatus(t, voteRec, http.StatusOK)
|
||||
|
||||
getVotesReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/votes", post.ID), nil)
|
||||
getVotesReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getVotesReq = testutils.WithUserContext(getVotesReq, middleware.UserIDKey, user.User.ID)
|
||||
getVotesReq = testutils.WithURLParams(getVotesReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getVotesRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getVotesRec, getVotesReq)
|
||||
|
||||
votesResponse := assertJSONResponse(t, getVotesRec, http.StatusOK)
|
||||
if votesResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if data, ok := votesResponse["data"].(map[string]any); ok {
|
||||
if votes, ok := data["votes"].([]any); ok && len(votes) > 0 {
|
||||
unvoteReq := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d/vote", post.ID), nil)
|
||||
unvoteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
unvoteReq = testutils.WithUserContext(unvoteReq, middleware.UserIDKey, user.User.ID)
|
||||
unvoteReq = testutils.WithURLParams(unvoteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
unvoteRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(unvoteRec, unvoteReq)
|
||||
|
||||
assertStatus(t, unvoteRec, http.StatusOK)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Complete_Page_Handler_Registration_Journey", func(t *testing.T) {
|
||||
pageCtx := setupPageHandlerTestContext(t)
|
||||
pageRouter := pageCtx.Router
|
||||
pageCtx.Suite.EmailSender.Reset()
|
||||
|
||||
csrfToken := getCSRFToken(t, pageRouter, "/register")
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "page_journey_user")
|
||||
reqBody.Set("email", "page_journey@example.com")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
reqBody.Set("password_confirm", "SecurePass123!")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/register", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
pageRouter.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
|
||||
verificationToken := pageCtx.Suite.EmailSender.VerificationToken()
|
||||
if verificationToken == "" {
|
||||
t.Fatal("Verification token not sent")
|
||||
}
|
||||
|
||||
confirmReq := httptest.NewRequest("GET", "/confirm?token="+url.QueryEscape(verificationToken), nil)
|
||||
confirmRec := httptest.NewRecorder()
|
||||
pageRouter.ServeHTTP(confirmRec, confirmReq)
|
||||
|
||||
assertStatusRange(t, confirmRec, http.StatusOK, http.StatusSeeOther)
|
||||
|
||||
loginCSRFToken := getCSRFToken(t, pageRouter, "/login")
|
||||
|
||||
loginBody := url.Values{}
|
||||
loginBody.Set("username", "page_journey_user")
|
||||
loginBody.Set("password", "SecurePass123!")
|
||||
loginBody.Set("csrf_token", loginCSRFToken)
|
||||
|
||||
loginReq := httptest.NewRequest("POST", "/login", strings.NewReader(loginBody.Encode()))
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.AddCookie(&http.Cookie{Name: "csrf_token", Value: loginCSRFToken})
|
||||
loginRec := httptest.NewRecorder()
|
||||
pageRouter.ServeHTTP(loginRec, loginReq)
|
||||
|
||||
assertStatus(t, loginRec, http.StatusSeeOther)
|
||||
|
||||
loginCookies := loginRec.Result().Cookies()
|
||||
var authToken string
|
||||
for _, cookie := range loginCookies {
|
||||
if cookie.Name == "auth_token" {
|
||||
authToken = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if authToken == "" {
|
||||
t.Fatal("Auth token not set after login")
|
||||
}
|
||||
|
||||
homeReq := httptest.NewRequest("GET", "/", nil)
|
||||
homeReq.AddCookie(&http.Cookie{Name: "auth_token", Value: authToken})
|
||||
homeRec := httptest.NewRecorder()
|
||||
pageRouter.ServeHTTP(homeRec, homeReq)
|
||||
|
||||
assertStatus(t, homeRec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Complete_Post_Creation_And_Update_Journey", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "post_update_journey_user", "post_update_journey@example.com")
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": "Original Title",
|
||||
"url": "https://example.com/original",
|
||||
"content": "Original content",
|
||||
}
|
||||
postBodyBytes, _ := json.Marshal(postBody)
|
||||
postReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(postBodyBytes))
|
||||
postReq.Header.Set("Content-Type", "application/json")
|
||||
postReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
postReq = testutils.WithUserContext(postReq, middleware.UserIDKey, user.User.ID)
|
||||
postRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(postRec, postReq)
|
||||
|
||||
postResponse := assertJSONResponse(t, postRec, http.StatusCreated)
|
||||
if postResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
postData, ok := postResponse["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Post response missing data")
|
||||
}
|
||||
|
||||
postID, ok := postData["id"].(float64)
|
||||
if !ok {
|
||||
t.Fatal("Post response missing id")
|
||||
}
|
||||
|
||||
updateBody := map[string]string{
|
||||
"title": "Updated Title",
|
||||
"content": "Updated content",
|
||||
}
|
||||
updateBodyBytes, _ := json.Marshal(updateBody)
|
||||
updateReq := httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%.0f", postID), bytes.NewBuffer(updateBodyBytes))
|
||||
updateReq.Header.Set("Content-Type", "application/json")
|
||||
updateReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
updateReq = testutils.WithUserContext(updateReq, middleware.UserIDKey, user.User.ID)
|
||||
updateReq = testutils.WithURLParams(updateReq, map[string]string{"id": fmt.Sprintf("%.0f", postID)})
|
||||
updateRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(updateRec, updateReq)
|
||||
|
||||
updateResponse := assertJSONResponse(t, updateRec, http.StatusOK)
|
||||
if updateResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
getPostReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%.0f", postID), nil)
|
||||
getPostRec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(getPostRec, getPostReq)
|
||||
|
||||
getPostResponse := assertJSONResponse(t, getPostRec, http.StatusOK)
|
||||
if getPostResponse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if data, ok := getPostResponse["data"].(map[string]any); ok {
|
||||
if post, ok := data["post"].(map[string]any); ok {
|
||||
if title, ok := post["title"].(string); ok && title != "Updated Title" {
|
||||
t.Errorf("Post title not updated: expected 'Updated Title', got '%s'", title)
|
||||
}
|
||||
if content, ok := post["content"].(string); ok && content != "Updated content" {
|
||||
t.Errorf("Post content not updated: expected 'Updated content', got '%s'", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
193
internal/integration/error_propagation_integration_test.go
Normal file
193
internal/integration/error_propagation_integration_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_ErrorPropagation(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
|
||||
t.Run("Invalid_JSON_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "json_error_user", "json_error@example.com")
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer([]byte("invalid json{")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("Validation_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username": "",
|
||||
"email": "invalid-email",
|
||||
"password": "weak",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("Database_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "db_error_user", "db_error@example.com")
|
||||
|
||||
reqBody := map[string]string{
|
||||
"title": "Test Post",
|
||||
"url": "https://example.com/test",
|
||||
"content": "Test content",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusInternalServerError {
|
||||
assertErrorResponse(t, rec, http.StatusInternalServerError)
|
||||
} else {
|
||||
assertStatus(t, rec, http.StatusCreated)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NotFound_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "notfound_error_user", "notfound_error@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/posts/999999", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": "999999"})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("Unauthorized_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"title": "Test Post",
|
||||
"url": "https://example.com/test",
|
||||
"content": "Test content",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Forbidden_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
owner := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "forbidden_owner", "forbidden_owner@example.com")
|
||||
otherUser := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "forbidden_other", "forbidden_other@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, owner.User.ID, "Forbidden Post", "https://example.com/forbidden")
|
||||
|
||||
updateBody := map[string]string{
|
||||
"title": "Updated Title",
|
||||
"content": "Updated content",
|
||||
}
|
||||
body, _ := json.Marshal(updateBody)
|
||||
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%d", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+otherUser.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, otherUser.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("Service_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username": "existing_user",
|
||||
"email": "existing@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusCreated)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusBadRequest, http.StatusConflict)
|
||||
assertErrorResponse(t, rec, rec.Code)
|
||||
})
|
||||
|
||||
t.Run("Middleware_Error_Propagation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer expired.invalid.token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Handler_Error_Response_Format", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/nonexistent", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
ctx.Router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusNotFound {
|
||||
if rec.Header().Get("Content-Type") == "application/json" {
|
||||
assertErrorResponse(t, rec, http.StatusNotFound)
|
||||
} else {
|
||||
if rec.Body.Len() == 0 {
|
||||
t.Error("Expected error response body")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
884
internal/integration/handlers_integration_test.go
Normal file
884
internal/integration/handlers_integration_test.go
Normal file
@@ -0,0 +1,884 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/handlers"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/repositories"
|
||||
"goyco/internal/services"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_Handlers(t *testing.T) {
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
|
||||
emailSender := suite.EmailSender
|
||||
userRepo := suite.UserRepo
|
||||
postRepo := suite.PostRepo
|
||||
titleFetcher := suite.TitleFetcher
|
||||
|
||||
authHandler := handlers.NewAuthHandler(authService, userRepo)
|
||||
postHandler := handlers.NewPostHandler(postRepo, titleFetcher, voteService)
|
||||
voteHandler := handlers.NewVoteHandler(voteService)
|
||||
userHandler := handlers.NewUserHandler(userRepo, authService)
|
||||
|
||||
t.Run("Auth_Handler_Complete_Workflow", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
registerData := map[string]string{
|
||||
"username": "handler_user",
|
||||
"email": "handler@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
registerBody, _ := json.Marshal(registerData)
|
||||
registerReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(registerBody))
|
||||
registerReq.Header.Set("Content-Type", "application/json")
|
||||
registerResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Register(registerResp, registerReq)
|
||||
if registerResp.Code != http.StatusCreated {
|
||||
t.Errorf("Expected status 201, got %d", registerResp.Code)
|
||||
}
|
||||
|
||||
var registerPayload map[string]any
|
||||
if err := json.Unmarshal(registerResp.Body.Bytes(), ®isterPayload); err != nil {
|
||||
t.Fatalf("Failed to decode register response: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
confirmReq := httptest.NewRequest(http.MethodGet, "/api/auth/confirm?token="+url.QueryEscape(mockToken), nil)
|
||||
confirmResp := httptest.NewRecorder()
|
||||
authHandler.ConfirmEmail(confirmResp, confirmReq)
|
||||
if confirmResp.Code != http.StatusOK {
|
||||
t.Fatalf("Expected 200 when confirming email via handler, got %d", confirmResp.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)
|
||||
}
|
||||
|
||||
meReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
meReq.Header.Set("Authorization", "Bearer "+loginAuth.AccessToken)
|
||||
meReq = testutils.WithUserContext(meReq, middleware.UserIDKey, loginSeed.User.ID)
|
||||
meResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(meResp, meReq)
|
||||
if meResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", meResp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Auth_Handler_Security_Validation", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
weakData := map[string]string{
|
||||
"username": "weak_user",
|
||||
"email": "weak@example.com",
|
||||
"password": "123",
|
||||
}
|
||||
weakBody, _ := json.Marshal(weakData)
|
||||
weakReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(weakBody))
|
||||
weakReq.Header.Set("Content-Type", "application/json")
|
||||
weakResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Register(weakResp, weakReq)
|
||||
if weakResp.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for weak password, got %d", weakResp.Code)
|
||||
}
|
||||
|
||||
var weakErrorResp map[string]any
|
||||
if err := json.Unmarshal(weakResp.Body.Bytes(), &weakErrorResp); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v", err)
|
||||
}
|
||||
if success, _ := weakErrorResp["success"].(bool); success {
|
||||
t.Error("Expected error response to have success=false")
|
||||
}
|
||||
if errorMsg, ok := weakErrorResp["error"].(string); !ok || errorMsg == "" {
|
||||
t.Error("Expected error response to contain validation error message")
|
||||
}
|
||||
|
||||
invalidData := map[string]string{
|
||||
"username": "invalid_user",
|
||||
"email": "not-an-email",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
invalidBody, _ := json.Marshal(invalidData)
|
||||
invalidReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(invalidBody))
|
||||
invalidReq.Header.Set("Content-Type", "application/json")
|
||||
invalidResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Register(invalidResp, invalidReq)
|
||||
if invalidResp.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid email, got %d", invalidResp.Code)
|
||||
}
|
||||
|
||||
var invalidEmailErrorResp map[string]any
|
||||
if err := json.Unmarshal(invalidResp.Body.Bytes(), &invalidEmailErrorResp); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v", err)
|
||||
}
|
||||
if success, _ := invalidEmailErrorResp["success"].(bool); success {
|
||||
t.Error("Expected error response to have success=false")
|
||||
}
|
||||
if errorMsg, ok := invalidEmailErrorResp["error"].(string); !ok || errorMsg == "" {
|
||||
t.Error("Expected error response to contain validation error message")
|
||||
}
|
||||
|
||||
incompleteData := map[string]string{
|
||||
"username": "incomplete_user",
|
||||
}
|
||||
incompleteBody, _ := json.Marshal(incompleteData)
|
||||
incompleteReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(incompleteBody))
|
||||
incompleteReq.Header.Set("Content-Type", "application/json")
|
||||
incompleteResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Register(incompleteResp, incompleteReq)
|
||||
if incompleteResp.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for missing fields, got %d", incompleteResp.Code)
|
||||
}
|
||||
|
||||
var incompleteErrorResp map[string]any
|
||||
if err := json.Unmarshal(incompleteResp.Body.Bytes(), &incompleteErrorResp); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v", err)
|
||||
}
|
||||
if success, _ := incompleteErrorResp["success"].(bool); success {
|
||||
t.Error("Expected error response to have success=false")
|
||||
}
|
||||
if errorMsg, ok := incompleteErrorResp["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")
|
||||
|
||||
postData := map[string]string{
|
||||
"title": "Handler Test Post",
|
||||
"url": "https://example.com/handler-test",
|
||||
"content": "This is a handler test post",
|
||||
}
|
||||
postBody, _ := json.Marshal(postData)
|
||||
postReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(postBody))
|
||||
postReq.Header.Set("Content-Type", "application/json")
|
||||
postReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
postReq = testutils.WithUserContext(postReq, middleware.UserIDKey, user.User.ID)
|
||||
postResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.CreatePost(postResp, postReq)
|
||||
if postResp.Code != http.StatusCreated {
|
||||
t.Errorf("Expected status 201, got %d", postResp.Code)
|
||||
}
|
||||
|
||||
var postResult map[string]any
|
||||
if err := json.Unmarshal(postResp.Body.Bytes(), &postResult); err != nil {
|
||||
t.Fatalf("Failed to decode post response: %v", err)
|
||||
}
|
||||
postDetails, ok := postResult["data"].(map[string]any)
|
||||
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")
|
||||
}
|
||||
|
||||
getReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d", int(postID)), nil)
|
||||
getReq = testutils.WithURLParams(getReq, map[string]string{"id": fmt.Sprintf("%d", int(postID))})
|
||||
getResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.GetPost(getResp, getReq)
|
||||
if getResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", getResp.Code)
|
||||
}
|
||||
|
||||
postsReq := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
postsResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.GetPosts(postsResp, postsReq)
|
||||
if postsResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", postsResp.Code)
|
||||
}
|
||||
|
||||
searchReq := httptest.NewRequest("GET", "/api/posts/search?q=handler", nil)
|
||||
searchResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.SearchPosts(searchResp, searchReq)
|
||||
if searchResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", searchResp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Post_Handler_Security_Validation", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
postData := map[string]string{
|
||||
"title": "Unauthorized Post",
|
||||
"url": "https://example.com/unauthorized",
|
||||
"content": "This should fail",
|
||||
}
|
||||
postBody, _ := json.Marshal(postData)
|
||||
postReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(postBody))
|
||||
postReq.Header.Set("Content-Type", "application/json")
|
||||
postResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.CreatePost(postResp, postReq)
|
||||
if postResp.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for unauthenticated post creation, got %d", postResp.Code)
|
||||
}
|
||||
|
||||
var authErrorResp map[string]any
|
||||
if err := json.Unmarshal(postResp.Body.Bytes(), &authErrorResp); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v", err)
|
||||
}
|
||||
if success, _ := authErrorResp["success"].(bool); success {
|
||||
t.Error("Expected error response to have success=false")
|
||||
}
|
||||
if errorMsg, ok := authErrorResp["error"].(string); !ok || errorMsg == "" {
|
||||
t.Error("Expected error response to contain authentication error message")
|
||||
}
|
||||
|
||||
user := createAuthenticatedUser(t, authService, userRepo, "security_user", "security@example.com")
|
||||
|
||||
invalidData := map[string]string{
|
||||
"title": "",
|
||||
"url": "not-a-url",
|
||||
"content": "Invalid post",
|
||||
}
|
||||
invalidBody, _ := json.Marshal(invalidData)
|
||||
invalidReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(invalidBody))
|
||||
invalidReq.Header.Set("Content-Type", "application/json")
|
||||
invalidReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
invalidReq = testutils.WithUserContext(invalidReq, middleware.UserIDKey, user.User.ID)
|
||||
invalidResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.CreatePost(invalidResp, invalidReq)
|
||||
if invalidResp.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid post data, got %d", invalidResp.Code)
|
||||
}
|
||||
|
||||
var postValidationErrorResp map[string]any
|
||||
if err := json.Unmarshal(invalidResp.Body.Bytes(), &postValidationErrorResp); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v", err)
|
||||
}
|
||||
if success, _ := postValidationErrorResp["success"].(bool); success {
|
||||
t.Error("Expected error response to have success=false")
|
||||
}
|
||||
if errorMsg, ok := postValidationErrorResp["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")
|
||||
|
||||
voteData := map[string]string{
|
||||
"type": "up",
|
||||
}
|
||||
voteBody, _ := json.Marshal(voteData)
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(voteBody))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteResp := httptest.NewRecorder()
|
||||
|
||||
voteHandler.CastVote(voteResp, voteReq)
|
||||
if voteResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", voteResp.Code)
|
||||
}
|
||||
|
||||
getVoteReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/vote", post.ID), nil)
|
||||
getVoteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getVoteReq = testutils.WithUserContext(getVoteReq, middleware.UserIDKey, user.User.ID)
|
||||
getVoteReq = testutils.WithURLParams(getVoteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getVoteResp := httptest.NewRecorder()
|
||||
|
||||
voteHandler.GetUserVote(getVoteResp, getVoteReq)
|
||||
if getVoteResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", getVoteResp.Code)
|
||||
}
|
||||
|
||||
getPostVotesReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d/votes", post.ID), nil)
|
||||
getPostVotesReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getPostVotesReq = testutils.WithUserContext(getPostVotesReq, middleware.UserIDKey, user.User.ID)
|
||||
getPostVotesReq = testutils.WithURLParams(getPostVotesReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getPostVotesResp := httptest.NewRecorder()
|
||||
|
||||
voteHandler.GetPostVotes(getPostVotesResp, getPostVotesReq)
|
||||
if getPostVotesResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", getPostVotesResp.Code)
|
||||
}
|
||||
|
||||
removeVoteReq := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d/vote", post.ID), nil)
|
||||
removeVoteReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
removeVoteReq = testutils.WithUserContext(removeVoteReq, middleware.UserIDKey, user.User.ID)
|
||||
removeVoteReq = testutils.WithURLParams(removeVoteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
removeVoteResp := httptest.NewRecorder()
|
||||
|
||||
voteHandler.RemoveVote(removeVoteResp, removeVoteReq)
|
||||
if removeVoteResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", removeVoteResp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User_Handler_Complete_Workflow", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createAuthenticatedUser(t, authService, userRepo, "user_handler_user", "user_handler@example.com")
|
||||
|
||||
usersReq := httptest.NewRequest("GET", "/api/users", nil)
|
||||
usersReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
usersReq = testutils.WithUserContext(usersReq, middleware.UserIDKey, user.User.ID)
|
||||
usersResp := httptest.NewRecorder()
|
||||
|
||||
userHandler.GetUsers(usersResp, usersReq)
|
||||
if usersResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", usersResp.Code)
|
||||
}
|
||||
|
||||
getUserReq := httptest.NewRequest("GET", fmt.Sprintf("/api/users/%d", user.User.ID), nil)
|
||||
getUserReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getUserReq = testutils.WithUserContext(getUserReq, middleware.UserIDKey, user.User.ID)
|
||||
getUserReq = testutils.WithURLParams(getUserReq, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
||||
getUserResp := httptest.NewRecorder()
|
||||
|
||||
userHandler.GetUser(getUserResp, getUserReq)
|
||||
if getUserResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", getUserResp.Code)
|
||||
}
|
||||
|
||||
getUserPostsReq := httptest.NewRequest("GET", fmt.Sprintf("/api/users/%d/posts", user.User.ID), nil)
|
||||
getUserPostsReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
getUserPostsReq = testutils.WithUserContext(getUserPostsReq, middleware.UserIDKey, user.User.ID)
|
||||
getUserPostsReq = testutils.WithURLParams(getUserPostsReq, map[string]string{"id": fmt.Sprintf("%d", user.User.ID)})
|
||||
getUserPostsResp := httptest.NewRecorder()
|
||||
|
||||
userHandler.GetUserPosts(getUserPostsResp, getUserPostsReq)
|
||||
if getUserPostsResp.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", getUserPostsResp.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Error_Handling_Invalid_Requests", func(t *testing.T) {
|
||||
invalidJSONReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer([]byte("invalid json")))
|
||||
invalidJSONReq.Header.Set("Content-Type", "application/json")
|
||||
invalidJSONResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Register(invalidJSONResp, invalidJSONReq)
|
||||
if invalidJSONResp.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid JSON, got %d", invalidJSONResp.Code)
|
||||
}
|
||||
|
||||
var jsonErrorResp map[string]any
|
||||
if err := json.Unmarshal(invalidJSONResp.Body.Bytes(), &jsonErrorResp); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v", err)
|
||||
}
|
||||
if success, _ := jsonErrorResp["success"].(bool); success {
|
||||
t.Error("Expected error response to have success=false")
|
||||
}
|
||||
if errorMsg, ok := jsonErrorResp["error"].(string); !ok || errorMsg == "" {
|
||||
t.Error("Expected error response to contain JSON parsing error message")
|
||||
}
|
||||
|
||||
missingCTData := map[string]string{
|
||||
"username": "missing_ct_user",
|
||||
"email": "missing_ct@example.com",
|
||||
"password": "SecurePass123!",
|
||||
}
|
||||
missingCTBody, _ := json.Marshal(missingCTData)
|
||||
missingCTReq := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(missingCTBody))
|
||||
missingCTResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Register(missingCTResp, missingCTReq)
|
||||
if missingCTResp.Code != http.StatusCreated {
|
||||
t.Errorf("Expected status 201, got %d", missingCTResp.Code)
|
||||
}
|
||||
|
||||
invalidEndpointReq := httptest.NewRequest("GET", "/api/invalid/endpoint", nil)
|
||||
invalidEndpointResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(invalidEndpointResp, invalidEndpointReq)
|
||||
if invalidEndpointResp.Code == http.StatusOK {
|
||||
t.Error("Expected error for invalid endpoint")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Security_Authentication_Bypass", func(t *testing.T) {
|
||||
meReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
meResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(meResp, meReq)
|
||||
if meResp.Code == http.StatusOK {
|
||||
t.Error("Expected error for unauthenticated request")
|
||||
}
|
||||
|
||||
invalidTokenReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
invalidTokenReq.Header.Set("Authorization", "Bearer invalid-token")
|
||||
invalidTokenResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(invalidTokenResp, invalidTokenReq)
|
||||
if invalidTokenResp.Code == http.StatusOK {
|
||||
t.Error("Expected error for invalid token")
|
||||
}
|
||||
|
||||
malformedTokenReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
malformedTokenReq.Header.Set("Authorization", "InvalidFormat token")
|
||||
malformedTokenResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(malformedTokenResp, malformedTokenReq)
|
||||
if malformedTokenResp.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")
|
||||
|
||||
xssData := map[string]string{
|
||||
"title": "<script>alert('xss')</script>",
|
||||
"url": "https://example.com/xss",
|
||||
"content": "XSS test content",
|
||||
}
|
||||
xssBody, _ := json.Marshal(xssData)
|
||||
xssReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(xssBody))
|
||||
xssReq.Header.Set("Content-Type", "application/json")
|
||||
xssReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
xssReq = testutils.WithUserContext(xssReq, middleware.UserIDKey, user.User.ID)
|
||||
xssResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.CreatePost(xssResp, xssReq)
|
||||
if xssResp.Code != http.StatusCreated {
|
||||
t.Errorf("Expected status 201 for XSS sanitization, got %d", xssResp.Code)
|
||||
}
|
||||
|
||||
var xssResult map[string]any
|
||||
if err := json.Unmarshal(xssResp.Body.Bytes(), &xssResult); err != nil {
|
||||
t.Fatalf("Failed to decode XSS response: %v", err)
|
||||
}
|
||||
if success, _ := xssResult["success"].(bool); !success {
|
||||
t.Error("Expected XSS response to have success=true")
|
||||
}
|
||||
|
||||
data, ok := xssResult["data"].(map[string]any)
|
||||
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)
|
||||
}
|
||||
|
||||
sqlData := map[string]string{
|
||||
"title": "'; DROP TABLE posts; --",
|
||||
"url": "https://example.com/sql",
|
||||
"content": "SQL injection test",
|
||||
}
|
||||
sqlBody, _ := json.Marshal(sqlData)
|
||||
sqlReq := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(sqlBody))
|
||||
sqlReq.Header.Set("Content-Type", "application/json")
|
||||
sqlReq.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
sqlReq = testutils.WithUserContext(sqlReq, middleware.UserIDKey, user.User.ID)
|
||||
sqlResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.CreatePost(sqlResp, sqlReq)
|
||||
if sqlResp.Code != http.StatusCreated {
|
||||
t.Errorf("Expected status 201 for SQL injection sanitization, got %d", sqlResp.Code)
|
||||
}
|
||||
|
||||
var sqlResult map[string]any
|
||||
if err := json.Unmarshal(sqlResp.Body.Bytes(), &sqlResult); err != nil {
|
||||
t.Fatalf("Failed to decode SQL response: %v", err)
|
||||
}
|
||||
if success, _ := sqlResult["success"].(bool); !success {
|
||||
t.Error("Expected SQL response to have success=true")
|
||||
}
|
||||
|
||||
sqlResponseData, ok := sqlResult["data"].(map[string]any)
|
||||
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()
|
||||
user1 := createAuthenticatedUser(t, authService, userRepo, "auth_user1", "auth1@example.com")
|
||||
user2 := createAuthenticatedUser(t, authService, userRepo, "auth_user2", "auth2@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, postRepo, user1.User.ID, "Private Post", "https://example.com/private")
|
||||
|
||||
getPostReq := httptest.NewRequest("GET", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
getPostReq.Header.Set("Authorization", "Bearer "+user2.Token)
|
||||
getPostReq = testutils.WithUserContext(getPostReq, middleware.UserIDKey, user2.User.ID)
|
||||
getPostReq = testutils.WithURLParams(getPostReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
getPostResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.GetPost(getPostResp, getPostReq)
|
||||
testutils.AssertHTTPStatus(t, getPostResp, http.StatusOK)
|
||||
|
||||
updateData := map[string]string{
|
||||
"title": "Updated Title",
|
||||
}
|
||||
updateBody, _ := json.Marshal(updateData)
|
||||
updateReq := httptest.NewRequest("PUT", fmt.Sprintf("/api/posts/%d", post.ID), bytes.NewBuffer(updateBody))
|
||||
updateReq.Header.Set("Content-Type", "application/json")
|
||||
updateReq.Header.Set("Authorization", "Bearer "+user2.Token)
|
||||
updateReq = testutils.WithUserContext(updateReq, middleware.UserIDKey, user2.User.ID)
|
||||
updateReq = testutils.WithURLParams(updateReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
updateResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.UpdatePost(updateResp, updateReq)
|
||||
testutils.AssertHTTPStatus(t, updateResp, http.StatusForbidden)
|
||||
|
||||
deleteReq := httptest.NewRequest("DELETE", fmt.Sprintf("/api/posts/%d", post.ID), nil)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+user2.Token)
|
||||
deleteReq = testutils.WithUserContext(deleteReq, middleware.UserIDKey, user2.User.ID)
|
||||
deleteReq = testutils.WithURLParams(deleteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
deleteResp := httptest.NewRecorder()
|
||||
|
||||
postHandler.DeletePost(deleteResp, deleteReq)
|
||||
testutils.AssertHTTPStatus(t, deleteResp, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("Authorization_Vote_Access_Control", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user1 := createAuthenticatedUser(t, authService, userRepo, "vote_auth_user1", "vote_auth1@example.com")
|
||||
user2 := createAuthenticatedUser(t, authService, userRepo, "vote_auth_user2", "vote_auth2@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, postRepo, user1.User.ID, "Vote Auth Post", "https://example.com/vote-auth")
|
||||
|
||||
voteData := map[string]string{"type": "up"}
|
||||
voteBody, _ := json.Marshal(voteData)
|
||||
voteReq := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(voteBody))
|
||||
voteReq.Header.Set("Content-Type", "application/json")
|
||||
voteReq.Header.Set("Authorization", "Bearer "+user2.Token)
|
||||
voteReq = testutils.WithUserContext(voteReq, middleware.UserIDKey, user2.User.ID)
|
||||
voteReq = testutils.WithURLParams(voteReq, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
voteResp := httptest.NewRecorder()
|
||||
|
||||
voteHandler.CastVote(voteResp, voteReq)
|
||||
if voteResp.Code != http.StatusOK {
|
||||
t.Errorf("Users should be able to vote on any post, got %d", voteResp.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)
|
||||
}
|
||||
|
||||
meReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
meReq.Header.Set("Authorization", "Bearer "+expiredToken)
|
||||
meResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(meResp, meReq)
|
||||
testutils.AssertHTTPStatus(t, meResp, 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"
|
||||
|
||||
meReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
meReq.Header.Set("Authorization", "Bearer "+tamperedToken)
|
||||
meResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(meResp, meReq)
|
||||
testutils.AssertHTTPStatus(t, meResp, 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)
|
||||
}
|
||||
|
||||
meReq := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
meReq.Header.Set("Authorization", "Bearer "+invalidToken)
|
||||
meResp := httptest.NewRecorder()
|
||||
|
||||
authHandler.Me(meResp, meReq)
|
||||
testutils.AssertHTTPStatus(t, meResp, 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 := response["data"].(map[string]any)
|
||||
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 := response["data"].(map[string]any)
|
||||
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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
358
internal/integration/helpers.go
Normal file
358
internal/integration/helpers.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/handlers"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/repositories"
|
||||
"goyco/internal/server"
|
||||
"goyco/internal/services"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
type testContext struct {
|
||||
Router http.Handler
|
||||
Suite *testutils.ServiceSuite
|
||||
AuthService *services.AuthFacade
|
||||
}
|
||||
|
||||
func setupTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
middleware.StopAllRateLimiters()
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
|
||||
metadataService := suite.TitleFetcher
|
||||
|
||||
authHandler := handlers.NewAuthHandler(authService, suite.UserRepo)
|
||||
postHandler := handlers.NewPostHandler(suite.PostRepo, metadataService, voteService)
|
||||
voteHandler := handlers.NewVoteHandler(voteService)
|
||||
userHandler := handlers.NewUserHandler(suite.UserRepo, authService)
|
||||
apiHandler := handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService, suite.DB, middleware.NewInMemoryDBMonitor())
|
||||
|
||||
staticDir := t.TempDir()
|
||||
robotsFile := filepath.Join(staticDir, "robots.txt")
|
||||
os.WriteFile(robotsFile, []byte("User-agent: *\nDisallow: /"), 0644)
|
||||
|
||||
router := server.NewRouter(server.RouterConfig{
|
||||
AuthHandler: authHandler,
|
||||
PostHandler: postHandler,
|
||||
VoteHandler: voteHandler,
|
||||
UserHandler: userHandler,
|
||||
APIHandler: apiHandler,
|
||||
AuthService: authService,
|
||||
PageHandler: nil,
|
||||
StaticDir: staticDir,
|
||||
Debug: false,
|
||||
DisableCache: false,
|
||||
DisableCompression: false,
|
||||
DBMonitor: middleware.NewInMemoryDBMonitor(),
|
||||
RateLimitConfig: testutils.AppTestConfig.RateLimit,
|
||||
})
|
||||
|
||||
return &testContext{
|
||||
Router: router,
|
||||
Suite: suite,
|
||||
AuthService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
func setupPageHandlerTestContext(t *testing.T) *testContext {
|
||||
t.Helper()
|
||||
middleware.StopAllRateLimiters()
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
|
||||
metadataService := suite.TitleFetcher
|
||||
|
||||
authHandler := handlers.NewAuthHandler(authService, suite.UserRepo)
|
||||
postHandler := handlers.NewPostHandler(suite.PostRepo, metadataService, voteService)
|
||||
voteHandler := handlers.NewVoteHandler(voteService)
|
||||
userHandler := handlers.NewUserHandler(suite.UserRepo, authService)
|
||||
apiHandler := handlers.NewAPIHandler(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService)
|
||||
|
||||
staticDir := t.TempDir()
|
||||
templatesDir := t.TempDir()
|
||||
|
||||
baseTemplate := `{{define "layout"}}<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{block "content" .}}{{end}}
|
||||
</body>
|
||||
</html>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "base.gohtml"), []byte(baseTemplate), 0644)
|
||||
|
||||
os.MkdirAll(filepath.Join(templatesDir, "partials"), 0755)
|
||||
|
||||
homeTemplate := `{{define "content"}}<h1>Home</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "home.gohtml"), []byte(homeTemplate), 0644)
|
||||
|
||||
loginTemplate := `{{define "content"}}<h1>Login</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "login.gohtml"), []byte(loginTemplate), 0644)
|
||||
|
||||
registerTemplate := `{{define "content"}}<h1>Register</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "register.gohtml"), []byte(registerTemplate), 0644)
|
||||
|
||||
settingsTemplate := `{{define "content"}}<h1>Settings</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "settings.gohtml"), []byte(settingsTemplate), 0644)
|
||||
|
||||
postTemplate := `{{define "content"}}<h1>{{.Post.Title}}</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "post.gohtml"), []byte(postTemplate), 0644)
|
||||
|
||||
errorTemplate := `{{define "content"}}<h1>Error</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "error.gohtml"), []byte(errorTemplate), 0644)
|
||||
|
||||
confirmTemplate := `{{define "content"}}<h1>Confirm</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "confirm.gohtml"), []byte(confirmTemplate), 0644)
|
||||
|
||||
confirmEmailTemplate := `{{define "content"}}<h1>Confirm Email</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "confirm_email.gohtml"), []byte(confirmEmailTemplate), 0644)
|
||||
|
||||
resendTemplate := `{{define "content"}}<h1>Resend</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "resend-verification.gohtml"), []byte(resendTemplate), 0644)
|
||||
|
||||
resendVerificationTemplate := `{{define "content"}}<h1>Resend Verification</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "resend_verification.gohtml"), []byte(resendVerificationTemplate), 0644)
|
||||
|
||||
forgotTemplate := `{{define "content"}}<h1>Forgot Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "forgot-password.gohtml"), []byte(forgotTemplate), 0644)
|
||||
|
||||
forgotPasswordTemplate := `{{define "content"}}<h1>Forgot Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "forgot_password.gohtml"), []byte(forgotPasswordTemplate), 0644)
|
||||
|
||||
resetTemplate := `{{define "content"}}<h1>Reset Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "reset-password.gohtml"), []byte(resetTemplate), 0644)
|
||||
|
||||
resetPasswordTemplate := `{{define "content"}}<h1>Reset Password</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "reset_password.gohtml"), []byte(resetPasswordTemplate), 0644)
|
||||
|
||||
searchTemplate := `{{define "content"}}<h1>Search</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "search.gohtml"), []byte(searchTemplate), 0644)
|
||||
|
||||
newPostTemplate := `{{define "content"}}<h1>New Post</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "new-post.gohtml"), []byte(newPostTemplate), 0644)
|
||||
|
||||
newPostTemplate2 := `{{define "content"}}<h1>New Post</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "new_post.gohtml"), []byte(newPostTemplate2), 0644)
|
||||
|
||||
confirmDeleteTemplate := `{{define "content"}}<h1>Confirm Delete</h1>{{end}}`
|
||||
os.WriteFile(filepath.Join(templatesDir, "confirm_delete.gohtml"), []byte(confirmDeleteTemplate), 0644)
|
||||
|
||||
pageHandler, err := handlers.NewPageHandler(templatesDir, authService, suite.PostRepo, voteService, suite.UserRepo, metadataService, testutils.AppTestConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create page handler: %v", err)
|
||||
}
|
||||
|
||||
router := server.NewRouter(server.RouterConfig{
|
||||
AuthHandler: authHandler,
|
||||
PostHandler: postHandler,
|
||||
VoteHandler: voteHandler,
|
||||
UserHandler: userHandler,
|
||||
APIHandler: apiHandler,
|
||||
AuthService: authService,
|
||||
PageHandler: pageHandler,
|
||||
StaticDir: staticDir,
|
||||
Debug: false,
|
||||
DisableCache: false,
|
||||
DisableCompression: false,
|
||||
DBMonitor: middleware.NewInMemoryDBMonitor(),
|
||||
RateLimitConfig: testutils.AppTestConfig.RateLimit,
|
||||
})
|
||||
|
||||
return &testContext{
|
||||
Router: router,
|
||||
Suite: suite,
|
||||
AuthService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
func getCSRFToken(t *testing.T, router http.Handler, path string, cookies ...*http.Cookie) string {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest("GET", path, nil)
|
||||
for _, cookie := range cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
cookieList := rec.Result().Cookies()
|
||||
for _, cookie := range cookieList {
|
||||
if cookie.Name == "csrf_token" {
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
t.Fatal("CSRF token not found")
|
||||
return ""
|
||||
}
|
||||
|
||||
func assertJSONResponse(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) map[string]any {
|
||||
t.Helper()
|
||||
if rec.Code != expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d. Body: %s", expectedStatus, rec.Code, rec.Body.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
var response map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v. Body: %s", err, rec.Body.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func assertErrorResponse(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) {
|
||||
t.Helper()
|
||||
if rec.Code != expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d. Body: %s", expectedStatus, rec.Code, rec.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
var response map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("Failed to decode error response: %v. Body: %s", err, rec.Body.String())
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := response["error"]; !ok {
|
||||
if _, ok := response["message"]; !ok {
|
||||
t.Error("Expected error or message field in error response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertStatus(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) {
|
||||
t.Helper()
|
||||
if rec.Code != expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d. Body: %s", expectedStatus, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func assertStatusRange(t *testing.T, rec *httptest.ResponseRecorder, minStatus, maxStatus int) {
|
||||
t.Helper()
|
||||
if rec.Code < minStatus || rec.Code > maxStatus {
|
||||
t.Errorf("Expected status between %d and %d, got %d. Body: %s", minStatus, maxStatus, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func assertCookie(t *testing.T, rec *httptest.ResponseRecorder, name, expectedValue string) {
|
||||
t.Helper()
|
||||
cookies := rec.Result().Cookies()
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == name {
|
||||
if expectedValue != "" && cookie.Value != expectedValue {
|
||||
t.Errorf("Expected cookie %s value %s, got %s", name, expectedValue, cookie.Value)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("Expected cookie %s not found", name)
|
||||
}
|
||||
|
||||
func assertCookieCleared(t *testing.T, rec *httptest.ResponseRecorder, name string) {
|
||||
t.Helper()
|
||||
cookies := rec.Result().Cookies()
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == name {
|
||||
if cookie.Value != "" {
|
||||
t.Errorf("Expected cookie %s to be cleared, got value %s", name, cookie.Value)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertHeader(t *testing.T, rec *httptest.ResponseRecorder, name, expectedValue string) {
|
||||
t.Helper()
|
||||
actualValue := rec.Header().Get(name)
|
||||
if actualValue != expectedValue {
|
||||
t.Errorf("Expected header %s=%s, got %s", name, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
|
||||
func assertHeaderContains(t *testing.T, rec *httptest.ResponseRecorder, name, substring string) {
|
||||
t.Helper()
|
||||
actualValue := rec.Header().Get(name)
|
||||
if !strings.Contains(actualValue, substring) {
|
||||
t.Errorf("Expected header %s to contain %s, got %s", name, substring, actualValue)
|
||||
}
|
||||
}
|
||||
|
||||
type authenticatedUser struct {
|
||||
User *database.User
|
||||
Token string
|
||||
}
|
||||
|
||||
func createAuthenticatedUser(t *testing.T, authService *services.AuthFacade, userRepo repositories.UserRepository, username, email string) *authenticatedUser {
|
||||
t.Helper()
|
||||
|
||||
password := "SecurePass123!"
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to hash password: %v", err)
|
||||
}
|
||||
|
||||
user := &database.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: string(hashedPassword),
|
||||
EmailVerified: true,
|
||||
}
|
||||
|
||||
if err := userRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create authenticated user: %v", err)
|
||||
}
|
||||
|
||||
loginResult, err := authService.Login(username, password)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login authenticated user: %v", err)
|
||||
}
|
||||
|
||||
return &authenticatedUser{
|
||||
User: loginResult.User,
|
||||
Token: loginResult.AccessToken,
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueTestUsername(t *testing.T, prefix string) string {
|
||||
return fmt.Sprintf("%s_%d_%d", prefix, time.Now().UnixNano(), len(t.Name()))
|
||||
}
|
||||
|
||||
func uniqueTestEmail(t *testing.T, prefix string) string {
|
||||
return fmt.Sprintf("%s_%d_%d@example.com", prefix, time.Now().UnixNano(), len(t.Name()))
|
||||
}
|
||||
|
||||
func createUserWithCleanup(t *testing.T, ctx *testContext, username, email string) *authenticatedUser {
|
||||
t.Helper()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, username, email)
|
||||
t.Cleanup(func() {
|
||||
if err := ctx.Suite.UserRepo.Delete(user.User.ID); err != nil {
|
||||
t.Logf("Failed to cleanup user %d: %v", user.User.ID, err)
|
||||
}
|
||||
})
|
||||
return user
|
||||
}
|
||||
218
internal/integration/page_handler_forms_integration_test.go
Normal file
218
internal/integration/page_handler_forms_integration_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_PageHandlerFormWorkflows(t *testing.T) {
|
||||
ctx := setupPageHandlerTestContext(t)
|
||||
router := ctx.Router
|
||||
authService := ctx.AuthService
|
||||
|
||||
t.Run("Settings_Email_Update_Form", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, authService, ctx.Suite.UserRepo, "settings_email_user", "settings_email@example.com")
|
||||
|
||||
getReq := httptest.NewRequest("GET", "/settings", nil)
|
||||
getReq.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
|
||||
csrfToken := getCSRFToken(t, router, "/settings", &http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("email", "newemail@example.com")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/settings/email", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Settings_Username_Update_Form", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, authService, ctx.Suite.UserRepo, "settings_username_user", "settings_username@example.com")
|
||||
|
||||
csrfToken := getCSRFToken(t, router, "/settings", &http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "new_username")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/settings/username", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Settings_Password_Update_Form", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
freshCtx := setupPageHandlerTestContext(t)
|
||||
freshCtx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, freshCtx.AuthService, freshCtx.Suite.UserRepo, "settings_password_user", "settings_password@example.com")
|
||||
|
||||
csrfToken := getCSRFToken(t, freshCtx.Router, "/settings", &http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("current_password", "SecurePass123!")
|
||||
reqBody.Set("new_password", "NewSecurePass123!")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/settings/password", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
freshCtx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Logout_Page_Handler", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
freshCtx := setupPageHandlerTestContext(t)
|
||||
freshCtx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, freshCtx.AuthService, freshCtx.Suite.UserRepo, "logout_page_user", "logout_page@example.com")
|
||||
|
||||
csrfToken := getCSRFToken(t, freshCtx.Router, "/settings", &http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/logout", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
freshCtx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusSeeOther)
|
||||
assertCookieCleared(t, rec, "auth_token")
|
||||
})
|
||||
|
||||
t.Run("Resend_Verification_Page_Handler", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
freshCtx := setupPageHandlerTestContext(t)
|
||||
freshCtx.Suite.EmailSender.Reset()
|
||||
|
||||
csrfToken := getCSRFToken(t, freshCtx.Router, "/resend-verification")
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("email", "resend_page@example.com")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/resend-verification", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
freshCtx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Post_Vote_Page_Handler", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
freshCtx := setupPageHandlerTestContext(t)
|
||||
freshCtx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, freshCtx.AuthService, freshCtx.Suite.UserRepo, "vote_page_user", "vote_page@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, freshCtx.Suite.PostRepo, user.User.ID, "Vote Page Test", "https://example.com/vote-page")
|
||||
|
||||
getReq := httptest.NewRequest("GET", fmt.Sprintf("/posts/%d", post.ID), nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
freshCtx.Router.ServeHTTP(getRec, getReq)
|
||||
|
||||
csrfToken := getCSRFToken(t, freshCtx.Router, fmt.Sprintf("/posts/%d", post.ID))
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("action", "up")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/posts/%d/vote", post.ID), strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
freshCtx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("Login_Page_Handler_Workflow", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
freshCtx := setupPageHandlerTestContext(t)
|
||||
freshCtx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, freshCtx.AuthService, freshCtx.Suite.UserRepo, "login_page_user", "login_page@example.com")
|
||||
|
||||
csrfToken := getCSRFToken(t, freshCtx.Router, "/login")
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "login_page_user")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
|
||||
req := httptest.NewRequest("POST", "/login", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
freshCtx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusSeeOther)
|
||||
cookies := rec.Result().Cookies()
|
||||
authCookieSet := false
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "auth_token" && cookie.Value != "" {
|
||||
authCookieSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authCookieSet {
|
||||
t.Error("Expected auth cookie to be set on login")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Email_Confirmation_Page_Handler", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, authService, ctx.Suite.UserRepo, "confirm_page_user", "confirm_page@example.com")
|
||||
|
||||
token := ctx.Suite.EmailSender.VerificationToken()
|
||||
if token == "" {
|
||||
token = "test-token"
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/confirm?token="+url.QueryEscape(token), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
164
internal/integration/page_handler_integration_test.go
Normal file
164
internal/integration/page_handler_integration_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_PageHandler(t *testing.T) {
|
||||
ctx := setupPageHandlerTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Home_Page_Renders", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
if !strings.Contains(rec.Body.String(), "<html") {
|
||||
t.Error("Expected HTML content")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Login_Form_Renders", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/login", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "login") && !strings.Contains(body, "Login") {
|
||||
t.Error("Expected login form content")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Register_Form_Renders", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/register", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "register") && !strings.Contains(body, "Register") {
|
||||
t.Error("Expected register form content")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PageHandler_With_CSRF_Token", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/register", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
cookies := rec.Result().Cookies()
|
||||
csrfFound := false
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "csrf_token" {
|
||||
csrfFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !csrfFound {
|
||||
t.Error("Expected CSRF token cookie to be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PageHandler_Form_Submission", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
getReq := httptest.NewRequest("GET", "/register", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(getRec, getReq)
|
||||
|
||||
cookies := getRec.Result().Cookies()
|
||||
var csrfCookie *http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "csrf_token" {
|
||||
csrfCookie = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if csrfCookie == nil {
|
||||
t.Fatal("Expected CSRF cookie")
|
||||
}
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username", "page_form_user")
|
||||
reqBody.Set("email", "page_form@example.com")
|
||||
reqBody.Set("password", "SecurePass123!")
|
||||
reqBody.Set("csrf_token", csrfCookie.Value)
|
||||
|
||||
req := httptest.NewRequest("POST", "/register", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(csrfCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
t.Run("PageHandler_Authenticated_Access", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "page_auth_user", "page_auth@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", "/settings", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: user.Token})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("PageHandler_Post_Display", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "page_post_user", "page_post@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Page Test Post", "https://example.com/page-test")
|
||||
|
||||
req := httptest.NewRequest("GET", "/posts/"+fmt.Sprintf("%d", post.ID), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "Page Test Post") {
|
||||
t.Error("Expected post title in page")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PageHandler_Search_Page", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/search?q=test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("PageHandler_Error_Handling", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/nonexistent", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
263
internal/integration/password_reset_integration_test.go
Normal file
263
internal/integration/password_reset_integration_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_PasswordReset_CompleteFlow(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("API_PasswordReset_Request", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "reset_user",
|
||||
Email: "reset@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := ctx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username_or_email": "reset_user",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/forgot-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if success, ok := response["success"].(bool); !ok || !success {
|
||||
t.Error("Expected success=true")
|
||||
}
|
||||
}
|
||||
|
||||
resetToken := ctx.Suite.EmailSender.PasswordResetToken()
|
||||
if resetToken == "" {
|
||||
t.Error("Expected password reset token to be generated")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("API_PasswordReset_Complete", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "reset_complete_user",
|
||||
Email: "reset_complete@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := ctx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
if err := ctx.AuthService.RequestPasswordReset("reset_complete_user"); err != nil {
|
||||
t.Fatalf("Failed to request password reset: %v", err)
|
||||
}
|
||||
|
||||
resetToken := ctx.Suite.EmailSender.PasswordResetToken()
|
||||
if resetToken == "" {
|
||||
t.Fatal("Expected password reset token")
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"token": resetToken,
|
||||
"new_password": "NewPassword123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/reset-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
loginResult, err := ctx.AuthService.Login("reset_complete_user", "NewPassword123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login with new password: %v", err)
|
||||
}
|
||||
|
||||
if loginResult.User.Username != "reset_complete_user" {
|
||||
t.Error("Expected login to succeed with new password")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Page_PasswordReset_Request", func(t *testing.T) {
|
||||
pageCtx := setupPageHandlerTestContext(t)
|
||||
pageRouter := pageCtx.Router
|
||||
pageCtx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "page_reset_user",
|
||||
Email: "page_reset@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := pageCtx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
csrfToken := getCSRFToken(t, pageRouter, "/forgot-password")
|
||||
|
||||
reqBody := url.Values{}
|
||||
reqBody.Set("username_or_email", "page_reset_user")
|
||||
reqBody.Set("csrf_token", csrfToken)
|
||||
req := httptest.NewRequest("POST", "/forgot-password", strings.NewReader(reqBody.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
pageRouter.ServeHTTP(rec, req)
|
||||
|
||||
assertStatusRange(t, rec, http.StatusOK, http.StatusSeeOther)
|
||||
|
||||
resetToken := pageCtx.Suite.EmailSender.PasswordResetToken()
|
||||
if resetToken == "" {
|
||||
t.Error("Expected password reset token to be generated")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PasswordReset_TokenExpiration", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "expire_user",
|
||||
Email: "expire@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := ctx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
if err := ctx.AuthService.RequestPasswordReset("expire_user"); err != nil {
|
||||
t.Fatalf("Failed to request password reset: %v", err)
|
||||
}
|
||||
|
||||
resetToken := ctx.Suite.EmailSender.PasswordResetToken()
|
||||
hashedToken := testutils.HashVerificationToken(resetToken)
|
||||
|
||||
user, err := ctx.Suite.UserRepo.GetByPasswordResetToken(hashedToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
expiredTime := time.Now().Add(-25 * time.Hour)
|
||||
user.PasswordResetExpiresAt = &expiredTime
|
||||
if err := ctx.Suite.UserRepo.Update(user); err != nil {
|
||||
t.Fatalf("Failed to update user: %v", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"token": resetToken,
|
||||
"new_password": "NewPassword123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/reset-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("PasswordReset_InvalidToken", func(t *testing.T) {
|
||||
reqBody := map[string]string{
|
||||
"token": "invalid-token",
|
||||
"new_password": "NewPassword123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/reset-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("PasswordReset_WeakPassword", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "weak_pass_user",
|
||||
Email: "weak_pass@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := ctx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
if err := ctx.AuthService.RequestPasswordReset("weak_pass_user"); err != nil {
|
||||
t.Fatalf("Failed to request password reset: %v", err)
|
||||
}
|
||||
|
||||
resetToken := ctx.Suite.EmailSender.PasswordResetToken()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"token": resetToken,
|
||||
"new_password": "123",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/reset-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("PasswordReset_EmailIntegration", func(t *testing.T) {
|
||||
middleware.StopAllRateLimiters()
|
||||
freshCtx := setupTestContext(t)
|
||||
freshCtx.Suite.EmailSender.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "email_reset_user",
|
||||
Email: "email_reset@example.com",
|
||||
Password: testutils.HashPassword("OldPassword123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := freshCtx.Suite.UserRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"username_or_email": "email_reset@example.com",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/forgot-password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
freshCtx.Router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
resetToken := freshCtx.Suite.EmailSender.PasswordResetToken()
|
||||
if resetToken == "" {
|
||||
t.Error("Expected password reset token when using email")
|
||||
}
|
||||
})
|
||||
}
|
||||
197
internal/integration/ratelimit_integration_test.go
Normal file
197
internal/integration/ratelimit_integration_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/config"
|
||||
"goyco/internal/handlers"
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/server"
|
||||
"goyco/internal/services"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func setupRateLimitRouter(t *testing.T, rateLimitConfig config.RateLimitConfig) (http.Handler, *testutils.ServiceSuite) {
|
||||
t.Helper()
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
|
||||
metadataService := services.NewURLMetadataService()
|
||||
|
||||
authHandler := handlers.NewAuthHandler(authService, suite.UserRepo)
|
||||
postHandler := handlers.NewPostHandler(suite.PostRepo, metadataService, voteService)
|
||||
voteHandler := handlers.NewVoteHandler(voteService)
|
||||
userHandler := handlers.NewUserHandler(suite.UserRepo, authService)
|
||||
apiHandler := handlers.NewAPIHandlerWithMonitoring(testutils.AppTestConfig, suite.PostRepo, suite.UserRepo, voteService, suite.DB, middleware.NewInMemoryDBMonitor())
|
||||
|
||||
staticDir := t.TempDir()
|
||||
|
||||
router := server.NewRouter(server.RouterConfig{
|
||||
AuthHandler: authHandler,
|
||||
PostHandler: postHandler,
|
||||
VoteHandler: voteHandler,
|
||||
UserHandler: userHandler,
|
||||
APIHandler: apiHandler,
|
||||
AuthService: authService,
|
||||
PageHandler: nil,
|
||||
StaticDir: staticDir,
|
||||
Debug: false,
|
||||
DisableCache: false,
|
||||
DisableCompression: false,
|
||||
DBMonitor: middleware.NewInMemoryDBMonitor(),
|
||||
RateLimitConfig: rateLimitConfig,
|
||||
})
|
||||
|
||||
return router, suite
|
||||
}
|
||||
|
||||
func TestIntegration_RateLimiting(t *testing.T) {
|
||||
t.Run("Auth_RateLimit_Enforced", func(t *testing.T) {
|
||||
rateLimitConfig := testutils.AppTestConfig.RateLimit
|
||||
rateLimitConfig.AuthLimit = 2
|
||||
router, _ := setupRateLimitRouter(t, rateLimitConfig)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBufferString(`{"username":"test","password":"test"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBufferString(`{"username":"test","password":"test"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusTooManyRequests)
|
||||
|
||||
if rec.Header().Get("Retry-After") == "" {
|
||||
t.Error("Expected Retry-After header")
|
||||
}
|
||||
|
||||
var response map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&response); err == nil {
|
||||
if _, exists := response["retry_after"]; !exists {
|
||||
t.Error("Expected retry_after in response")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("General_RateLimit_Enforced", func(t *testing.T) {
|
||||
rateLimitConfig := testutils.AppTestConfig.RateLimit
|
||||
rateLimitConfig.GeneralLimit = 5
|
||||
router, _ := setupRateLimitRouter(t, rateLimitConfig)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusTooManyRequests)
|
||||
})
|
||||
|
||||
t.Run("Health_RateLimit_Enforced", func(t *testing.T) {
|
||||
rateLimitConfig := testutils.AppTestConfig.RateLimit
|
||||
rateLimitConfig.HealthLimit = 3
|
||||
router, _ := setupRateLimitRouter(t, rateLimitConfig)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusTooManyRequests)
|
||||
})
|
||||
|
||||
t.Run("Metrics_RateLimit_Enforced", func(t *testing.T) {
|
||||
rateLimitConfig := testutils.AppTestConfig.RateLimit
|
||||
rateLimitConfig.MetricsLimit = 2
|
||||
router, _ := setupRateLimitRouter(t, rateLimitConfig)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusTooManyRequests)
|
||||
})
|
||||
|
||||
t.Run("RateLimit_Different_Endpoints_Independent", func(t *testing.T) {
|
||||
rateLimitConfig := testutils.AppTestConfig.RateLimit
|
||||
rateLimitConfig.AuthLimit = 2
|
||||
rateLimitConfig.GeneralLimit = 10
|
||||
router, _ := setupRateLimitRouter(t, rateLimitConfig)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBufferString(`{"username":"test","password":"test"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("RateLimit_With_Authentication", func(t *testing.T) {
|
||||
rateLimitConfig := testutils.AppTestConfig.RateLimit
|
||||
rateLimitConfig.GeneralLimit = 3
|
||||
router, suite := setupRateLimitRouter(t, rateLimitConfig)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, authService, suite.UserRepo, uniqueTestUsername(t, "ratelimit_auth"), uniqueTestEmail(t, "ratelimit_auth"))
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertErrorResponse(t, rec, http.StatusTooManyRequests)
|
||||
})
|
||||
}
|
||||
621
internal/integration/repositories_integration_test.go
Normal file
621
internal/integration/repositories_integration_test.go
Normal file
@@ -0,0 +1,621 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/repositories"
|
||||
)
|
||||
|
||||
func TestIntegration_Repositories(t *testing.T) {
|
||||
suite := repositories.NewTestSuite(t)
|
||||
|
||||
t.Run("User_Lifecycle", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "lifecycle_user",
|
||||
Email: "lifecycle@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: false,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.UserRepo.GetByID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve user: %v", err)
|
||||
}
|
||||
if retrieved.Username != "lifecycle_user" {
|
||||
t.Errorf("Expected username 'lifecycle_user', got '%s'", retrieved.Username)
|
||||
}
|
||||
|
||||
retrieved.EmailVerified = true
|
||||
err = suite.UserRepo.Update(retrieved)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update user: %v", err)
|
||||
}
|
||||
|
||||
updated, err := suite.UserRepo.GetByID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve updated user: %v", err)
|
||||
}
|
||||
if !updated.EmailVerified {
|
||||
t.Error("Expected email to be verified")
|
||||
}
|
||||
|
||||
err = suite.UserRepo.Delete(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete user: %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.UserRepo.GetByID(user.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected user to be deleted")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Post_Lifecycle", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "post_author",
|
||||
Email: "author@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Integration Test Post",
|
||||
URL: "https://example.com/integration-test",
|
||||
Content: "This is a comprehensive integration test post",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err = suite.PostRepo.Create(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.PostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve post: %v", err)
|
||||
}
|
||||
if retrieved.Title != "Integration Test Post" {
|
||||
t.Errorf("Expected title 'Integration Test Post', got '%s'", retrieved.Title)
|
||||
}
|
||||
|
||||
retrieved.Title = "Updated Integration Test Post"
|
||||
err = suite.PostRepo.Update(retrieved)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update post: %v", err)
|
||||
}
|
||||
|
||||
updated, err := suite.PostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve updated post: %v", err)
|
||||
}
|
||||
if updated.Title != "Updated Integration Test Post" {
|
||||
t.Errorf("Expected updated title, got '%s'", updated.Title)
|
||||
}
|
||||
|
||||
err = suite.PostRepo.Delete(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete post: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Vote_Lifecycle", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "voter",
|
||||
Email: "voter@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Vote Test Post",
|
||||
URL: "https://example.com/vote-test",
|
||||
Content: "Vote test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err = suite.PostRepo.Create(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post: %v", err)
|
||||
}
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err = suite.VoteRepo.Create(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vote: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := suite.VoteRepo.GetByID(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve vote: %v", err)
|
||||
}
|
||||
if retrieved.Type != database.VoteUp {
|
||||
t.Errorf("Expected vote type %v, got %v", database.VoteUp, retrieved.Type)
|
||||
}
|
||||
|
||||
retrieved.Type = database.VoteDown
|
||||
err = suite.VoteRepo.Update(retrieved)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update vote: %v", err)
|
||||
}
|
||||
|
||||
updated, err := suite.VoteRepo.GetByID(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve updated vote: %v", err)
|
||||
}
|
||||
if updated.Type != database.VoteDown {
|
||||
t.Errorf("Expected updated vote type, got %v", updated.Type)
|
||||
}
|
||||
|
||||
err = suite.VoteRepo.Delete(vote.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete vote: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Security_SQL_Injection_Protection", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
initialCount, err := suite.UserRepo.Count()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get initial user count: %v", err)
|
||||
}
|
||||
|
||||
maliciousUser := &database.User{
|
||||
Username: "'; DROP TABLE users; --",
|
||||
Email: "malicious@example.com",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err = suite.UserRepo.Create(maliciousUser)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user with malicious username: %v", err)
|
||||
}
|
||||
|
||||
finalCount, err := suite.UserRepo.Count()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get final user count: %v", err)
|
||||
}
|
||||
if finalCount != initialCount+1 {
|
||||
t.Errorf("Expected user count to increase by 1, got %d -> %d", initialCount, finalCount)
|
||||
}
|
||||
|
||||
retrieved, err := suite.UserRepo.GetByUsername("'; DROP TABLE users; --")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve user with malicious username: %v", err)
|
||||
}
|
||||
if retrieved.Username != "'; DROP TABLE users; --" {
|
||||
t.Errorf("Expected malicious username to be stored as-is")
|
||||
}
|
||||
|
||||
users, err := suite.UserRepo.GetAll(10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get users after SQL injection test: %v", err)
|
||||
}
|
||||
if len(users) == 0 {
|
||||
t.Error("Users table appears to have been dropped")
|
||||
}
|
||||
|
||||
var tableName string
|
||||
err = suite.DB.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").Scan(&tableName).Error
|
||||
if err != nil || tableName != "users" {
|
||||
t.Error("Users table should still exist after SQL injection attempt")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Security_Input_Validation", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
longString := string(make([]byte, 10000))
|
||||
for i := range longString {
|
||||
longString = longString[:i] + "a" + longString[i+1:]
|
||||
}
|
||||
|
||||
user := &database.User{
|
||||
Username: longString,
|
||||
Email: "long@example.com",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user with long username: %v", err)
|
||||
}
|
||||
|
||||
specialUser := &database.User{
|
||||
Username: "user<script>alert('xss')</script>",
|
||||
Email: "special@example.com",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err = suite.UserRepo.Create(specialUser)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user with special characters: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Data_Consistency_Cross_Repository", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "consistency_user",
|
||||
Email: "consistency@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Consistency Test Post",
|
||||
URL: "https://example.com/consistency",
|
||||
Content: "Consistency test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err = suite.PostRepo.Create(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post: %v", err)
|
||||
}
|
||||
|
||||
voters := make([]*database.User, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
voter := &database.User{
|
||||
Username: fmt.Sprintf("voter_%d", i),
|
||||
Email: fmt.Sprintf("voter%d@example.com", i),
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(voter)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create voter %d: %v", i, err)
|
||||
}
|
||||
voters[i] = voter
|
||||
}
|
||||
|
||||
for i, voter := range voters {
|
||||
voteType := database.VoteUp
|
||||
if i%2 == 0 {
|
||||
voteType = database.VoteDown
|
||||
}
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &voter.ID,
|
||||
PostID: post.ID,
|
||||
Type: voteType,
|
||||
}
|
||||
err := suite.VoteRepo.Create(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vote %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
votes, err := suite.VoteRepo.GetByPostID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get votes: %v", err)
|
||||
}
|
||||
|
||||
var upVotes, downVotes int64
|
||||
for _, vote := range votes {
|
||||
if vote.Type == database.VoteUp {
|
||||
upVotes++
|
||||
} else if vote.Type == database.VoteDown {
|
||||
downVotes++
|
||||
}
|
||||
}
|
||||
|
||||
expectedScore := int(upVotes - downVotes)
|
||||
post.Score = expectedScore
|
||||
err = suite.PostRepo.Update(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update post score: %v", err)
|
||||
}
|
||||
|
||||
updatedPost, err := suite.PostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve updated post: %v", err)
|
||||
}
|
||||
if updatedPost.Score != expectedScore {
|
||||
t.Errorf("Expected post score %d, got %d", expectedScore, updatedPost.Score)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Edge_Cases_Invalid_Data", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "",
|
||||
Email: "empty@example.com",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err == nil {
|
||||
t.Error("Expected error for empty username")
|
||||
}
|
||||
|
||||
user = &database.User{
|
||||
Username: "invalid_email",
|
||||
Email: "not-an-email",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err = suite.UserRepo.Create(user)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid email format")
|
||||
}
|
||||
|
||||
user1 := &database.User{
|
||||
Username: "duplicate",
|
||||
Email: "duplicate1@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err = suite.UserRepo.Create(user1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create first user: %v", err)
|
||||
}
|
||||
user2 := &database.User{
|
||||
Username: "duplicate",
|
||||
Email: "duplicate2@example.com",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err = suite.UserRepo.Create(user2)
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate username")
|
||||
}
|
||||
|
||||
user3 := &database.User{
|
||||
Username: "duplicate_email",
|
||||
Email: "duplicate1@example.com",
|
||||
Password: hashPassword("password"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err = suite.UserRepo.Create(user3)
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate email")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Edge_Cases_Concurrent_Conflicts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "conflict_user",
|
||||
Email: "conflict@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Conflict Test Post",
|
||||
URL: "https://example.com/conflict",
|
||||
Content: "Conflict test content",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err = suite.PostRepo.Create(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post: %v", err)
|
||||
}
|
||||
|
||||
vote1 := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err = suite.VoteRepo.Create(vote1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create first vote: %v", err)
|
||||
}
|
||||
|
||||
vote2 := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteDown,
|
||||
}
|
||||
err = suite.VoteRepo.Create(vote2)
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate vote")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Transaction_Rollback_On_Error", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "transaction_user",
|
||||
Email: "transaction@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
tx := suite.DB.Begin()
|
||||
defer tx.Rollback()
|
||||
|
||||
post := &database.Post{
|
||||
Title: "Transaction Test Post",
|
||||
URL: "https://example.com/transaction",
|
||||
Content: "This is a transaction test post",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err = tx.Create(post).Error
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post in transaction: %v", err)
|
||||
}
|
||||
|
||||
var postInTx database.Post
|
||||
err = tx.First(&postInTx, post.ID).Error
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve post in transaction: %v", err)
|
||||
}
|
||||
|
||||
tx.Rollback()
|
||||
|
||||
var postAfterRollback database.Post
|
||||
err = suite.DB.First(&postAfterRollback, post.ID).Error
|
||||
if err == nil {
|
||||
t.Error("Expected post to not exist after transaction rollback")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cascading_Delete_User_With_Posts", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "cascade_user",
|
||||
Email: "cascade@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
post1 := &database.Post{
|
||||
Title: "Post 1",
|
||||
URL: "https://example.com/1",
|
||||
Content: "Content 1",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
post2 := &database.Post{
|
||||
Title: "Post 2",
|
||||
URL: "https://example.com/2",
|
||||
Content: "Content 2",
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err = suite.PostRepo.Create(post1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post1: %v", err)
|
||||
}
|
||||
err = suite.PostRepo.Create(post2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post2: %v", err)
|
||||
}
|
||||
|
||||
vote := &database.Vote{
|
||||
UserID: &user.ID,
|
||||
PostID: post1.ID,
|
||||
Type: database.VoteUp,
|
||||
}
|
||||
err = suite.VoteRepo.Create(vote)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vote: %v", err)
|
||||
}
|
||||
|
||||
err = suite.UserRepo.Delete(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete user: %v", err)
|
||||
}
|
||||
|
||||
_, err = suite.UserRepo.GetByID(user.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected user to be deleted")
|
||||
}
|
||||
|
||||
posts, err := suite.PostRepo.GetByUserID(user.ID, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get posts: %v", err)
|
||||
}
|
||||
if len(posts) > 0 {
|
||||
t.Errorf("Expected posts to be deleted or orphaned, found %d posts", len(posts))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Search_Functionality", func(t *testing.T) {
|
||||
suite.Reset()
|
||||
|
||||
user := &database.User{
|
||||
Username: "search_user",
|
||||
Email: "search@example.com",
|
||||
Password: hashPassword("SecurePass123!"),
|
||||
EmailVerified: true,
|
||||
}
|
||||
err := suite.UserRepo.Create(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
posts := []struct {
|
||||
title string
|
||||
content string
|
||||
}{
|
||||
{"Go Programming", "This post is about Go programming language"},
|
||||
{"Python Tutorial", "Learn Python programming with this tutorial"},
|
||||
{"Database Design", "Best practices for database design"},
|
||||
{"Web Development", "Modern web development techniques"},
|
||||
}
|
||||
|
||||
for i, p := range posts {
|
||||
post := &database.Post{
|
||||
Title: p.title,
|
||||
URL: fmt.Sprintf("https://example.com/post-%d", i),
|
||||
Content: p.content,
|
||||
AuthorID: &user.ID,
|
||||
}
|
||||
err := suite.PostRepo.Create(post)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create post %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
results, err := suite.PostRepo.Search("Go", 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to search posts: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Error("Expected to find posts containing 'Go'")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, result := range results {
|
||||
if result.Title == "Go Programming" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Expected to find 'Go Programming' post in search results")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func hashPassword(password string) string {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to hash password: %v", err))
|
||||
}
|
||||
return string(hashed)
|
||||
}
|
||||
224
internal/integration/router_integration_test.go
Normal file
224
internal/integration/router_integration_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_Router_FullMiddlewareChain(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("SecurityHeaders_Present", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
headers := []string{
|
||||
"X-Content-Type-Options",
|
||||
"X-Frame-Options",
|
||||
"X-XSS-Protection",
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
if rec.Header().Get(header) == "" {
|
||||
t.Errorf("Expected header %s to be present", header)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CORS_Headers_Present", func(t *testing.T) {
|
||||
req := httptest.NewRequest("OPTIONS", "/api/posts", nil)
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("Access-Control-Allow-Origin") == "" {
|
||||
t.Error("Expected CORS headers to be present")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logging_Middleware_Executes", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == 0 {
|
||||
t.Error("Expected logging middleware to execute")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RequestSizeLimit_Enforced", func(t *testing.T) {
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "size_limit_user", "size_limit@example.com")
|
||||
largeBody := strings.Repeat("a", 10*1024*1024)
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBufferString(largeBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusRequestEntityTooLarge && rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 413 or 400 for oversized request, got %d. Body: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DBMonitoring_Active", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var response map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&response); err == nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["database_stats"]; !exists {
|
||||
t.Error("Expected database_stats in health response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Metrics_Middleware_Executes", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["database"]; !exists {
|
||||
t.Error("Expected database metrics in response")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("StaticFiles_Served", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/robots.txt", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
if !strings.Contains(rec.Body.String(), "User-agent") {
|
||||
t.Error("Expected robots.txt content")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("API_Routes_Accessible", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("Health_Endpoint_Accessible", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if success, ok := response["success"].(bool); !ok || !success {
|
||||
t.Error("Expected success=true in health response")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Middleware_Order_Correct", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("X-Content-Type-Options") == "" {
|
||||
t.Error("Security headers should be applied before response")
|
||||
}
|
||||
|
||||
if rec.Code == 0 {
|
||||
t.Error("Response should have status code")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Compression_Middleware_Active", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
req.Header.Set("Accept-Encoding", "gzip")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Header().Get("Content-Encoding") == "" {
|
||||
t.Log("Compression may not be applied to small responses")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cache_Middleware_Active", func(t *testing.T) {
|
||||
req1 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
req2 := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
if rec1.Code != rec2.Code {
|
||||
t.Error("Cached responses should have same status")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Auth_Middleware_Integration", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "auth_middleware_user", "auth_middleware@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("RateLimit_Middleware_Integration", func(t *testing.T) {
|
||||
rateLimitCtx := setupTestContext(t)
|
||||
rateLimitRouter := rateLimitCtx.Router
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBufferString(`{"username":"test","password":"test"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
rateLimitRouter.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBufferString(`{"username":"test","password":"test"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
rateLimitRouter.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusTooManyRequests {
|
||||
t.Log("Rate limiting is working")
|
||||
}
|
||||
})
|
||||
}
|
||||
832
internal/integration/services_integration_test.go
Normal file
832
internal/integration/services_integration_test.go
Normal file
@@ -0,0 +1,832 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"goyco/internal/database"
|
||||
"goyco/internal/repositories"
|
||||
"goyco/internal/services"
|
||||
"goyco/internal/testutils"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func TestIntegration_Services(t *testing.T) {
|
||||
suite := testutils.NewServiceSuite(t)
|
||||
|
||||
authService, err := services.NewAuthFacadeForTest(testutils.AppTestConfig, suite.UserRepo, suite.PostRepo, suite.DeletionRepo, suite.RefreshTokenRepo, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
voteService := services.NewVoteService(suite.VoteRepo, suite.PostRepo, suite.DB)
|
||||
emailSender := suite.EmailSender
|
||||
userRepo := suite.UserRepo
|
||||
deletionRepo := suite.DeletionRepo
|
||||
postRepo := suite.PostRepo
|
||||
titleFetcher := suite.TitleFetcher
|
||||
|
||||
t.Run("Auth_Complete_User_Lifecycle", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
registerResult, err := authService.Register("lifecycle_user", "lifecycle@example.com", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register user: %v", err)
|
||||
}
|
||||
|
||||
if registerResult.User.Username != "lifecycle_user" {
|
||||
t.Errorf("Expected username 'lifecycle_user', got '%s'", registerResult.User.Username)
|
||||
}
|
||||
|
||||
verificationToken := setupVerificationTokenForTest(t, emailSender, userRepo, "lifecycle_user")
|
||||
|
||||
_, err = authService.ConfirmEmail(verificationToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to confirm email: %v", err)
|
||||
}
|
||||
|
||||
loginResult, err := authService.Login("lifecycle_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login user: %v", err)
|
||||
}
|
||||
|
||||
if loginResult.User.Username != "lifecycle_user" {
|
||||
t.Errorf("Expected username 'lifecycle_user', got '%s'", loginResult.User.Username)
|
||||
}
|
||||
|
||||
updateResult, err := authService.UpdateUsername(loginResult.User.ID, "updated_lifecycle_user")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update username: %v", err)
|
||||
}
|
||||
|
||||
if updateResult.Username != "updated_lifecycle_user" {
|
||||
t.Errorf("Expected updated username, got '%s'", updateResult.Username)
|
||||
}
|
||||
|
||||
emailSender.Reset()
|
||||
emailResult, err := authService.UpdateEmail(loginResult.User.ID, "updated@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update email: %v", err)
|
||||
}
|
||||
|
||||
if emailResult.Email != "updated@example.com" {
|
||||
t.Errorf("Expected updated email, got '%s'", emailResult.Email)
|
||||
}
|
||||
|
||||
updatedToken := setupVerificationTokenForTest(t, emailSender, userRepo, "updated_lifecycle_user")
|
||||
|
||||
_, err = authService.ConfirmEmail(updatedToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to confirm updated email: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.UpdatePassword(loginResult.User.ID, "SecurePass123!", "NewSecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update password: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.Login("updated_lifecycle_user", "NewSecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login with new password: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Auth_Security_Validation", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
_, err := authService.Register("weak_user", "weak@example.com", "123")
|
||||
if err == nil {
|
||||
t.Error("Expected error for weak password")
|
||||
}
|
||||
|
||||
_, err = authService.Register("invalid_user", "not-an-email", "SecurePass123!")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid email")
|
||||
}
|
||||
|
||||
_, err = authService.Register("duplicate_user", "duplicate1@example.com", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register first user: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.Register("duplicate_user", "duplicate2@example.com", "SecurePass123!")
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate username")
|
||||
}
|
||||
|
||||
_, err = authService.Register("another_user", "duplicate1@example.com", "SecurePass123!")
|
||||
if err == nil {
|
||||
t.Error("Expected error for duplicate email")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Auth_Account_Deletion_Workflow", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
registerResult, err := authService.Register("deletion_user", "deletion@example.com", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register user: %v", err)
|
||||
}
|
||||
|
||||
verificationToken := setupVerificationTokenForTest(t, emailSender, userRepo, "deletion_user")
|
||||
|
||||
_, err = authService.ConfirmEmail(verificationToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to confirm email: %v", err)
|
||||
}
|
||||
|
||||
err = authService.RequestAccountDeletion(registerResult.User.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to request account deletion: %v", err)
|
||||
}
|
||||
|
||||
deletionToken := setupDeletionTokenForTest(t, emailSender, deletionRepo, registerResult.User.ID)
|
||||
|
||||
err = authService.ConfirmAccountDeletion(deletionToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to confirm account deletion: %v", err)
|
||||
}
|
||||
|
||||
if err := authService.ConfirmAccountDeletion(deletionToken); !errors.Is(err, services.ErrInvalidDeletionToken) {
|
||||
t.Fatalf("Expected token reuse to return ErrInvalidDeletionToken, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Auth_Locked_User_Session_Invalidation", func(t *testing.T) {
|
||||
user := &database.User{
|
||||
Username: "locked_user",
|
||||
Email: "locked@example.com",
|
||||
Password: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
||||
EmailVerified: true,
|
||||
}
|
||||
if err := userRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := services.TokenClaims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
SessionVersion: user.SessionVersion,
|
||||
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.ID),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(testutils.AppTestConfig.JWT.Secret))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
userID, err := authService.VerifyToken(tokenString)
|
||||
if err != nil {
|
||||
t.Fatalf("Token should be valid before locking: %v", err)
|
||||
}
|
||||
if userID != user.ID {
|
||||
t.Fatalf("Expected user ID %d, got %d", user.ID, userID)
|
||||
}
|
||||
|
||||
if err := userRepo.Lock(user.ID); err != nil {
|
||||
t.Fatalf("Failed to lock user: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.VerifyToken(tokenString)
|
||||
if !errors.Is(err, services.ErrAccountLocked) {
|
||||
t.Fatalf("Expected ErrAccountLocked, got %v", err)
|
||||
}
|
||||
|
||||
if err := userRepo.Unlock(user.ID); err != nil {
|
||||
t.Fatalf("Failed to unlock user: %v", err)
|
||||
}
|
||||
|
||||
userID, err = authService.VerifyToken(tokenString)
|
||||
if err != nil {
|
||||
t.Fatalf("Token should be valid after unlock: %v", err)
|
||||
}
|
||||
if userID != user.ID {
|
||||
t.Fatalf("Expected user ID %d, got %d", user.ID, userID)
|
||||
}
|
||||
|
||||
userRepo.HardDelete(user.ID)
|
||||
})
|
||||
|
||||
t.Run("Auth_Password_Change_Session_Invalidation", func(t *testing.T) {
|
||||
user := &database.User{
|
||||
Username: "password_test_user",
|
||||
Email: "password_test@example.com",
|
||||
Password: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
||||
EmailVerified: true,
|
||||
SessionVersion: 1,
|
||||
}
|
||||
if err := userRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := services.TokenClaims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
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.ID),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(testutils.AppTestConfig.JWT.Secret))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
userID, err := authService.VerifyToken(tokenString)
|
||||
if err != nil {
|
||||
t.Fatalf("Token should be valid before password change: %v", err)
|
||||
}
|
||||
if userID != user.ID {
|
||||
t.Fatalf("Expected user ID %d, got %d", user.ID, userID)
|
||||
}
|
||||
|
||||
if err := authService.InvalidateAllSessions(user.ID); err != nil {
|
||||
t.Fatalf("Failed to invalidate sessions: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.VerifyToken(tokenString)
|
||||
if err == nil {
|
||||
t.Fatalf("Token should be invalid after session invalidation")
|
||||
}
|
||||
|
||||
userRepo.HardDelete(user.ID)
|
||||
})
|
||||
|
||||
t.Run("Auth_Email_Change_Verification_Template", func(t *testing.T) {
|
||||
user := &database.User{
|
||||
Username: "email_change_user",
|
||||
Email: "old@example.com",
|
||||
Password: "$2a$10$abcdefghijklmnopqrstuvwxyz",
|
||||
EmailVerified: true,
|
||||
SessionVersion: 1,
|
||||
}
|
||||
if err := userRepo.Create(user); err != nil {
|
||||
t.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
emailService, err := services.NewEmailService(testutils.AppTestConfig, suite.EmailSender)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create email service: %v", err)
|
||||
}
|
||||
verificationURL := "https://example.com/confirm?token=test123"
|
||||
body := emailService.GenerateEmailChangeVerificationEmailBody(user.Username, verificationURL)
|
||||
|
||||
if !strings.Contains(body, "Confirm your new email address") {
|
||||
t.Error("Email should contain 'Confirm your new email address'")
|
||||
}
|
||||
if !strings.Contains(body, "You've requested to change your email address") {
|
||||
t.Error("Email should contain email change specific message")
|
||||
}
|
||||
if !strings.Contains(body, "Confirm New Email Address") {
|
||||
t.Error("Email should contain 'Confirm New Email Address' button text")
|
||||
}
|
||||
if !strings.Contains(body, "your new email address will be active") {
|
||||
t.Error("Email should mention that new email will be active")
|
||||
}
|
||||
if !strings.Contains(body, "If you didn't request this email change") {
|
||||
t.Error("Email should contain security warning about email change")
|
||||
}
|
||||
|
||||
userRepo.HardDelete(user.ID)
|
||||
})
|
||||
|
||||
t.Run("Vote_Service_Complete_Workflow", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "vote_user", "vote@example.com")
|
||||
post := testutils.CreatePostWithRepo(t, postRepo, user.ID, "Vote Test Post", "https://example.com/vote-test")
|
||||
|
||||
voteRequest := services.VoteRequest{
|
||||
UserID: user.ID,
|
||||
PostID: post.ID,
|
||||
Type: "up",
|
||||
}
|
||||
voteResult, err := voteService.CastVote(voteRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to cast vote: %v", err)
|
||||
}
|
||||
|
||||
if voteResult.Type != database.VoteUp {
|
||||
t.Errorf("Expected vote type 'up', got '%v'", voteResult.Type)
|
||||
}
|
||||
|
||||
userVote, err := voteService.GetUserVote(user.ID, post.ID, "127.0.0.1", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user vote: %v", err)
|
||||
}
|
||||
|
||||
if userVote == nil || userVote.Type != database.VoteUp {
|
||||
t.Errorf("Expected user vote type 'up', got '%v'", userVote)
|
||||
}
|
||||
|
||||
votes, err := voteService.GetPostVotes(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get post votes: %v", err)
|
||||
}
|
||||
|
||||
totalVotes := len(votes)
|
||||
if totalVotes != 1 {
|
||||
t.Errorf("Expected 1 vote, got %d", totalVotes)
|
||||
}
|
||||
|
||||
voteRequest = services.VoteRequest{
|
||||
UserID: user.ID,
|
||||
PostID: post.ID,
|
||||
Type: "down",
|
||||
}
|
||||
voteResult, err = voteService.CastVote(voteRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to change vote: %v", err)
|
||||
}
|
||||
|
||||
if voteResult.Type != database.VoteDown {
|
||||
t.Errorf("Expected vote type 'down', got '%v'", voteResult.Type)
|
||||
}
|
||||
|
||||
removeRequest := services.VoteRequest{
|
||||
UserID: user.ID,
|
||||
PostID: post.ID,
|
||||
Type: database.VoteNone,
|
||||
}
|
||||
_, err = voteService.CastVote(removeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove vote: %v", err)
|
||||
}
|
||||
|
||||
_, err = voteService.GetUserVote(user.ID, post.ID, "127.0.0.1", "test")
|
||||
if err == nil {
|
||||
t.Error("Expected error when getting removed vote")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Vote_Service_Concurrent_Operations", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
users := make([]*database.User, 5)
|
||||
for i := range 5 {
|
||||
users[i] = createTestUserWithAuth(authService, emailSender, suite.UserRepo, fmt.Sprintf("concurrent_user_%d", i), fmt.Sprintf("concurrent%d@example.com", i))
|
||||
}
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, postRepo, users[0].ID, "Concurrent Vote Post", "https://example.com/concurrent-vote")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, len(users))
|
||||
|
||||
for i, user := range users {
|
||||
wg.Add(1)
|
||||
go func(index int, u *database.User) {
|
||||
defer wg.Done()
|
||||
|
||||
voteType := database.VoteUp
|
||||
if index%2 == 0 {
|
||||
voteType = database.VoteDown
|
||||
}
|
||||
|
||||
voteRequest := services.VoteRequest{
|
||||
UserID: u.ID,
|
||||
PostID: post.ID,
|
||||
Type: voteType,
|
||||
}
|
||||
_, err := voteService.CastVote(voteRequest)
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("failed to cast vote for user %d: %v", index, err)
|
||||
}
|
||||
}(i, user)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
var errs []error
|
||||
for err := range errors {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("concurrent vote failures: %v", errs)
|
||||
}
|
||||
|
||||
votes, err := voteService.GetPostVotes(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get post votes: %v", err)
|
||||
}
|
||||
|
||||
totalVotes := len(votes)
|
||||
if totalVotes != 5 {
|
||||
t.Errorf("Expected 5 votes, got %d", totalVotes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Title_Fetcher_Functionality", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
titleFetcher.SetTitle("Mock Title")
|
||||
title, err := titleFetcher.FetchTitle(context.Background(), "https://example.com/test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to fetch title: %v", err)
|
||||
}
|
||||
|
||||
if title != "Mock Title" {
|
||||
t.Errorf("Expected title 'Mock Title', got '%s'", title)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Error_Handling_Invalid_Operations", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "error_user", "error@example.com")
|
||||
voteRequest := services.VoteRequest{
|
||||
UserID: user.ID,
|
||||
PostID: 99999,
|
||||
Type: "up",
|
||||
}
|
||||
_, err := voteService.CastVote(voteRequest)
|
||||
if err == nil {
|
||||
t.Error("Expected error when voting on non-existent post")
|
||||
}
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, postRepo, user.ID, "Error Test Post", "https://example.com/error-test")
|
||||
voteRequest = services.VoteRequest{
|
||||
UserID: 99999,
|
||||
PostID: post.ID,
|
||||
Type: "up",
|
||||
}
|
||||
_, err = voteService.CastVote(voteRequest)
|
||||
if err == nil {
|
||||
t.Error("Expected error when voting with non-existent user")
|
||||
}
|
||||
|
||||
voteRequest = services.VoteRequest{
|
||||
UserID: user.ID,
|
||||
PostID: post.ID,
|
||||
Type: "invalid",
|
||||
}
|
||||
_, err = voteService.CastVote(voteRequest)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid vote type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Data_Consistency_Cross_Services", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "consistency_user", "consistency@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, postRepo, user.ID, "Consistency Test Post", "https://example.com/consistency")
|
||||
|
||||
voters := make([]*database.User, 3)
|
||||
for i := range 3 {
|
||||
voters[i] = createTestUserWithAuth(authService, emailSender, suite.UserRepo, fmt.Sprintf("voter_%d", i), fmt.Sprintf("voter%d@example.com", i))
|
||||
}
|
||||
|
||||
for i, voter := range voters {
|
||||
voteType := database.VoteUp
|
||||
if i%2 == 0 {
|
||||
voteType = database.VoteDown
|
||||
}
|
||||
voteRequest := services.VoteRequest{
|
||||
UserID: voter.ID,
|
||||
PostID: post.ID,
|
||||
Type: voteType,
|
||||
}
|
||||
_, err := voteService.CastVote(voteRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to cast vote %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
votes, err := voteService.GetPostVotes(post.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get post votes: %v", err)
|
||||
}
|
||||
|
||||
totalVotes := len(votes)
|
||||
if totalVotes != 3 {
|
||||
t.Errorf("Expected 3 votes, got %d", totalVotes)
|
||||
}
|
||||
|
||||
for i, voter := range voters {
|
||||
userVote, err := voteService.GetUserVote(voter.ID, post.ID, "127.0.0.1", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user vote %d: %v", i, err)
|
||||
}
|
||||
|
||||
expectedType := database.VoteUp
|
||||
if i%2 == 0 {
|
||||
expectedType = database.VoteDown
|
||||
}
|
||||
|
||||
if userVote.Type != expectedType {
|
||||
t.Errorf("Expected vote type '%v' for user %d, got '%v'", expectedType, i, userVote.Type)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmailSender_Integration", func(t *testing.T) {
|
||||
sender := testutils.GetSMTPSenderFromEnv(t)
|
||||
|
||||
recipient := os.Getenv("SMTP_TEST_RECIPIENT")
|
||||
if strings.TrimSpace(recipient) == "" {
|
||||
recipient = sender.From
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("Test Subject %d", time.Now().UnixNano())
|
||||
body := fmt.Sprintf("Test Body sent at %s", time.Now().Format(time.RFC3339))
|
||||
|
||||
err := sender.Send(recipient, subject, body)
|
||||
if err != nil {
|
||||
t.Errorf("Send failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmailSender_HTML_Email", func(t *testing.T) {
|
||||
sender := testutils.GetSMTPSenderFromEnv(t)
|
||||
|
||||
recipient := os.Getenv("SMTP_TEST_RECIPIENT")
|
||||
if strings.TrimSpace(recipient) == "" {
|
||||
recipient = sender.From
|
||||
}
|
||||
|
||||
htmlBody := "<html><body><h1>Test</h1><p>This is a test email.</p></body></html>"
|
||||
err := sender.Send(recipient, "HTML Test Subject", htmlBody)
|
||||
if err != nil {
|
||||
t.Errorf("Send failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmailSender_Async_Email", func(t *testing.T) {
|
||||
sender := testutils.GetSMTPSenderFromEnv(t)
|
||||
|
||||
recipient := os.Getenv("SMTP_TEST_RECIPIENT")
|
||||
if strings.TrimSpace(recipient) == "" {
|
||||
recipient = sender.From
|
||||
}
|
||||
|
||||
asyncBody := fmt.Sprintf("Async Test Body sent at %s", time.Now().Format(time.RFC3339))
|
||||
err := sender.Send(recipient, "Async Test Subject", asyncBody)
|
||||
if err != nil {
|
||||
t.Errorf("Send failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Complete_Workflow", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "refresh_user", "refresh@example.com")
|
||||
|
||||
loginResult, err := authService.Login("refresh_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %v", err)
|
||||
}
|
||||
|
||||
if loginResult.RefreshToken == "" {
|
||||
t.Fatal("Login should return a refresh token")
|
||||
}
|
||||
|
||||
newAccessToken, err := authService.RefreshAccessToken(loginResult.RefreshToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to refresh access token: %v", err)
|
||||
}
|
||||
|
||||
if newAccessToken.AccessToken == "" {
|
||||
t.Fatal("Refresh should return a new access token")
|
||||
}
|
||||
|
||||
if newAccessToken.AccessToken == loginResult.AccessToken {
|
||||
t.Error("New access token should be different from original")
|
||||
}
|
||||
|
||||
userID, err := authService.VerifyToken(newAccessToken.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("New access token should be valid: %v", err)
|
||||
}
|
||||
|
||||
if userID != user.ID {
|
||||
t.Errorf("Expected user ID %d, got %d", user.ID, userID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Expiration", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
createTestUserWithAuth(authService, emailSender, suite.UserRepo, "expire_user", "expire@example.com")
|
||||
|
||||
loginResult, err := authService.Login("expire_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %v", err)
|
||||
}
|
||||
|
||||
refreshToken, err := suite.RefreshTokenRepo.GetByTokenHash(testutils.HashVerificationToken(loginResult.RefreshToken))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get refresh token: %v", err)
|
||||
}
|
||||
|
||||
refreshToken.ExpiresAt = time.Now().Add(-1 * time.Hour)
|
||||
if err := suite.DB.Model(refreshToken).Update("expires_at", refreshToken.ExpiresAt).Error; err != nil {
|
||||
t.Fatalf("Failed to update token expiration: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.RefreshAccessToken(loginResult.RefreshToken)
|
||||
if err == nil {
|
||||
t.Error("Expected error for expired refresh token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Revocation", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
createTestUserWithAuth(authService, emailSender, suite.UserRepo, "revoke_user", "revoke@example.com")
|
||||
|
||||
loginResult, err := authService.Login("revoke_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %v", err)
|
||||
}
|
||||
|
||||
err = authService.RevokeRefreshToken(loginResult.RefreshToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to revoke refresh token: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.RefreshAccessToken(loginResult.RefreshToken)
|
||||
if err == nil {
|
||||
t.Error("Expected error for revoked refresh token")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Multiple_Tokens", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "multi_token_user", "multi@example.com")
|
||||
|
||||
login1, err := authService.Login("multi_token_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed first login: %v", err)
|
||||
}
|
||||
|
||||
login2, err := authService.Login("multi_token_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed second login: %v", err)
|
||||
}
|
||||
|
||||
if login1.RefreshToken == login2.RefreshToken {
|
||||
t.Error("Each login should generate a unique refresh token")
|
||||
}
|
||||
|
||||
accessToken1, err := authService.RefreshAccessToken(login1.RefreshToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to refresh with first token: %v", err)
|
||||
}
|
||||
|
||||
accessToken2, err := authService.RefreshAccessToken(login2.RefreshToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to refresh with second token: %v", err)
|
||||
}
|
||||
|
||||
if accessToken1.AccessToken == accessToken2.AccessToken {
|
||||
t.Error("Different refresh tokens should generate different access tokens")
|
||||
}
|
||||
|
||||
userID1, err := authService.VerifyToken(accessToken1.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("First access token should be valid: %v", err)
|
||||
}
|
||||
|
||||
userID2, err := authService.VerifyToken(accessToken2.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Second access token should be valid: %v", err)
|
||||
}
|
||||
|
||||
if userID1 != user.ID || userID2 != user.ID {
|
||||
t.Error("Both tokens should belong to the same user")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Revoke_All", func(t *testing.T) {
|
||||
emailSender.Reset()
|
||||
user := createTestUserWithAuth(authService, emailSender, suite.UserRepo, "revoke_all_user", "revoke_all@example.com")
|
||||
|
||||
login1, err := authService.Login("revoke_all_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed first login: %v", err)
|
||||
}
|
||||
|
||||
login2, err := authService.Login("revoke_all_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed second login: %v", err)
|
||||
}
|
||||
|
||||
err = authService.RevokeAllUserTokens(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to revoke all tokens: %v", err)
|
||||
}
|
||||
|
||||
_, err = authService.RefreshAccessToken(login1.RefreshToken)
|
||||
if err == nil {
|
||||
t.Error("Expected error for revoked refresh token")
|
||||
}
|
||||
|
||||
_, err = authService.RefreshAccessToken(login2.RefreshToken)
|
||||
if err == nil {
|
||||
t.Error("Expected error for revoked refresh token")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func createTestUserWithAuth(authService interface {
|
||||
Register(username, email, password string) (*services.RegistrationResult, error)
|
||||
ConfirmEmail(token string) (*database.User, error)
|
||||
}, emailSender interface {
|
||||
Reset()
|
||||
VerificationToken() string
|
||||
}, userRepo repositories.UserRepository, username, email string) *database.User {
|
||||
emailSender.Reset()
|
||||
|
||||
_, err := authService.Register(username, email, "SecurePass123!")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to register user: %v", err))
|
||||
}
|
||||
|
||||
verificationToken := emailSender.VerificationToken()
|
||||
if verificationToken == "" {
|
||||
panic("Failed to capture verification token during test setup")
|
||||
}
|
||||
|
||||
hashedToken := testutils.HashVerificationToken(verificationToken)
|
||||
|
||||
user, err := userRepo.GetByUsername(username)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to get user: %v", err))
|
||||
}
|
||||
user.EmailVerificationToken = hashedToken
|
||||
if err := userRepo.Update(user); err != nil {
|
||||
panic(fmt.Sprintf("Failed to update user with hashed token: %v", err))
|
||||
}
|
||||
|
||||
confirmResult, err := authService.ConfirmEmail(verificationToken)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to confirm email: %v", err))
|
||||
}
|
||||
|
||||
return confirmResult
|
||||
}
|
||||
|
||||
func setupVerificationTokenForTest(t *testing.T, emailSender *testutils.MockEmailSender, userRepo repositories.UserRepository, username string) string {
|
||||
t.Helper()
|
||||
|
||||
verificationToken := emailSender.VerificationToken()
|
||||
if verificationToken == "" {
|
||||
t.Fatal("Expected verification token to be generated")
|
||||
}
|
||||
|
||||
hashedToken := testutils.HashVerificationToken(verificationToken)
|
||||
|
||||
user, err := userRepo.GetByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
user.EmailVerificationToken = hashedToken
|
||||
if err := userRepo.Update(user); err != nil {
|
||||
t.Fatalf("Failed to update user with hashed token: %v", err)
|
||||
}
|
||||
|
||||
return verificationToken
|
||||
}
|
||||
|
||||
func setupDeletionTokenForTest(t *testing.T, emailSender *testutils.MockEmailSender, deletionRepo repositories.AccountDeletionRepository, userID uint) string {
|
||||
t.Helper()
|
||||
|
||||
deletionToken := emailSender.DeletionToken()
|
||||
if deletionToken == "" {
|
||||
t.Fatal("Expected deletion token to be generated")
|
||||
}
|
||||
|
||||
hashedToken := testutils.HashVerificationToken(deletionToken)
|
||||
|
||||
if err := deletionRepo.DeleteByUserID(userID); err != nil {
|
||||
t.Fatalf("Cannot delete user %d", userID)
|
||||
}
|
||||
|
||||
req := &database.AccountDeletionRequest{
|
||||
UserID: userID,
|
||||
TokenHash: hashedToken,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
if err := deletionRepo.Create(req); err != nil {
|
||||
t.Fatalf("Failed to create account deletion request: %v", err)
|
||||
}
|
||||
|
||||
return deletionToken
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/middleware"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func TestIntegration_SessionManagement(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Session_Invalidation_On_Password_Change", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "session_pass_user", "session_pass@example.com")
|
||||
|
||||
req1 := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req1.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req1 = testutils.WithUserContext(req1, middleware.UserIDKey, user.User.ID)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
assertStatus(t, rec1, http.StatusOK)
|
||||
|
||||
reqBody := map[string]string{
|
||||
"current_password": "SecurePass123!",
|
||||
"new_password": "NewSecurePass123!",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req2 := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(body))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req2 = testutils.WithUserContext(req2, middleware.UserIDKey, user.User.ID)
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
assertStatus(t, rec2, http.StatusOK)
|
||||
|
||||
req3 := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req3.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req3 = testutils.WithUserContext(req3, middleware.UserIDKey, user.User.ID)
|
||||
rec3 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec3, req3)
|
||||
|
||||
assertErrorResponse(t, rec3, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Session_Invalidation_On_Account_Lock", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "session_lock_user", "session_lock@example.com")
|
||||
|
||||
req1 := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req1.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req1 = testutils.WithUserContext(req1, middleware.UserIDKey, user.User.ID)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
assertStatus(t, rec1, http.StatusOK)
|
||||
|
||||
if err := ctx.Suite.UserRepo.Lock(user.User.ID); err != nil {
|
||||
t.Fatalf("Failed to lock user: %v", err)
|
||||
}
|
||||
|
||||
req2 := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req2.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req2 = testutils.WithUserContext(req2, middleware.UserIDKey, user.User.ID)
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
assertErrorResponse(t, rec2, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Refresh_Token_Revocation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "refresh_revoke_user", "refresh_revoke@example.com")
|
||||
|
||||
loginResult, err := ctx.AuthService.Login("refresh_revoke_user", "SecurePass123!")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login: %v", err)
|
||||
}
|
||||
|
||||
if loginResult.RefreshToken == "" {
|
||||
t.Fatal("Expected refresh token")
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"refresh_token": loginResult.RefreshToken,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", "/api/auth/refresh", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
assertStatus(t, rec, http.StatusOK)
|
||||
|
||||
if err := ctx.AuthService.RevokeRefreshToken(loginResult.RefreshToken); err != nil {
|
||||
t.Fatalf("Failed to revoke token: %v", err)
|
||||
}
|
||||
|
||||
req2 := httptest.NewRequest("POST", "/api/auth/refresh", bytes.NewBuffer(body))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
assertErrorResponse(t, rec2, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("Multiple_Sessions_Independent", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user1 := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "multi_session_user1", "multi_session1@example.com")
|
||||
user2 := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "multi_session_user2", "multi_session2@example.com")
|
||||
|
||||
req1 := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req1.Header.Set("Authorization", "Bearer "+user1.Token)
|
||||
req1 = testutils.WithUserContext(req1, middleware.UserIDKey, user1.User.ID)
|
||||
rec1 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec1, req1)
|
||||
|
||||
req2 := httptest.NewRequest("GET", "/api/auth/me", nil)
|
||||
req2.Header.Set("Authorization", "Bearer "+user2.Token)
|
||||
req2 = testutils.WithUserContext(req2, middleware.UserIDKey, user2.User.ID)
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
assertStatus(t, rec1, http.StatusOK)
|
||||
assertStatus(t, rec2, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_AccountDeletion(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Account_Deletion_Complete_Flow", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "del_flow_user", "del_flow@example.com")
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Test Post", "https://example.com")
|
||||
|
||||
reqBody := map[string]string{}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("DELETE", "/api/auth/account", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := response["message"]; !ok {
|
||||
t.Error("Expected message field in response")
|
||||
}
|
||||
|
||||
deletionToken := ctx.Suite.EmailSender.DeletionToken()
|
||||
if deletionToken == "" {
|
||||
t.Fatal("Expected deletion token")
|
||||
}
|
||||
|
||||
confirmBody := map[string]any{
|
||||
"token": deletionToken,
|
||||
}
|
||||
confirmBodyBytes, _ := json.Marshal(confirmBody)
|
||||
confirmReq := httptest.NewRequest("POST", "/api/auth/account/confirm", bytes.NewBuffer(confirmBodyBytes))
|
||||
confirmReq.Header.Set("Content-Type", "application/json")
|
||||
confirmRec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(confirmRec, confirmReq)
|
||||
|
||||
confirmResponse := assertJSONResponse(t, confirmRec, http.StatusOK)
|
||||
if confirmResponse == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := confirmResponse["message"]; !ok {
|
||||
t.Error("Expected message field in confirmation response")
|
||||
}
|
||||
if data, ok := confirmResponse["data"].(map[string]any); ok {
|
||||
if postsDeleted, ok := data["posts_deleted"].(bool); ok && postsDeleted {
|
||||
t.Error("Expected posts_deleted to be false when not specified")
|
||||
}
|
||||
}
|
||||
|
||||
_, err := ctx.Suite.UserRepo.GetByID(user.User.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected user to be deleted")
|
||||
}
|
||||
|
||||
retrievedPost, err := ctx.Suite.PostRepo.GetByID(post.ID)
|
||||
if err != nil {
|
||||
t.Fatal("Expected post to still exist after soft delete")
|
||||
}
|
||||
if retrievedPost.AuthorID != nil {
|
||||
t.Error("Expected post author_id to be null after user deletion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Account_Deletion_With_Posts", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "del_posts_user", "del_posts@example.com")
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Deletion Post", "https://example.com/deletion")
|
||||
|
||||
reqBody := map[string]string{}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("DELETE", "/api/auth/account", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := response["message"]; !ok {
|
||||
t.Error("Expected message field in response")
|
||||
}
|
||||
|
||||
deletionToken := ctx.Suite.EmailSender.DeletionToken()
|
||||
if deletionToken == "" {
|
||||
t.Fatal("Expected deletion token")
|
||||
}
|
||||
|
||||
confirmBody := map[string]any{
|
||||
"token": deletionToken,
|
||||
"delete_posts": true,
|
||||
}
|
||||
confirmBodyBytes, _ := json.Marshal(confirmBody)
|
||||
confirmReq := httptest.NewRequest("POST", "/api/auth/account/confirm", bytes.NewBuffer(confirmBodyBytes))
|
||||
confirmReq.Header.Set("Content-Type", "application/json")
|
||||
confirmRec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(confirmRec, confirmReq)
|
||||
|
||||
confirmResponse := assertJSONResponse(t, confirmRec, http.StatusOK)
|
||||
if confirmResponse == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := confirmResponse["message"]; !ok {
|
||||
t.Error("Expected message field in confirmation response")
|
||||
}
|
||||
if data, ok := confirmResponse["data"].(map[string]any); ok {
|
||||
if postsDeleted, ok := data["posts_deleted"].(bool); !ok || !postsDeleted {
|
||||
t.Error("Expected posts_deleted to be true")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected data field with posts_deleted in confirmation response")
|
||||
}
|
||||
|
||||
_, err := ctx.Suite.UserRepo.GetByID(user.User.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected user to be deleted")
|
||||
}
|
||||
|
||||
_, err = ctx.Suite.PostRepo.GetByID(post.ID)
|
||||
if err == nil {
|
||||
t.Error("Expected post to be deleted with user")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_MetricsCollection(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Metrics_Endpoint_Returns_Data", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
response := assertJSONResponse(t, rec, http.StatusOK)
|
||||
if response != nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if _, exists := data["database"]; !exists {
|
||||
t.Error("Expected database metrics")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Metrics_Includes_DB_Stats", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "metrics_user", "metrics@example.com")
|
||||
|
||||
req := httptest.NewRequest("GET", "/metrics", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var response map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&response); err == nil {
|
||||
if data, ok := response["data"].(map[string]any); ok {
|
||||
if dbData, exists := data["database"].(map[string]any); exists {
|
||||
if _, hasQueries := dbData["total_queries"]; !hasQueries {
|
||||
t.Log("Database query metrics may not be available")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_ConcurrentRequests(t *testing.T) {
|
||||
ctx := setupTestContext(t)
|
||||
router := ctx.Router
|
||||
|
||||
t.Run("Concurrent_Post_Creation", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "concurrent_user", "concurrent@example.com")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
postBody := map[string]string{
|
||||
"title": fmt.Sprintf("Concurrent Post %d", index),
|
||||
"url": fmt.Sprintf("https://example.com/concurrent-%d", index),
|
||||
"content": "Concurrent test content",
|
||||
}
|
||||
body, _ := json.Marshal(postBody)
|
||||
req := httptest.NewRequest("POST", "/api/posts", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
errors <- fmt.Errorf("Post %d failed with status %d", index, rec.Code)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
var errs []error
|
||||
for err := range errors {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("Concurrent post creation failed: %v", errs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Concurrent_Vote_Operations", func(t *testing.T) {
|
||||
ctx.Suite.EmailSender.Reset()
|
||||
user := createAuthenticatedUser(t, ctx.AuthService, ctx.Suite.UserRepo, "concurrent_vote_user", "concurrent_vote@example.com")
|
||||
|
||||
post := testutils.CreatePostWithRepo(t, ctx.Suite.PostRepo, user.User.ID, "Concurrent Vote Post", "https://example.com/concurrent-vote")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
voteBody := map[string]string{
|
||||
"type": "up",
|
||||
}
|
||||
body, _ := json.Marshal(voteBody)
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/posts/%d/vote", post.ID), bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+user.Token)
|
||||
req = testutils.WithUserContext(req, middleware.UserIDKey, user.User.ID)
|
||||
req = testutils.WithURLParams(req, map[string]string{"id": fmt.Sprintf("%d", post.ID)})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
errors <- fmt.Errorf("Vote failed with status %d", rec.Code)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
var errs []error
|
||||
for err := range errors {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
t.Logf("Some concurrent votes may have failed (expected): %v", errs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Concurrent_Read_Operations", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 20)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/posts", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
errors <- fmt.Errorf("Read failed with status %d", rec.Code)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
var errs []error
|
||||
for err := range errors {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("Concurrent reads failed: %v", errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user