diff options
Diffstat (limited to 'backend')
-rw-r--r-- | backend/Makefile | 47 | ||||
-rw-r--r-- | backend/go.mod | 6 | ||||
-rw-r--r-- | backend/go.sum | 8 | ||||
-rw-r--r-- | backend/handlers/goal_handler.go | 18 | ||||
-rw-r--r-- | backend/handlers/goal_handler_test.go | 471 |
5 files changed, 543 insertions, 7 deletions
diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..0506ee3 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,47 @@ +# Makefile for the finance backend application + +# Variables +BINARY_NAME=finance-backend +GO=go +GOTEST=$(GO) test +GOVET=$(GO) vet +GOBUILD=$(GO) build + +.PHONY: all build clean test vet run lint + +# Default target +all: test build + +# Build the application +build: + $(GOBUILD) -o $(BINARY_NAME) ./cmd/api/main.go + +# Clean build files +clean: + rm -f $(BINARY_NAME) + +# Run the application +run: build + ./$(BINARY_NAME) + +# Run all tests with verbose output +test: + $(GOTEST) -v ./... + +# Test only the handlers +test-handlers: + $(GOTEST) -v ./handlers/... + +# Run code vetting +vet: + $(GOVET) ./... + +# Run linting (requires golint) +lint: + @command -v golint >/dev/null 2>&1 || { echo >&2 "golint not installed. Installing..."; $(GO) install golang.org/x/lint/golint@latest; } + golint ./... + +# Run tests with coverage report +test-coverage: + $(GOTEST) -coverprofile=coverage.out ./... + $(GO) tool cover -html=coverage.out
\ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index 1de9ec7..54ed412 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,6 +7,7 @@ require ( github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/cors v1.7.5 // indirect github.com/gin-contrib/sse v1.0.0 // indirect @@ -27,9 +28,13 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.15.0 // indirect @@ -41,5 +46,6 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/postgres v1.5.11 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect gorm.io/gorm v1.25.12 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 948fa7e..b374bcb 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -13,6 +13,7 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= @@ -67,6 +68,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -76,10 +79,12 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -88,6 +93,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -130,6 +136,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 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"]) + } + }) + } +} |