Ctrl K
ComunidadGuías…

internal/handler/user_handler_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestNewUserHandler(t *testing.T) {
	handler := NewUserHandler(nil, nil, createTestLogger())
	if handler == nil {
		t.Fatal("NewUserHandler() should not return nil")
	}
}

// Removed struct field assignment tests:
// - TestUpdateProfileRequest_Struct
// - TestProfileResponse_Struct
// These tests verified Go struct assignment works, not business logic.

func TestUserHandler_UpdateProfile_Unauthorized(t *testing.T) {
	handler := &UserHandler{}

	// Request without user context
	req := createTestRequest(http.MethodPut, "/api/profile", `{"name": "New Name"}`)
	rr := httptest.NewRecorder()

	handler.UpdateProfile(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestUserHandler_UpdateProfile_InvalidJSON(t *testing.T) {
	handler := &UserHandler{}

	req := createAuthenticatedRequest(http.MethodPut, "/api/profile", "{bad json", 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UpdateProfile(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid request body")
}

func TestUserHandler_UpdateProfile_InvalidBirthday(t *testing.T) {
	handler := &UserHandler{}

	// Invalid birthday format
	req := createAuthenticatedRequest(http.MethodPut, "/api/profile", `{"birthday": "invalid-date"}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UpdateProfile(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid birthday format")
}

func TestUserHandler_UpdateProfile_InvalidBirthdayFormats(t *testing.T) {
	handler := &UserHandler{}

	testCases := []struct {
		name     string
		birthday string
	}{
		{"wrong format", "15-05-1990"},
		{"partial date", "1990-05"},
		{"text date", "May 15, 1990"},
		{"slash format", "1990/05/15"},
		{"invalid month", "1990-13-01"},
		{"invalid day", "1990-01-32"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			body := `{"birthday": "` + tc.birthday + `"}`
			req := createAuthenticatedRequest(http.MethodPut, "/api/profile", body, 1, "test@example.com", "user")
			rr := httptest.NewRecorder()

			handler.UpdateProfile(rr, req)

			assertStatusCode(t, rr, http.StatusBadRequest)
			assertBodyContains(t, rr, "Invalid birthday format")
		})
	}
}

func TestUserHandler_GetProfile_Unauthorized(t *testing.T) {
	handler := &UserHandler{}

	req := createTestRequest(http.MethodGet, "/api/profile", "")
	rr := httptest.NewRecorder()

	handler.GetProfile(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestUserHandler_UploadAvatar_Unauthorized(t *testing.T) {
	handler := &UserHandler{}

	req := createTestRequest(http.MethodPost, "/api/profile/avatar", "")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestUserHandler_UploadAvatar_NoFile(t *testing.T) {
	handler := &UserHandler{}

	// Request with no multipart form
	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/avatar", "", 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
}

func TestUserHandler_DeleteAvatar_Unauthorized(t *testing.T) {
	handler := &UserHandler{}

	req := createTestRequest(http.MethodDelete, "/api/profile/avatar", "")
	rr := httptest.NewRecorder()

	handler.DeleteAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestUserHandler_ChangePassword_Unauthorized(t *testing.T) {
	handler := &UserHandler{}

	req := createTestRequest(http.MethodPost, "/api/profile/password", `{"old_password": "old", "new_password": "newpassword123"}`)
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Unauthorized")
}

func TestUserHandler_ChangePassword_InvalidJSON(t *testing.T) {
	handler := &UserHandler{}

	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", "{bad json", 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Invalid request body")
}

func TestUserHandler_ChangePassword_MissingOldPassword(t *testing.T) {
	handler := &UserHandler{}

	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", `{"new_password": "newpassword123"}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Both old_password and new_password are required")
}

func TestUserHandler_ChangePassword_MissingNewPassword(t *testing.T) {
	handler := &UserHandler{}

	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", `{"old_password": "oldpassword"}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Both old_password and new_password are required")
}

func TestUserHandler_ChangePassword_MissingBothPasswords(t *testing.T) {
	handler := &UserHandler{}

	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", `{}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Both old_password and new_password are required")
}

func TestUserHandler_ChangePassword_PasswordTooShort(t *testing.T) {
	handler := &UserHandler{}

	// Password less than 8 characters
	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", `{"old_password": "oldpass", "new_password": "short"}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "New password must be at least 8 characters")
}

func TestUserHandler_ChangePassword_PasswordExactly7Chars(t *testing.T) {
	handler := &UserHandler{}

	// Password exactly 7 characters (boundary test)
	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", `{"old_password": "oldpass", "new_password": "1234567"}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "New password must be at least 8 characters")
}

func TestUserHandler_ChangePassword_EmptyStrings(t *testing.T) {
	handler := &UserHandler{}

	// Empty string passwords
	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password", `{"old_password": "", "new_password": ""}`, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Both old_password and new_password are required")
}

func TestUserHandler_UploadAvatar_InvalidContentType(t *testing.T) {
	handler := &UserHandler{
		logger: createTestLogger(),
	}

	// Upload a non-image file (text file)
	fileContent := []byte("This is not an image")
	req := createMultipartRequest(http.MethodPost, "/api/profile/avatar", "avatar", "test.txt", "text/plain", fileContent, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "File must be an image")
}

func TestUserHandler_UploadAvatar_ApplicationPDF(t *testing.T) {
	handler := &UserHandler{
		logger: createTestLogger(),
	}

	// Upload a PDF file (not an image)
	fileContent := []byte("PDF content")
	req := createMultipartRequest(http.MethodPost, "/api/profile/avatar", "avatar", "test.pdf", "application/pdf", fileContent, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "File must be an image")
}

func TestUserHandler_UploadAvatar_SpoofedContentType(t *testing.T) {
	handler := &UserHandler{
		logger: createTestLogger(),
	}

	// Filename and Content-Type both claim image/png, but bytes are HTML.
	// Magic-byte sniffing must override the client-supplied header.
	fileContent := []byte("<script>alert('xss')</script>")
	req := createMultipartRequest(http.MethodPost, "/api/profile/avatar", "avatar", "evil.png", "image/png", fileContent, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "File must be an image")
}

func TestUserHandler_UploadAvatar_DisallowedExtensionWithImageBytes(t *testing.T) {
	handler := &UserHandler{
		logger: createTestLogger(),
	}

	// Valid PNG magic bytes but a .html filename. Magic-byte sniffing alone
	// would let this through; the extension allowlist is what stops the
	// stored-XSS attack vector via a later /uploads/... navigation.
	pngMagic := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
	pngBody := append(pngMagic, []byte("rest of png data ...")...)
	req := createMultipartRequest(http.MethodPost, "/api/profile/avatar", "avatar", "evil.html", "image/png", pngBody, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "Only jpg, png, gif, and webp images are allowed")
}

func TestUserHandler_UploadAvatar_WrongFieldName(t *testing.T) {
	handler := &UserHandler{
		logger: createTestLogger(),
	}

	// Upload with wrong field name (not "avatar")
	fileContent := []byte{0x89, 0x50, 0x4E, 0x47}
	req := createMultipartRequest(http.MethodPost, "/api/profile/avatar", "wrong_field", "test.png", "image/png", fileContent, 1, "test@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UploadAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusBadRequest)
	assertBodyContains(t, rr, "No file provided")
}

// Removed 11 panic-expectation tests:
// - TestUserHandler_UpdateProfile_NilService
// - TestUserHandler_GetProfile_NilService
// - TestUserHandler_DeleteAvatar_NilService
// - TestUserHandler_ChangePassword_NilService
// - TestUserHandler_UpdateProfile_WithValidBirthday
// - TestUserHandler_UpdateProfile_EmptyBirthday
// - TestUserHandler_UpdateProfile_OnlyName
// - TestUserHandler_UpdateProfile_OnlyEmail
// - TestUserHandler_UpdateProfile_AllFields
// - TestUserHandler_UploadAvatar_ValidImageNilService
// - TestUserHandler_UploadAvatar_ValidJPEGNilService
// These tests verified nil pointer panics, not business logic.

// Tests with mock service

func TestUserHandler_GetProfile_Success(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	req := createAuthenticatedRequest(http.MethodGet, "/api/profile", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.GetProfile(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
	assertBodyContains(t, rr, "email")
}

func TestUserHandler_GetProfile_NotFound(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	// User ID 999 doesn't exist in mock
	req := createAuthenticatedRequest(http.MethodGet, "/api/profile", "", 999, "unknown@example.com", "user")
	rr := httptest.NewRecorder()

	handler.GetProfile(rr, req)

	// Handler returns 500 for user not found
	assertStatusCode(t, rr, http.StatusInternalServerError)
}

func TestUserHandler_UpdateProfile_Success(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	req := createAuthenticatedRequest(http.MethodPut, "/api/profile", `{"name": "Updated Name"}`, 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.UpdateProfile(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestUserHandler_UpdateProfile_NotFound(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	// User ID 999 doesn't exist in mock
	req := createAuthenticatedRequest(http.MethodPut, "/api/profile", `{"name": "Updated Name"}`, 999, "unknown@example.com", "user")
	rr := httptest.NewRecorder()

	handler.UpdateProfile(rr, req)

	// Handler returns 500 for user not found
	assertStatusCode(t, rr, http.StatusInternalServerError)
}

func TestUserHandler_DeleteAvatar_Success(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	req := createAuthenticatedRequest(http.MethodDelete, "/api/profile/avatar", "", 1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.DeleteAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusOK)
}

func TestUserHandler_DeleteAvatar_NotFound(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	// User ID 999 doesn't exist in mock
	req := createAuthenticatedRequest(http.MethodDelete, "/api/profile/avatar", "", 999, "unknown@example.com", "user")
	rr := httptest.NewRecorder()

	handler.DeleteAvatar(rr, req)

	assertStatusCode(t, rr, http.StatusInternalServerError)
}

func TestUserHandler_ChangePassword_WrongOldPassword(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password",
		`{"old_password": "wrongpassword", "new_password": "newpassword123"}`,
		1, "admin@example.com", "admin")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	// Returns 401 when old password doesn't match
	assertStatusCode(t, rr, http.StatusUnauthorized)
	assertBodyContains(t, rr, "Current password is incorrect")
}

func TestUserHandler_ChangePassword_NotFound(t *testing.T) {
	userService := createTestUserService()
	handler := NewUserHandler(userService, nil, createTestLogger())

	// User ID 999 doesn't exist in mock
	req := createAuthenticatedRequest(http.MethodPost, "/api/profile/password",
		`{"old_password": "oldpassword", "new_password": "newpassword123"}`,
		999, "unknown@example.com", "user")
	rr := httptest.NewRecorder()

	handler.ChangePassword(rr, req)

	assertStatusCode(t, rr, http.StatusInternalServerError)
}