diff --git a/internal/api/handler/handler_test.go b/internal/api/handler/handler_test.go index bae464c..bc9490d 100644 --- a/internal/api/handler/handler_test.go +++ b/internal/api/handler/handler_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "mime/multipart" "net/http" "net/http/httptest" "sync" @@ -103,6 +104,7 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) { WithPasswordHistoryRepo(passwordHistoryRepo) themeRepo := repository.NewThemeConfigRepository(db) themeSvc := service.NewThemeService(themeRepo) + avatarH := handler.NewAvatarHandler(userRepo) rateLimitCfg := config.RateLimitConfig{} rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg) @@ -127,7 +129,7 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) { authHandler, userHandler, roleHandler, permHandler, deviceHandler, logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware, pwdResetHandler, captchaHandler, totpHandler, nil, - nil, nil, nil, nil, nil, themeHandler, nil, nil, nil, + nil, nil, nil, nil, nil, themeHandler, nil, nil, nil, avatarH, ) engine := r.Setup() @@ -1276,3 +1278,99 @@ func TestAuthHandler_RefreshToken_MissingToken(t *testing.T) { t.Errorf("expected status %d for missing refresh token, got %d", http.StatusBadRequest, resp.StatusCode) } } + +// ============================================================================= +// Avatar Handler Tests +// ============================================================================= + +func doUploadFile(url, token string, fieldName string, fileName string, fileContent []byte) (*http.Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile(fieldName, fileName) + if err != nil { + return nil, err + } + if _, err := part.Write(fileContent); err != nil { + return nil, err + } + if err := writer.Close(); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + client := &http.Client{} + return client.Do(req) +} + +func TestAvatarHandler_UploadAvatar_Unauthorized(t *testing.T) { + server, cleanup := setupHandlerTestServer(t) + defer cleanup() + + // Create a fake PNG file + fileContent := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + + resp, err := doUploadFile(server.URL+"/api/v1/users/1/avatar", "", "avatar", "test.png", fileContent) + if err != nil { + t.Fatalf("upload request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("expected status %d for unauthorized request, got %d", http.StatusUnauthorized, resp.StatusCode) + } +} + +func TestAvatarHandler_UploadAvatar_NonAdminCannotUpdateOther(t *testing.T) { + server, cleanup := setupHandlerTestServer(t) + defer cleanup() + + // Register two users + registerUser(server.URL, "user1", "user1@test.com", "UserPass123!") + token1 := getToken(server.URL, "user1", "UserPass123!") + registerUser(server.URL, "user2", "user2@test.com", "UserPass123!") + + // user1 tries to update user2's avatar (should be forbidden) + fileContent := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + resp, err := doUploadFile(server.URL+"/api/v1/users/2/avatar", token1, "avatar", "test.png", fileContent) + if err != nil { + t.Fatalf("upload request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusForbidden { + t.Errorf("expected status %d for non-admin updating other's avatar, got %d", http.StatusForbidden, resp.StatusCode) + } +} + +func TestAvatarHandler_UploadAvatar_UserNotFoundOrForbidden(t *testing.T) { + server, cleanup := setupHandlerTestServer(t) + defer cleanup() + + // Register and login as a user + registerUser(server.URL, "avataruser", "avataruser@test.com", "UserPass123!") + token := getToken(server.URL, "avataruser", "UserPass123!") + + // Try to upload avatar for non-existent user (ID 9999) + // Should return 403 because permission check happens before existence check + // (security: don't reveal whether user exists) + fileContent := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + resp, err := doUploadFile(server.URL+"/api/v1/users/9999/avatar", token, "avatar", "test.png", fileContent) + if err != nil { + t.Fatalf("upload request failed: %v", err) + } + defer resp.Body.Close() + + // Handler returns 403 (permission denied) before checking if user exists + // This is intentional security behavior - don't leak whether user ID exists + if resp.StatusCode != http.StatusForbidden { + t.Errorf("expected status %d for updating non-existent user's avatar, got %d", http.StatusForbidden, resp.StatusCode) + } +}