aboutsummaryrefslogtreecommitdiffstats
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/Makefile47
-rw-r--r--backend/go.mod6
-rw-r--r--backend/go.sum8
-rw-r--r--backend/handlers/goal_handler.go18
-rw-r--r--backend/handlers/goal_handler_test.go471
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"])
+ }
+ })
+ }
+}