package testutils import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "net/http" "strings" "sync/atomic" "testing" "time" "goyco/internal/database" "goyco/internal/repositories" "golang.org/x/crypto/bcrypt" ) var loginIPCounter uint64 func GenerateTestIP() string { counter := atomic.AddUint64(&loginIPCounter, 1) octet := 100 + (counter % 155) return fmt.Sprintf("192.168.1.%d", octet) } type TestUser struct { ID uint Username string Email string Password string EmailVerified bool } type TestPost struct { ID uint Title string URL string Content string AuthorID uint } type AuthenticatedClient struct { Client *http.Client Token string RefreshToken string BaseURL string } type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` Data any `json:"data"` } type LoginData struct { Token string `json:"token"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } type LoginResponse struct { Success bool `json:"success"` Message string `json:"message"` Data LoginData `json:"data"` } type PostResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { ID uint `json:"id"` Title string `json:"title"` URL string `json:"url"` Content string `json:"content"` AuthorID uint `json:"author_id"` UpVotes int `json:"up_votes"` DownVotes int `json:"down_votes"` Score int `json:"score"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } `json:"data"` } type PostsListResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Posts []Post `json:"posts"` Count int `json:"count"` Limit int `json:"limit"` Offset int `json:"offset"` } `json:"data"` } type Post struct { ID uint `json:"id"` Title string `json:"title"` URL string `json:"url"` Content string `json:"content"` AuthorID uint `json:"author_id"` UpVotes int `json:"up_votes"` DownVotes int `json:"down_votes"` Score int `json:"score"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Author struct { ID uint `json:"id"` Username string `json:"username"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Locked bool `json:"locked"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } `json:"author"` } type VoteResponse struct { Success bool `json:"success"` Message string `json:"message"` Data any `json:"data,omitempty"` Error string `json:"error,omitempty"` } type HealthResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Status string `json:"status"` Timestamp string `json:"timestamp"` Version string `json:"version"` Services struct { Database string `json:"database"` API string `json:"api"` } `json:"services"` PingTime string `json:"ping_time,omitempty"` DatabaseStats struct { TotalQueries int64 `json:"total_queries,omitempty"` SlowQueries int64 `json:"slow_queries,omitempty"` AverageDuration string `json:"average_duration,omitempty"` MaxDuration string `json:"max_duration,omitempty"` ErrorCount int64 `json:"error_count,omitempty"` LastQueryTime string `json:"last_query_time,omitempty"` } `json:"database_stats"` } `json:"data"` } type MetricsResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Posts struct { TotalCount int64 `json:"total_count"` TopPostsCount int `json:"top_posts_count"` TotalScore int `json:"total_score"` AverageScore float64 `json:"average_score"` } `json:"posts"` Users struct { TotalCount int64 `json:"total_count"` } `json:"users"` Votes struct { TotalCount int64 `json:"total_count"` AveragePerPost float64 `json:"average_per_post"` Note string `json:"note"` } `json:"votes"` System struct { Timestamp string `json:"timestamp"` Version string `json:"version"` } `json:"system"` Database struct { TotalQueries int64 `json:"total_queries,omitempty"` SlowQueries int64 `json:"slow_queries,omitempty"` AverageDuration string `json:"average_duration,omitempty"` MaxDuration string `json:"max_duration,omitempty"` ErrorCount int64 `json:"error_count,omitempty"` LastQueryTime string `json:"last_query_time,omitempty"` } `json:"database"` Performance struct { RequestCount int64 `json:"request_count,omitempty"` AverageResponse string `json:"average_response,omitempty"` MaxResponse string `json:"max_response,omitempty"` ErrorCount int64 `json:"error_count,omitempty"` } `json:"performance"` } `json:"data"` } type UserResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Users []struct { ID uint `json:"id"` Username string `json:"username"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } `json:"users"` Count int `json:"count"` Limit int `json:"limit"` Offset int `json:"offset"` } `json:"data"` } type ProfileResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { ID uint `json:"id"` Username string `json:"username"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Locked bool `json:"locked"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } `json:"data"` } type AccountDeletionResponse struct { Success bool `json:"success"` Message string `json:"message"` Data struct { DeletionToken string `json:"deletion_token"` ExpiresAt string `json:"expires_at"` } `json:"data"` } func CreateE2ETestUser(t *testing.T, userRepo repositories.UserRepository, username, email, password string) *TestUser { t.Helper() normalizedEmail := strings.ToLower(strings.TrimSpace(email)) user := &database.User{ Username: username, Email: normalizedEmail, Password: hashPassword(password), EmailVerified: true, } if err := userRepo.Create(user); err != nil { t.Fatalf("Failed to create test user: %v", err) } createdUser, err := userRepo.GetByID(user.ID) if err != nil { t.Fatalf("Failed to fetch created user: %v", err) } if !createdUser.EmailVerified { now := time.Now() createdUser.EmailVerified = true createdUser.EmailVerifiedAt = &now if err := userRepo.Update(createdUser); err != nil { t.Fatalf("Failed to auto-verify test user email: %v", err) } } if !createdUser.EmailVerified { t.Fatalf("User email verification not set correctly. Expected true, got %v", createdUser.EmailVerified) } return &TestUser{ ID: createdUser.ID, Username: createdUser.Username, Email: createdUser.Email, Password: password, EmailVerified: createdUser.EmailVerified, } } func LoginUserSafe(client *http.Client, baseURL, username, password string) (*AuthenticatedClient, error) { loginData := map[string]string{ "username": username, "password": password, } loginBody, err := json.Marshal(loginData) if err != nil { return nil, fmt.Errorf("failed to marshal login data: %w", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/login", bytes.NewReader(loginBody)) if err != nil { return nil, fmt.Errorf("failed to create login request: %w", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) request.Header.Set("X-Forwarded-For", GenerateTestIP()) resp, err := client.Do(request) if err != nil { return nil, fmt.Errorf("failed to make login request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes := make([]byte, 1024) n, _ := resp.Body.Read(bodyBytes) return nil, fmt.Errorf("login failed with status %d. Response: %s", resp.StatusCode, string(bodyBytes[:n])) } reader, err := decompressResponse(resp) if err != nil { return nil, fmt.Errorf("failed to decompress response: %w", err) } var loginResp LoginResponse if err := json.NewDecoder(reader).Decode(&loginResp); err != nil { return nil, fmt.Errorf("failed to decode login response: %w", err) } if !loginResp.Success { return nil, fmt.Errorf("login response indicates failure: %s", loginResp.Message) } accessToken := loginResp.Data.AccessToken if accessToken == "" { accessToken = loginResp.Data.Token } if accessToken == "" { return nil, fmt.Errorf("login response missing access token") } return &AuthenticatedClient{ Client: client, Token: accessToken, RefreshToken: loginResp.Data.RefreshToken, BaseURL: baseURL, }, nil } func LoginUser(t *testing.T, client *http.Client, baseURL, username, password string) *AuthenticatedClient { t.Helper() loginData := map[string]string{ "username": username, "password": password, } loginBody, err := json.Marshal(loginData) if err != nil { t.Fatalf("Failed to marshal login data: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/login", bytes.NewReader(loginBody)) if err != nil { t.Fatalf("Failed to create login request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) request.Header.Set("X-Forwarded-For", GenerateTestIP()) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make login request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes := make([]byte, 1024) n, _ := resp.Body.Read(bodyBytes) t.Fatalf("Login failed with status %d. Response: %s", resp.StatusCode, string(bodyBytes[:n])) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var loginResp LoginResponse if err := json.NewDecoder(reader).Decode(&loginResp); err != nil { t.Fatalf("Failed to decode login response: %v", err) } if !loginResp.Success { t.Fatalf("Login response indicates failure: %s", loginResp.Message) } accessToken := loginResp.Data.AccessToken if accessToken == "" { accessToken = loginResp.Data.Token } if accessToken == "" { t.Fatalf("Login response missing access token") } return &AuthenticatedClient{ Client: client, Token: accessToken, RefreshToken: loginResp.Data.RefreshToken, BaseURL: baseURL, } } func CreateOversizedPayload() []byte { data := make([]byte, 1024*1024) for i := range data { data[i] = 'A' } return data } func WithStandardHeaders(request *http.Request) { request.Header.Set("User-Agent", StandardUserAgent) request.Header.Set("Accept-Encoding", StandardAcceptEncoding) } func AssertPostInList(t *testing.T, posts *PostsListResponse, expectedPost *TestPost) { t.Helper() if len(posts.Data.Posts) == 0 { t.Errorf("Expected at least one post in response, got empty array") return } found := false for _, post := range posts.Data.Posts { if post.ID == expectedPost.ID && post.Title == expectedPost.Title { found = true break } } if !found { t.Errorf("Expected post with ID %d and title '%s' not found in posts list", expectedPost.ID, expectedPost.Title) } } func (ac *AuthenticatedClient) CreatePostSafe(title, url, content string) (*TestPost, error) { postData := map[string]string{ "title": title, "url": url, "content": content, } postBody, err := json.Marshal(postData) if err != nil { return nil, fmt.Errorf("failed to marshal post data: %w", err) } request, err := http.NewRequest("POST", ac.BaseURL+"/api/posts", bytes.NewReader(postBody)) if err != nil { return nil, fmt.Errorf("failed to create post request: %w", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { return nil, fmt.Errorf("failed to make post request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return nil, fmt.Errorf("post creation failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { return nil, fmt.Errorf("failed to decompress response: %w", err) } var postResp PostResponse if err := json.NewDecoder(reader).Decode(&postResp); err != nil { return nil, fmt.Errorf("failed to decode post response: %w", err) } if !postResp.Success { return nil, fmt.Errorf("post creation response indicates failure: %s", postResp.Message) } return &TestPost{ ID: postResp.Data.ID, Title: postResp.Data.Title, URL: postResp.Data.URL, Content: postResp.Data.Content, AuthorID: postResp.Data.AuthorID, }, nil } func (ac *AuthenticatedClient) VoteOnPostSafe(postID uint, voteType string) (*VoteResponse, error) { voteData := map[string]string{ "type": voteType, } voteBody, err := json.Marshal(voteData) if err != nil { return nil, fmt.Errorf("failed to marshal vote data: %w", err) } url := fmt.Sprintf("%s/api/posts/%d/vote", ac.BaseURL, postID) request, err := http.NewRequest("POST", url, bytes.NewReader(voteBody)) if err != nil { return nil, fmt.Errorf("failed to create vote request: %w", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { return nil, fmt.Errorf("failed to make vote request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("vote failed with status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read vote response: %w", err) } var voteResp VoteResponse if len(body) > 0 { if err := json.Unmarshal(body, &voteResp); err != nil { voteResp = VoteResponse{ Success: false, Message: string(bytes.TrimSpace(body)), } } } if !voteResp.Success { return nil, fmt.Errorf("vote response indicates failure: %s", voteResp.Message) } return &voteResp, nil } func (ac *AuthenticatedClient) SearchPostsSafe(query string) (*PostsListResponse, error) { url := fmt.Sprintf("%s/api/posts/search?q=%s", ac.BaseURL, query) request, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create search request: %w", err) } WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { return nil, fmt.Errorf("failed to make search request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("search posts failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { return nil, fmt.Errorf("failed to decompress response: %w", err) } var searchResp PostsListResponse if err := json.NewDecoder(reader).Decode(&searchResp); err != nil { return nil, fmt.Errorf("failed to decode search response: %w", err) } if !searchResp.Success { return nil, fmt.Errorf("search posts response indicates failure: %s", searchResp.Message) } return &searchResp, nil } func (ac *AuthenticatedClient) CreatePost(t *testing.T, title, url, content string) *TestPost { t.Helper() postData := map[string]string{ "title": title, "url": url, "content": content, } postBody, err := json.Marshal(postData) if err != nil { t.Fatalf("Failed to marshal post data: %v", err) } request, err := http.NewRequest("POST", ac.BaseURL+"/api/posts", bytes.NewReader(postBody)) if err != nil { t.Fatalf("Failed to create post request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make post request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("Post creation failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var postResp PostResponse if err := json.NewDecoder(reader).Decode(&postResp); err != nil { t.Fatalf("Failed to decode post response: %v", err) } if !postResp.Success { t.Fatalf("Post creation response indicates failure: %s", postResp.Message) } return &TestPost{ ID: postResp.Data.ID, Title: postResp.Data.Title, URL: postResp.Data.URL, Content: postResp.Data.Content, AuthorID: postResp.Data.AuthorID, } } func (ac *AuthenticatedClient) VoteOnPost(t *testing.T, postID uint, voteType string) *VoteResponse { t.Helper() voteResp, statusCode := ac.VoteOnPostRaw(t, postID, voteType) if statusCode != http.StatusOK { t.Fatalf("Vote failed with status %d", statusCode) } if !voteResp.Success { t.Fatalf("Vote response indicates failure: %s", voteResp.Message) } return voteResp } func (ac *AuthenticatedClient) VoteOnPostRaw(t *testing.T, postID uint, voteType string) (*VoteResponse, int) { t.Helper() voteData := map[string]string{ "type": voteType, } voteBody, err := json.Marshal(voteData) if err != nil { t.Fatalf("Failed to marshal vote data: %v", err) } url := fmt.Sprintf("%s/api/posts/%d/vote", ac.BaseURL, postID) request, err := http.NewRequest("POST", url, bytes.NewReader(voteBody)) if err != nil { t.Fatalf("Failed to create vote request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make vote request: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Failed to read vote response: %v", err) } var voteResp VoteResponse if len(body) > 0 { if err := json.Unmarshal(body, &voteResp); err != nil { voteResp = VoteResponse{ Success: false, Message: string(bytes.TrimSpace(body)), } } } return &voteResp, resp.StatusCode } func (ac *AuthenticatedClient) GetPosts(t *testing.T) *PostsListResponse { t.Helper() request, err := http.NewRequest("GET", ac.BaseURL+"/api/posts", nil) if err != nil { t.Fatalf("Failed to create posts request: %v", err) } WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make posts request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Get posts failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var postsResp PostsListResponse if err := json.NewDecoder(reader).Decode(&postsResp); err != nil { t.Fatalf("Failed to decode posts response: %v", err) } if !postsResp.Success { t.Fatalf("Get posts response indicates failure: %s", postsResp.Message) } return &postsResp } func (ac *AuthenticatedClient) SearchPosts(t *testing.T, query string) *PostsListResponse { t.Helper() url := fmt.Sprintf("%s/api/posts/search?q=%s", ac.BaseURL, query) request, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("Failed to create search request: %v", err) } WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make search request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Search posts failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var searchResp PostsListResponse if err := json.NewDecoder(reader).Decode(&searchResp); err != nil { t.Fatalf("Failed to decode search response: %v", err) } if !searchResp.Success { t.Fatalf("Search posts response indicates failure: %s", searchResp.Message) } return &searchResp } func (ac *AuthenticatedClient) Logout(t *testing.T) { t.Helper() request, err := http.NewRequest("POST", ac.BaseURL+"/api/auth/logout", nil) if err != nil { t.Fatalf("Failed to create logout request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make logout request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Logout failed with status %d", resp.StatusCode) } } func (ac *AuthenticatedClient) RefreshAccessToken(t *testing.T, ipAddress ...string) (string, int) { t.Helper() var ip string if len(ipAddress) > 0 { ip = ipAddress[0] } newAccessToken, newRefreshToken, statusCode := RefreshTokenWithIP(t, ac.Client, ac.BaseURL, ac.RefreshToken, ip) if statusCode == http.StatusOK { ac.Token = newAccessToken if newRefreshToken != "" { ac.RefreshToken = newRefreshToken } } return newAccessToken, statusCode } func (ac *AuthenticatedClient) RevokeToken(t *testing.T, refreshToken string) int { t.Helper() return RevokeToken(t, ac.Client, ac.BaseURL, refreshToken, ac.Token) } func (ac *AuthenticatedClient) RevokeAllTokens(t *testing.T) int { t.Helper() return RevokeAllTokens(t, ac.Client, ac.BaseURL, ac.Token) } func (ac *AuthenticatedClient) ConfirmAccountDeletion(t *testing.T, token string, deletePosts bool) int { t.Helper() return ConfirmAccountDeletion(t, ac.Client, ac.BaseURL, token, deletePosts) } func (ac *AuthenticatedClient) GetProfile(t *testing.T) *ProfileResponse { t.Helper() request, err := http.NewRequest("GET", ac.BaseURL+"/api/auth/me", nil) if err != nil { t.Fatalf("Failed to create profile request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make profile request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Get profile failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var profileResp ProfileResponse if err := json.NewDecoder(reader).Decode(&profileResp); err != nil { t.Fatalf("Failed to decode profile response: %v", err) } if !profileResp.Success { t.Fatalf("Get profile response indicates failure: %s", profileResp.Message) } return &profileResp } func (ac *AuthenticatedClient) UpdateUsername(t *testing.T, newUsername string) { t.Helper() updateData := map[string]string{ "username": newUsername, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal username update data: %v", err) } request, err := http.NewRequest("PUT", ac.BaseURL+"/api/auth/username", bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create username update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make username update request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Username update failed with status %d", resp.StatusCode) } } func (ac *AuthenticatedClient) UpdatePassword(t *testing.T, currentPassword, newPassword string) { t.Helper() updateData := map[string]string{ "current_password": currentPassword, "new_password": newPassword, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal password update data: %v", err) } request, err := http.NewRequest("PUT", ac.BaseURL+"/api/auth/password", bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create password update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make password update request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Password update failed with status %d", resp.StatusCode) } } func (ac *AuthenticatedClient) RegisterUser(t *testing.T, username, email, password string) *LoginResponse { t.Helper() registerData := map[string]string{ "username": username, "email": email, "password": password, } registerBody, err := json.Marshal(registerData) if err != nil { t.Fatalf("Failed to marshal register data: %v", err) } request, err := http.NewRequest("POST", ac.BaseURL+"/api/auth/register", bytes.NewReader(registerBody)) if err != nil { t.Fatalf("Failed to create register request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make register request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("Registration failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var registerResp LoginResponse if err := json.NewDecoder(reader).Decode(®isterResp); err != nil { t.Fatalf("Failed to decode register response: %v", err) } if !registerResp.Success { t.Fatalf("Registration response indicates failure: %s", registerResp.Message) } return ®isterResp } func (ac *AuthenticatedClient) UpdateEmail(t *testing.T, newEmail string) { t.Helper() updateData := map[string]string{ "email": newEmail, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal email update data: %v", err) } request, err := http.NewRequest("PUT", ac.BaseURL+"/api/auth/email", bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create email update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make email update request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Email update failed with status %d", resp.StatusCode) } } func (ac *AuthenticatedClient) UpdatePost(t *testing.T, postID uint, title, url, content string) *TestPost { t.Helper() updateData := map[string]string{ "title": title, "url": url, "content": content, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal post update data: %v", err) } postURL := fmt.Sprintf("%s/api/posts/%d", ac.BaseURL, postID) request, err := http.NewRequest("PUT", postURL, bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create post update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make post update request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Post update failed with status %d", resp.StatusCode) } var postResp PostResponse if err := json.NewDecoder(resp.Body).Decode(&postResp); err != nil { t.Fatalf("Failed to decode post update response: %v", err) } if !postResp.Success { t.Fatalf("Post update response indicates failure: %s", postResp.Message) } return &TestPost{ ID: postResp.Data.ID, Title: postResp.Data.Title, URL: postResp.Data.URL, Content: postResp.Data.Content, AuthorID: postResp.Data.AuthorID, } } func (ac *AuthenticatedClient) DeletePost(t *testing.T, postID uint) { t.Helper() url := fmt.Sprintf("%s/api/posts/%d", ac.BaseURL, postID) request, err := http.NewRequest("DELETE", url, nil) if err != nil { t.Fatalf("Failed to create post delete request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make post delete request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Post delete failed with status %d", resp.StatusCode) } } func (ac *AuthenticatedClient) RemoveVote(t *testing.T, postID uint) { t.Helper() url := fmt.Sprintf("%s/api/posts/%d/vote", ac.BaseURL, postID) request, err := http.NewRequest("DELETE", url, nil) if err != nil { t.Fatalf("Failed to create vote removal request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make vote removal request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Vote removal failed with status %d", resp.StatusCode) } } func (ac *AuthenticatedClient) GetUserVote(t *testing.T, postID uint) *VoteResponse { t.Helper() url := fmt.Sprintf("%s/api/posts/%d/vote", ac.BaseURL, postID) request, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("Failed to create get vote request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make get vote request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Get vote failed with status %d", resp.StatusCode) } var voteResp VoteResponse if err := json.NewDecoder(resp.Body).Decode(&voteResp); err != nil { t.Fatalf("Failed to decode vote response: %v", err) } return &voteResp } func (ac *AuthenticatedClient) GetPostVotes(t *testing.T, postID uint) *VoteResponse { t.Helper() url := fmt.Sprintf("%s/api/posts/%d/votes", ac.BaseURL, postID) request, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("Failed to create get post votes request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make get post votes request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Get post votes failed with status %d", resp.StatusCode) } var voteResp VoteResponse if err := json.NewDecoder(resp.Body).Decode(&voteResp); err != nil { t.Fatalf("Failed to decode post votes response: %v", err) } return &voteResp } func (ac *AuthenticatedClient) GetUsers(t *testing.T) *UserResponse { t.Helper() request, err := http.NewRequest("GET", ac.BaseURL+"/api/users", nil) if err != nil { t.Fatalf("Failed to create get users request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) response, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make get users request: %v", err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { t.Fatalf("Get users failed with status %d", response.StatusCode) } reader, err := decompressResponse(response) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var usersResponse UserResponse if err := json.NewDecoder(reader).Decode(&usersResponse); err != nil { t.Fatalf("Failed to decode users response: %v", err) } if !usersResponse.Success { t.Fatalf("Get users response indicates failure: %s", usersResponse.Message) } return &usersResponse } func (ac *AuthenticatedClient) RequestAccountDeletion(t *testing.T) *AccountDeletionResponse { t.Helper() request, err := http.NewRequest("DELETE", ac.BaseURL+"/api/auth/account", nil) if err != nil { t.Fatalf("Failed to create account deletion request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) response, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make account deletion request: %v", err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { bodyBytes := make([]byte, 1024) n, _ := response.Body.Read(bodyBytes) t.Fatalf("Account deletion request failed with status %d. Response: %s", response.StatusCode, string(bodyBytes[:n])) } reader, err := decompressResponse(response) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var deletionResponse AccountDeletionResponse if err := json.NewDecoder(reader).Decode(&deletionResponse); err != nil { t.Fatalf("Failed to decode account deletion response: %v", err) } if !deletionResponse.Success { t.Fatalf("Account deletion response indicates failure: %s", deletionResponse.Message) } return &deletionResponse } func ConfirmAccountDeletion(t *testing.T, client *http.Client, baseURL, token string, deletePosts bool) int { t.Helper() requestData := map[string]any{ "token": token, "delete_posts": deletePosts, } requestBody, err := json.Marshal(requestData) if err != nil { t.Fatalf("Failed to marshal account deletion confirmation request: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/account/confirm", bytes.NewReader(requestBody)) if err != nil { t.Fatalf("Failed to create account deletion confirmation request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make account deletion confirmation request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func ResendVerificationEmail(t *testing.T, client *http.Client, baseURL, email string) int { t.Helper() requestData := map[string]string{ "email": email, } requestBody, err := json.Marshal(requestData) if err != nil { t.Fatalf("Failed to marshal resend verification request: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/resend-verification", bytes.NewReader(requestBody)) if err != nil { t.Fatalf("Failed to create resend verification request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make resend verification request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func hashPassword(password string) string { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { panic(fmt.Sprintf("Failed to hash password: %v", err)) } return string(hashedPassword) } func decompressResponse(resp *http.Response) (io.Reader, error) { if resp.Header.Get("Content-Encoding") == "gzip" { gzReader, err := gzip.NewReader(resp.Body) if err != nil { return nil, fmt.Errorf("failed to create gzip reader: %w", err) } return gzReader, nil } return resp.Body, nil } func GetHealth(t *testing.T, client *http.Client, baseURL string) *HealthResponse { t.Helper() request, err := http.NewRequest("GET", baseURL+"/health", nil) if err != nil { t.Fatalf("Failed to create health request: %v", err) } WithStandardHeaders(request) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make health request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Health check failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var healthResp HealthResponse if err := json.NewDecoder(reader).Decode(&healthResp); err != nil { t.Fatalf("Failed to decode health response: %v", err) } if !healthResp.Success { t.Fatalf("Health response indicates failure: %s", healthResp.Message) } return &healthResp } func GetMetrics(t *testing.T, client *http.Client, baseURL string) *MetricsResponse { t.Helper() request, err := http.NewRequest("GET", baseURL+"/metrics", nil) if err != nil { t.Fatalf("Failed to create metrics request: %v", err) } WithStandardHeaders(request) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make metrics request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Metrics request failed with status %d", resp.StatusCode) } reader, err := decompressResponse(resp) if err != nil { t.Fatalf("Failed to decompress response: %v", err) } var metricsResp MetricsResponse if err := json.NewDecoder(reader).Decode(&metricsResp); err != nil { t.Fatalf("Failed to decode metrics response: %v", err) } if !metricsResp.Success { t.Fatalf("Metrics response indicates failure: %s", metricsResp.Message) } return &metricsResp } func (ac *AuthenticatedClient) UpdatePostExpectStatus(t *testing.T, postID uint, title, url, content string) int { t.Helper() updateData := map[string]string{ "title": title, "url": url, "content": content, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal post update data: %v", err) } postURL := fmt.Sprintf("%s/api/posts/%d", ac.BaseURL, postID) request, err := http.NewRequest("PUT", postURL, bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create post update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make post update request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func (ac *AuthenticatedClient) DeletePostExpectStatus(t *testing.T, postID uint) int { t.Helper() url := fmt.Sprintf("%s/api/posts/%d", ac.BaseURL, postID) request, err := http.NewRequest("DELETE", url, nil) if err != nil { t.Fatalf("Failed to create post delete request: %v", err) } request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make post delete request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func (ac *AuthenticatedClient) UpdateEmailExpectStatus(t *testing.T, newEmail string) int { t.Helper() updateData := map[string]string{ "email": newEmail, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal email update data: %v", err) } request, err := http.NewRequest("PUT", ac.BaseURL+"/api/auth/email", bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create email update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make email update request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func (ac *AuthenticatedClient) UpdateUsernameExpectStatus(t *testing.T, newUsername string) int { t.Helper() updateData := map[string]string{ "username": newUsername, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal username update data: %v", err) } request, err := http.NewRequest("PUT", ac.BaseURL+"/api/auth/username", bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create username update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make username update request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func AssertVoteData(t *testing.T, voteResp *VoteResponse) map[string]any { t.Helper() data, ok := voteResp.Data.(map[string]any) if !ok { t.Fatalf("Expected vote data to be a map, got %T", voteResp.Data) } return data } func (ac *AuthenticatedClient) UpdatePasswordExpectStatus(t *testing.T, currentPassword, newPassword string) int { t.Helper() updateData := map[string]string{ "current_password": currentPassword, "new_password": newPassword, } updateBody, err := json.Marshal(updateData) if err != nil { t.Fatalf("Failed to marshal password update data: %v", err) } request, err := http.NewRequest("PUT", ac.BaseURL+"/api/auth/password", bytes.NewReader(updateBody)) if err != nil { t.Fatalf("Failed to create password update request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+ac.Token) WithStandardHeaders(request) resp, err := ac.Client.Do(request) if err != nil { t.Fatalf("Failed to make password update request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func RequestPasswordReset(t *testing.T, client *http.Client, baseURL, usernameOrEmail, ipAddress string) int { t.Helper() requestData := map[string]string{ "username_or_email": usernameOrEmail, } requestBody, err := json.Marshal(requestData) if err != nil { t.Fatalf("Failed to marshal password reset request: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/forgot-password", bytes.NewReader(requestBody)) if err != nil { t.Fatalf("Failed to create password reset request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) if ipAddress != "" { request.Header.Set("X-Forwarded-For", ipAddress) } resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make password reset request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func ResetPassword(t *testing.T, client *http.Client, baseURL, token, newPassword, ipAddress string) int { t.Helper() requestData := map[string]string{ "token": token, "new_password": newPassword, } requestBody, err := json.Marshal(requestData) if err != nil { t.Fatalf("Failed to marshal password reset request: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/reset-password", bytes.NewReader(requestBody)) if err != nil { t.Fatalf("Failed to create password reset request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) if ipAddress != "" { request.Header.Set("X-Forwarded-For", ipAddress) } resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make password reset request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func RefreshToken(t *testing.T, client *http.Client, baseURL, refreshToken string) (accessToken string, returnedRefreshToken string, statusCode int) { return RefreshTokenWithIP(t, client, baseURL, refreshToken, "") } func RefreshTokenWithIP(t *testing.T, client *http.Client, baseURL, refreshToken, ipAddress string) (accessToken string, returnedRefreshToken string, statusCode int) { t.Helper() requestData := map[string]string{ "refresh_token": refreshToken, } requestBody, err := json.Marshal(requestData) if err != nil { t.Fatalf("Failed to marshal refresh token request: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/refresh", bytes.NewReader(requestBody)) if err != nil { t.Fatalf("Failed to create refresh token request: %v", err) } request.Header.Set("Content-Type", "application/json") WithStandardHeaders(request) if ipAddress != "" { request.Header.Set("X-Forwarded-For", ipAddress) } resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make refresh token request: %v", err) } defer resp.Body.Close() reader, err := decompressResponse(resp) if err != nil { reader = resp.Body } var refreshResp LoginResponse bodyBytes, err := io.ReadAll(reader) if err != nil { t.Fatalf("Failed to read refresh token response: %v", err) } if resp.StatusCode == http.StatusOK { if err := json.Unmarshal(bodyBytes, &refreshResp); err != nil { t.Fatalf("Failed to decode refresh token response: %v. Body: %s", err, string(bodyBytes)) } accessToken = refreshResp.Data.AccessToken if accessToken == "" { accessToken = refreshResp.Data.Token } returnedRefreshToken = refreshResp.Data.RefreshToken } return accessToken, returnedRefreshToken, resp.StatusCode } func RevokeToken(t *testing.T, client *http.Client, baseURL, refreshToken, accessToken string) int { t.Helper() requestData := map[string]string{ "refresh_token": refreshToken, } requestBody, err := json.Marshal(requestData) if err != nil { t.Fatalf("Failed to marshal revoke token request: %v", err) } request, err := http.NewRequest("POST", baseURL+"/api/auth/revoke", bytes.NewReader(requestBody)) if err != nil { t.Fatalf("Failed to create revoke token request: %v", err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("Authorization", "Bearer "+accessToken) WithStandardHeaders(request) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make revoke token request: %v", err) } defer resp.Body.Close() return resp.StatusCode } func RevokeAllTokens(t *testing.T, client *http.Client, baseURL, accessToken string) int { t.Helper() request, err := http.NewRequest("POST", baseURL+"/api/auth/revoke-all", nil) if err != nil { t.Fatalf("Failed to create revoke all tokens request: %v", err) } request.Header.Set("Authorization", "Bearer "+accessToken) WithStandardHeaders(request) resp, err := client.Do(request) if err != nil { t.Fatalf("Failed to make revoke all tokens request: %v", err) } defer resp.Body.Close() return resp.StatusCode }