package e2e import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "testing" "goyco/internal/repositories" "goyco/internal/testutils" ) func TestE2E_SecurityWorkflows(t *testing.T) { ctx := setupTestContext(t) t.Run("security_workflows", func(t *testing.T) { createdUser := ctx.createUserWithCleanup(t, "testuser", "StrongPass123!") _ = ctx.loginUser(t, createdUser.Username, createdUser.Password) t.Run("unauthorized_access_attempts", func(t *testing.T) { request, err := testutils.NewRequestBuilder("GET", ctx.baseURL+"/api/auth/me").Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(request) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("Expected 401 for unauthorized access, got %d", resp.StatusCode) } }) t.Run("invalid_token_access", func(t *testing.T) { request, err := testutils.NewRequestBuilder("GET", ctx.baseURL+"/api/auth/me"). WithAuth("invalid-token-12345"). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(request) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Errorf("Expected 401 for invalid token, got %d", resp.StatusCode) } }) t.Run("rate_limiting", func(t *testing.T) { rateLimitCtx := setupTestContextWithAuthRateLimit(t, 5) rateLimitUser := rateLimitCtx.createUserWithCleanup(t, "ratelimituser", "StrongPass123!") _ = rateLimitCtx.loginUser(t, rateLimitUser.Username, rateLimitUser.Password) testIP := testutils.GenerateTestIP() rateLimited := false for range 10 { statusCode := rateLimitCtx.loginExpectStatusWithIP(t, rateLimitUser.Username, "WrongPass123!", http.StatusUnauthorized, testIP) if statusCode == http.StatusTooManyRequests { rateLimited = true break } } if !rateLimited { t.Errorf("Expected rate limiting to occur after multiple failed login attempts") } }) }) } func TestE2E_SearchSanitization(t *testing.T) { ctx := setupTestContext(t) t.Run("search_sanitization", func(t *testing.T) { _, authClient := ctx.createUserAndLogin(t, "testuser", "StrongPass123!") _ = authClient.CreatePost(t, "Searchable Post", "https://example.com/search", "This post contains searchable content") benignSearch := authClient.SearchPosts(t, "searchable") if !benignSearch.Success { t.Errorf("Expected benign search to succeed, got failure: %s", benignSearch.Message) } if len(benignSearch.Data.Posts) == 0 { t.Errorf("Expected to find post with benign search query") } maliciousQuery := "searchable'; DROP TABLE users; --" request, err := testutils.NewRequestBuilder("GET", ctx.baseURL+"/api/posts/search?q="+url.QueryEscape(maliciousQuery)).Build() if err != nil { t.Fatalf("Failed to create malicious search request: %v", err) } resp, err := ctx.client.Do(request) if err != nil { t.Fatalf("Failed to make malicious search request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("Expected 400 for malicious search query, got %d", resp.StatusCode) } }) } func TestE2E_SecurityHeaders(t *testing.T) { ctx := setupTestContext(t) expectedHeaders := map[string]string{ "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "X-XSS-Protection": "1; mode=block", "Referrer-Policy": "strict-origin-when-cross-origin", } type endpointTest struct { name string method string path string auth bool body []byte } endpoints := []endpointTest{ {name: "health_endpoint", method: "GET", path: "/health", auth: false}, {name: "metrics_endpoint", method: "GET", path: "/metrics", auth: false}, {name: "api_registration", method: "POST", path: "/api/auth/register", auth: false, body: []byte(`{"username":"testuser","email":"test@example.com","password":"StrongPass123!"}`)}, {name: "api_posts", method: "GET", path: "/api/posts", auth: true}, {name: "api_auth_me", method: "GET", path: "/api/auth/me", auth: true}, } t.Run("security_headers_on_all_endpoints", func(t *testing.T) { testUser := ctx.createUserWithCleanup(t, "headertest", "StrongPass123!") var authToken string authClient, err := ctx.loginUserSafe(t, testUser.Username, testUser.Password) if err == nil { authToken = authClient.Token } for _, endpoint := range endpoints { t.Run(endpoint.name, func(t *testing.T) { var req *http.Request var err error if endpoint.body != nil { req, err = http.NewRequest(endpoint.method, ctx.baseURL+endpoint.path, bytes.NewReader(endpoint.body)) } else { req, err = http.NewRequest(endpoint.method, ctx.baseURL+endpoint.path, nil) } if err != nil { t.Fatalf("Failed to create request: %v", err) } if endpoint.auth && authToken != "" { req.Header.Set("Authorization", "Bearer "+authToken) } testutils.WithStandardHeaders(req) resp, err := ctx.client.Do(req) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() for headerName, expectedValue := range expectedHeaders { actualValue := resp.Header.Get(headerName) if actualValue != expectedValue { t.Errorf("Endpoint %s: Expected %s header to be '%s', got '%s'", endpoint.path, headerName, expectedValue, actualValue) } } csp := resp.Header.Get("Content-Security-Policy") if csp == "" { t.Errorf("Endpoint %s: Content-Security-Policy header should be present", endpoint.path) } }) } }) } func TestE2E_SQLInjectionAcrossEndpoints(t *testing.T) { ctx := setupTestContext(t) sqlPayloads := testutils.SQLInjectionPayloads t.Run("sql_injection_in_post_fields", func(t *testing.T) { testUser := ctx.createUserWithCleanup(t, "sqltest", "StrongPass123!") authClient, err := ctx.loginUserSafe(t, testUser.Username, testUser.Password) if err != nil { t.Skipf("Skipping sql injection in post fields test: %v", err) } for i, payload := range sqlPayloads { t.Run(fmt.Sprintf("payload_%d", i), func(t *testing.T) { postData := map[string]string{ "title": payload, "url": fmt.Sprintf("https://example.com/test%d", i), "content": "Test content", } req, err := testutils.NewRequestBuilder("POST", ctx.baseURL+"/api/posts"). WithAuth(authClient.Token). WithJSONBody(postData). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(req) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusInternalServerError { t.Errorf("SQL injection in title caused server crash (500). Payload: %s", payload) } postData2 := map[string]string{ "title": fmt.Sprintf("Test Post %d", i), "url": fmt.Sprintf("https://example.com/test2-%d", i), "content": payload, } req2, err := testutils.NewRequestBuilder("POST", ctx.baseURL+"/api/posts"). WithAuth(authClient.Token). WithJSONBody(postData2). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp2, err := ctx.client.Do(req2) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp2.Body.Close() if resp2.StatusCode == http.StatusInternalServerError { t.Errorf("SQL injection in content caused server crash (500). Payload: %s", payload) } }) } }) t.Run("sql_injection_in_registration_fields", func(t *testing.T) { for i, payload := range sqlPayloads { t.Run(fmt.Sprintf("payload_%d", i), func(t *testing.T) { regData := map[string]string{ "username": payload, "email": uniqueEmail(t, fmt.Sprintf("test%d", i)), "password": "StrongPass123!", } req, err := testutils.NewRequestBuilder("POST", ctx.baseURL+"/api/auth/register"). WithJSONBody(regData). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(req) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusInternalServerError { t.Errorf("SQL injection in username caused server crash (500). Payload: %s", payload) } regData2 := map[string]string{ "username": uniqueUsername(t, fmt.Sprintf("user%d", i)), "email": fmt.Sprintf("test%s@example.com", payload), "password": "StrongPass123!", } req2, err := testutils.NewRequestBuilder("POST", ctx.baseURL+"/api/auth/register"). WithJSONBody(regData2). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp2, err := ctx.client.Do(req2) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp2.Body.Close() if resp2.StatusCode == http.StatusInternalServerError { t.Errorf("SQL injection in email caused server crash (500). Payload: %s", payload) } }) } }) t.Run("sql_injection_in_url_fields", func(t *testing.T) { testUser := ctx.createUserWithCleanup(t, "sqltest2", "StrongPass123!") authClient, err := ctx.loginUserSafe(t, testUser.Username, testUser.Password) if err != nil { t.Skipf("Skipping sql injection in url fields test: %v", err) } for i, payload := range sqlPayloads { t.Run(fmt.Sprintf("payload_%d", i), func(t *testing.T) { postData := map[string]string{ "title": fmt.Sprintf("Test Post %d", i), "url": fmt.Sprintf("https://example.com/test%s", payload), "content": "Test content", } req, err := testutils.NewRequestBuilder("POST", ctx.baseURL+"/api/posts"). WithAuth(authClient.Token). WithJSONBody(postData). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(req) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusInternalServerError { t.Errorf("SQL injection in URL caused server crash (500). Payload: %s", payload) } }) } }) t.Run("sql_injection_in_query_parameters", func(t *testing.T) { testUser := ctx.createUserWithCleanup(t, "sqltest3", "StrongPass123!") authClient, err := ctx.loginUserSafe(t, testUser.Username, testUser.Password) if err != nil { t.Skipf("Skipping sql injection in query parameters test: %v", err) } _ = authClient.CreatePost(t, "Searchable Post", "https://example.com/search", "Content") for i, payload := range sqlPayloads { t.Run(fmt.Sprintf("payload_%d", i), func(t *testing.T) { searchURL := ctx.baseURL + "/api/posts/search?q=" + url.QueryEscape(payload) req, err := testutils.NewRequestBuilder("GET", searchURL). WithAuth(authClient.Token). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(req) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusInternalServerError { t.Errorf("SQL injection in search query caused server crash (500). Payload: %s", payload) } if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusOK { t.Logf("SQL injection in search query returned status %d (acceptable if sanitized). Payload: %s", resp.StatusCode, payload) } }) } }) } func TestE2E_XSSPrevention(t *testing.T) { ctx := setupTestContext(t) xssPayloads := testutils.XSSPayloads t.Run("xss_in_post_fields", func(t *testing.T) { testUser := ctx.createUserWithCleanup(t, "xsstest", "StrongPass123!") authClient, err := ctx.loginUserSafe(t, testUser.Username, testUser.Password) if err != nil { t.Fatalf("Failed to login: %v", err) } for idx, payload := range xssPayloads { t.Run(fmt.Sprintf("payload_%d", idx), func(t *testing.T) { postData := map[string]string{ "title": payload, "url": fmt.Sprintf("https://example.com/xss-test-%d", idx), "content": "Test content", } req, err := testutils.NewRequestBuilder("POST", ctx.baseURL+"/api/posts"). WithAuth(authClient.Token). WithJSONBody(postData). Build() if err != nil { t.Fatalf("Failed to create request: %v", err) } resp, err := ctx.client.Do(req) if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusInternalServerError { t.Errorf("XSS payload in title caused server crash (500). Payload: %s", payload) } if resp.StatusCode == http.StatusCreated { reader, cleanup, err := getResponseReader(resp) if err != nil { t.Fatalf("Failed to get response reader: %v", err) } defer cleanup() var postResp PostResponse if err := json.NewDecoder(reader).Decode(&postResp); err == nil { if strings.Contains(postResp.Data.Title, "