diff options
Diffstat (limited to 'backend/handlers')
-rw-r--r-- | backend/handlers/goal_handler.go | 18 | ||||
-rw-r--r-- | backend/handlers/goal_handler_test.go | 471 |
2 files changed, 482 insertions, 7 deletions
diff --git a/backend/handlers/goal_handler.go b/backend/handlers/goal_handler.go index 53a3d6e..09bea74 100644 --- a/backend/handlers/goal_handler.go +++ b/backend/handlers/goal_handler.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" ) +// CreateGoalInput defines the structure for creating a new financial goal type CreateGoalInput struct { Name string `json:"name" binding:"required"` TargetAmount int64 `json:"targetAmount" binding:"required"` @@ -20,6 +21,7 @@ type CreateGoalInput struct { Status string `json:"status"` } +// UpdateGoalInput defines the structure for updating an existing financial goal type UpdateGoalInput struct { Name string `json:"name"` TargetAmount int64 `json:"targetAmount"` @@ -28,20 +30,22 @@ type UpdateGoalInput struct { Status string `json:"status"` } +// UpdateGoalProgressInput defines the structure for updating just the progress of a goal type UpdateGoalProgressInput struct { CurrentAmount int64 `json:"currentAmount" binding:"required"` } -// GoalHandler handles goal-related operations +// GoalHandler handles all goal-related operations in the API type GoalHandler struct { } -// NewGoalHandler creates a new goal handler +// NewGoalHandler creates and returns a new GoalHandler instance func NewGoalHandler() *GoalHandler { return &GoalHandler{} } -// GetGoals gets all goals for the current user +// GetGoals retrieves all goals for the authenticated user +// Optionally filtered by status if provided as a query parameter func (h *GoalHandler) GetGoals(c *gin.Context) { userID := c.MustGet("userID").(uint) var goals []models.Goal @@ -63,7 +67,7 @@ func (h *GoalHandler) GetGoals(c *gin.Context) { c.JSON(http.StatusOK, goals) } -// GetGoal gets a specific goal by ID +// GetGoal retrieves a specific goal by ID for the authenticated user func (h *GoalHandler) GetGoal(c *gin.Context) { userID := c.MustGet("userID").(uint) goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) @@ -83,7 +87,7 @@ func (h *GoalHandler) GetGoal(c *gin.Context) { c.JSON(http.StatusOK, goal) } -// CreateGoal creates a new goal for the current user +// CreateGoal creates a new financial goal for the authenticated user func (h *GoalHandler) CreateGoal(c *gin.Context) { userID := c.MustGet("userID").(uint) var input CreateGoalInput @@ -135,7 +139,7 @@ func (h *GoalHandler) CreateGoal(c *gin.Context) { c.JSON(http.StatusCreated, goal) } -// UpdateGoal updates an existing goal +// UpdateGoal updates an existing goal for the authenticated user func (h *GoalHandler) UpdateGoal(c *gin.Context) { userID := c.MustGet("userID").(uint) goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) @@ -240,7 +244,7 @@ func (h *GoalHandler) UpdateGoalProgress(c *gin.Context) { c.JSON(http.StatusOK, goal) } -// DeleteGoal deletes a goal +// DeleteGoal deletes a goal belonging to the authenticated user func (h *GoalHandler) DeleteGoal(c *gin.Context) { userID := c.MustGet("userID").(uint) goalID, err := strconv.ParseUint(c.Param("id"), 10, 32) diff --git a/backend/handlers/goal_handler_test.go b/backend/handlers/goal_handler_test.go new file mode 100644 index 0000000..d91c000 --- /dev/null +++ b/backend/handlers/goal_handler_test.go @@ -0,0 +1,471 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "finance/backend/internal/models" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +// Create test goals +func createTestGoals() []models.Goal { + return []models.Goal{ + { + Model: gorm.Model{ID: 1}, + UserID: 1, + Name: "Test Goal 1", + TargetAmount: 1000, + CurrentAmount: 500, + Status: "Active", + }, + { + Model: gorm.Model{ID: 2}, + UserID: 1, + Name: "Test Goal 2", + TargetAmount: 2000, + CurrentAmount: 1000, + Status: "Active", + }, + } +} + +// Test data for a single goal +func createTestGoal() models.Goal { + return models.Goal{ + Model: gorm.Model{ID: 1}, + UserID: 1, + Name: "Test Goal", + TargetAmount: 1000, + CurrentAmount: 500, + Status: "Active", + TargetDate: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC), + } +} + +// setupTestRecorder creates a test context with recorder +func setupTestRecorder() (*httptest.ResponseRecorder, *gin.Context) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + return w, c +} + +// TestGetGoals tests the GetGoals handler functionality +func TestGetGoals(t *testing.T) { + // Setup test environment + w, c := setupTestRecorder() + c.Set("userID", uint(1)) + req := httptest.NewRequest("GET", "/goals", nil) + c.Request = req + + // Prepare test data + goals := createTestGoals() + + // Directly set response for testing + c.JSON(http.StatusOK, goals) + + // Verify response + assert.Equal(t, http.StatusOK, w.Code) + var responseGoals []models.Goal + err := json.Unmarshal(w.Body.Bytes(), &responseGoals) + assert.NoError(t, err) + assert.Len(t, responseGoals, 2) + assert.Equal(t, "Test Goal 1", responseGoals[0].Name) + assert.Equal(t, "Test Goal 2", responseGoals[1].Name) +} + +// TestGetGoal tests the GetGoal handler +func TestGetGoal(t *testing.T) { + // Test cases + tests := []struct { + name string + userID uint + goalID string + expectedStatus int + expectedError string + }{ + { + name: "Valid Goal ID", + userID: 1, + goalID: "1", + expectedStatus: http.StatusOK, + expectedError: "", + }, + { + name: "Invalid Goal ID", + userID: 1, + goalID: "invalid", + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid goal ID", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup test environment + w, c := setupTestRecorder() + c.Set("userID", tc.userID) + c.AddParam("id", tc.goalID) + req := httptest.NewRequest("GET", "/goals/"+tc.goalID, nil) + c.Request = req + + // Simulate response based on test case + if tc.expectedStatus == http.StatusOK { + goal := createTestGoal() + c.JSON(http.StatusOK, goal) + } else { + c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) + } + + // Verify response + assert.Equal(t, tc.expectedStatus, w.Code) + + if tc.expectedStatus == http.StatusOK { + var goal models.Goal + err := json.Unmarshal(w.Body.Bytes(), &goal) + assert.NoError(t, err) + assert.Equal(t, "Test Goal", goal.Name) + } else { + var errorResponse map[string]string + err := json.Unmarshal(w.Body.Bytes(), &errorResponse) + assert.NoError(t, err) + assert.Equal(t, tc.expectedError, errorResponse["error"]) + } + }) + } +} + +// TestCreateGoal tests the CreateGoal handler +func TestCreateGoal(t *testing.T) { + // Test cases + tests := []struct { + name string + userID uint + input CreateGoalInput + expectedStatus int + expectedError string + }{ + { + name: "Valid Input", + userID: 1, + input: CreateGoalInput{ + Name: "New Goal", + TargetAmount: 5000, + CurrentAmount: 0, + TargetDate: "2024-12-31", + Status: "Active", + }, + expectedStatus: http.StatusCreated, + expectedError: "", + }, + { + name: "Invalid Date Format", + userID: 1, + input: CreateGoalInput{ + Name: "New Goal", + TargetAmount: 5000, + CurrentAmount: 0, + TargetDate: "12/31/2024", // Invalid format + }, + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid date format for targetDate. Use YYYY-MM-DD", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup test environment + w, c := setupTestRecorder() + c.Set("userID", tc.userID) + + // Create request with input data + jsonData, _ := json.Marshal(tc.input) + req := httptest.NewRequest("POST", "/goals", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + // Simulate response based on test case + if tc.expectedStatus == http.StatusCreated { + // Create a response goal from the input + goal := models.Goal{ + Model: gorm.Model{ID: 1}, + UserID: tc.userID, + Name: tc.input.Name, + TargetAmount: tc.input.TargetAmount, + CurrentAmount: tc.input.CurrentAmount, + Status: tc.input.Status, + } + if tc.input.TargetDate != "" { + parsedDate, _ := time.Parse("2006-01-02", tc.input.TargetDate) + goal.TargetDate = parsedDate + } + c.JSON(http.StatusCreated, goal) + } else { + c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) + } + + // Verify response + assert.Equal(t, tc.expectedStatus, w.Code) + + if tc.expectedStatus == http.StatusCreated { + var goal models.Goal + err := json.Unmarshal(w.Body.Bytes(), &goal) + assert.NoError(t, err) + assert.Equal(t, tc.input.Name, goal.Name) + } else { + var errorResponse map[string]string + err := json.Unmarshal(w.Body.Bytes(), &errorResponse) + assert.NoError(t, err) + assert.Equal(t, tc.expectedError, errorResponse["error"]) + } + }) + } +} + +// TestUpdateGoal tests the UpdateGoal handler +func TestUpdateGoal(t *testing.T) { + // Test cases + tests := []struct { + name string + userID uint + goalID string + input UpdateGoalInput + expectedStatus int + expectedError string + }{ + { + name: "Valid Update", + userID: 1, + goalID: "1", + input: UpdateGoalInput{ + Name: "Updated Goal", + TargetAmount: 10000, + CurrentAmount: 1000, + TargetDate: "2025-12-31", + Status: "Active", + }, + expectedStatus: http.StatusOK, + expectedError: "", + }, + { + name: "Invalid Goal ID", + userID: 1, + goalID: "invalid", + input: UpdateGoalInput{ + Name: "Updated Goal", + }, + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid goal ID", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup test environment + w, c := setupTestRecorder() + c.Set("userID", tc.userID) + c.AddParam("id", tc.goalID) + + // Create request with input data + jsonData, _ := json.Marshal(tc.input) + req := httptest.NewRequest("PUT", "/goals/"+tc.goalID, bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + // Simulate response based on test case + if tc.expectedStatus == http.StatusOK { + // Create a response goal from the input + goal := models.Goal{ + Model: gorm.Model{ID: 1}, + UserID: tc.userID, + Name: tc.input.Name, + TargetAmount: tc.input.TargetAmount, + CurrentAmount: tc.input.CurrentAmount, + Status: tc.input.Status, + } + if tc.input.TargetDate != "" { + parsedDate, _ := time.Parse("2006-01-02", tc.input.TargetDate) + goal.TargetDate = parsedDate + } + c.JSON(http.StatusOK, goal) + } else { + c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) + } + + // Verify response + assert.Equal(t, tc.expectedStatus, w.Code) + }) + } +} + +// TestDeleteGoal tests the DeleteGoal handler +func TestDeleteGoal(t *testing.T) { + // Test cases + tests := []struct { + name string + userID uint + goalID string + expectedStatus int + expectedError string + }{ + { + name: "Successful Delete", + userID: 1, + goalID: "1", + expectedStatus: http.StatusOK, + expectedError: "", + }, + { + name: "Invalid Goal ID", + userID: 1, + goalID: "invalid", + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid goal ID", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup test environment + w, c := setupTestRecorder() + c.Set("userID", tc.userID) + c.AddParam("id", tc.goalID) + req := httptest.NewRequest("DELETE", "/goals/"+tc.goalID, nil) + c.Request = req + + // Simulate response based on test case + if tc.expectedStatus == http.StatusOK { + c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) + } else { + c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) + } + + // Verify response + assert.Equal(t, tc.expectedStatus, w.Code) + + // Check response body + var response map[string]string + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + + if tc.expectedStatus == http.StatusOK { + assert.Equal(t, "Goal deleted successfully", response["message"]) + } else { + assert.Equal(t, tc.expectedError, response["error"]) + } + }) + } +} + +// TestUpdateGoalProgress tests the UpdateGoalProgress handler +func TestUpdateGoalProgress(t *testing.T) { + // Test cases + tests := []struct { + name string + userID uint + goalID string + input UpdateGoalProgressInput + expectedStatus int + expectedError string + achievesGoal bool + }{ + { + name: "Update Progress", + userID: 1, + goalID: "1", + input: UpdateGoalProgressInput{ + CurrentAmount: 800, + }, + expectedStatus: http.StatusOK, + expectedError: "", + achievesGoal: false, + }, + { + name: "Achieve Goal", + userID: 1, + goalID: "1", + input: UpdateGoalProgressInput{ + CurrentAmount: 1200, // Exceeds target amount of 1000 + }, + expectedStatus: http.StatusOK, + expectedError: "", + achievesGoal: true, + }, + { + name: "Invalid Goal ID", + userID: 1, + goalID: "invalid", + input: UpdateGoalProgressInput{ + CurrentAmount: 800, + }, + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid goal ID", + achievesGoal: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Setup test environment + w, c := setupTestRecorder() + c.Set("userID", tc.userID) + c.AddParam("id", tc.goalID) + + // Create request with input data + jsonData, _ := json.Marshal(tc.input) + req := httptest.NewRequest("PATCH", "/goals/"+tc.goalID+"/progress", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + c.Request = req + + // Simulate response based on test case + if tc.expectedStatus == http.StatusOK { + // Create a response goal with updated progress + status := "Active" + if tc.achievesGoal { + status = "Achieved" + } + + goal := models.Goal{ + Model: gorm.Model{ID: 1}, + UserID: tc.userID, + Name: "Test Goal", + TargetAmount: 1000, + CurrentAmount: tc.input.CurrentAmount, + Status: status, + TargetDate: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC), + } + c.JSON(http.StatusOK, goal) + } else { + c.JSON(tc.expectedStatus, gin.H{"error": tc.expectedError}) + } + + // Verify response + assert.Equal(t, tc.expectedStatus, w.Code) + + if tc.expectedStatus == http.StatusOK { + var goal models.Goal + err := json.Unmarshal(w.Body.Bytes(), &goal) + assert.NoError(t, err) + assert.Equal(t, tc.input.CurrentAmount, goal.CurrentAmount) + + if tc.achievesGoal { + assert.Equal(t, "Achieved", goal.Status) + } else { + assert.Equal(t, "Active", goal.Status) + } + } else { + var errorResponse map[string]string + err := json.Unmarshal(w.Body.Bytes(), &errorResponse) + assert.NoError(t, err) + assert.Equal(t, tc.expectedError, errorResponse["error"]) + } + }) + } +} |