diff --git a/backend/api_test.go b/backend/api_test.go index 48a35c6..5884042 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -1,11 +1,17 @@ package main import ( + "strconv" "testing" "github.com/gavv/httpexpect/v2" ) +const ( + TestGoalName = "Test Goal" + UserGoalsURL = "/user/1/goals" +) + func startServer(t *testing.T) *httpexpect.Expect { config := ServerConfig{ Datasource: ":memory:", @@ -44,7 +50,7 @@ func TestGetUserBadId(t *testing.T) { func TestGetUserGoalsWhenNoGoalsPresent(t *testing.T) { e := startServer(t) - result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array() + result := e.GET(UserGoalsURL).Expect().Status(200).JSON().Array() result.Length().IsEqual(0) } @@ -63,12 +69,12 @@ func TestCreateUserGoal(t *testing.T) { // Create a new goal requestBody := map[string]interface{}{ - "name": "Test Goal", + "name": TestGoalName, "target": 5000, "weight": 10, } - response := e.POST("/user/1/goals"). + response := e.POST(UserGoalsURL). WithJSON(requestBody). Expect(). Status(201). @@ -79,7 +85,7 @@ func TestCreateUserGoal(t *testing.T) { goalId := response.Value("id").Number().Raw() // Verify the goal exists in the list of goals - goals := e.GET("/user/1/goals"). + goals := e.GET(UserGoalsURL). Expect(). Status(200). JSON().Array() @@ -88,7 +94,7 @@ func TestCreateUserGoal(t *testing.T) { goal := goals.Value(0).Object() goal.Value("id").IsEqual(goalId) - goal.Value("name").IsEqual("Test Goal") + goal.Value("name").IsEqual(TestGoalName) goal.Value("target").IsEqual(5000) goal.Value("weight").IsEqual(10) goal.Value("progress").IsEqual(0) @@ -98,7 +104,7 @@ func TestCreateUserGoalNoUser(t *testing.T) { e := startServer(t) requestBody := map[string]interface{}{ - "name": "Test Goal", + "name": TestGoalName, "target": 5000, "weight": 10, } @@ -119,7 +125,7 @@ func TestCreateUserGoalInvalidInput(t *testing.T) { "weight": 10, } - e.POST("/user/1/goals"). + e.POST(UserGoalsURL). WithJSON(requestBody). Expect(). Status(400) @@ -129,7 +135,7 @@ func TestCreateUserGoalInvalidInput(t *testing.T) { "target": 5000, } - e.POST("/user/1/goals"). + e.POST(UserGoalsURL). WithJSON(invalidRequest). Expect(). Status(400) @@ -139,7 +145,7 @@ func TestCreateUserGoalBadId(t *testing.T) { e := startServer(t) requestBody := map[string]interface{}{ - "name": "Test Goal", + "name": TestGoalName, "target": 5000, "weight": 10, } @@ -149,3 +155,54 @@ func TestCreateUserGoalBadId(t *testing.T) { Expect(). Status(400) } + +func TestDeleteUserGoal(t *testing.T) { + e := startServer(t) + + // Create a new goal to delete + createRequest := map[string]interface{}{ + "name": TestGoalName, + "target": 1000, + "weight": 5, + } + response := e.POST(UserGoalsURL). + WithJSON(createRequest). + Expect(). + Status(201). + JSON().Object() + + goalId := response.Value("id").Number().Raw() + + // Delete the goal + e.DELETE("/user/1/goal/" + strconv.Itoa(int(goalId))). + Expect(). + Status(200). + JSON().Object().Value("message").IsEqual("Goal deleted successfully") + + // Verify the goal no longer exists + goals := e.GET(UserGoalsURL). + Expect(). + Status(200). + JSON().Array() + goals.Length().IsEqual(0) +} + +func TestDeleteUserGoalNotFound(t *testing.T) { + e := startServer(t) + + // Attempt to delete a non-existent goal + e.DELETE("/user/1/goal/999"). + Expect(). + Status(404). + JSON().Object().Value("error").IsEqual("Goal not found") +} + +func TestDeleteUserGoalInvalidId(t *testing.T) { + e := startServer(t) + + // Attempt to delete a goal with an invalid ID + e.DELETE("/user/1/goal/invalid-id"). + Expect(). + Status(400). + JSON().Object().Value("error").IsEqual("Invalid goal ID") +} diff --git a/backend/db.go b/backend/db.go index 9f2c0f6..6e68f31 100644 --- a/backend/db.go +++ b/backend/db.go @@ -128,3 +128,25 @@ func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) { return lastId, nil } + +func (db *Db) DeleteGoal(userId int, goalId int) error { + // Check if the goal exists for the user + count := 0 + err := db.db.Query("select count(*) from goals where id = ? and user_id = ?"). + Bind(goalId, userId).ScanSingle(&count) + if err != nil { + return err + } + if count == 0 { + return errors.New("goal not found") + } + + // Delete the goal + err = db.db.Query("delete from goals where id = ? and user_id = ?"). + Bind(goalId, userId).Exec() + if err != nil { + return err + } + + return nil +} diff --git a/backend/main.go b/backend/main.go index 848d130..b3ce37a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -16,6 +16,13 @@ import ( var migrations embed.FS var db *Db +const ( + ErrInternalServerError = "Internal Server Error" + ErrInvalidUserID = "Invalid user ID" + ErrUserNotFound = "User not found" + ErrCheckingUserExist = "Error checking user existence: %v" +) + // ServerConfig holds configuration for the server. type ServerConfig struct { // The datasource to the SQLite database. @@ -34,7 +41,7 @@ func getUsers(c *gin.Context) { users, err := db.GetUsers() if err != nil { log.Printf("Error getting users: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, users) @@ -44,19 +51,19 @@ func getUser(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { - log.Printf("Invalid user ID: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + log.Printf(ErrInvalidUserID+": %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } user, err := db.GetUser(userId) if err != nil { log.Printf("Error getting user: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if user == nil { - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } @@ -67,27 +74,27 @@ func getUserGoals(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { - log.Printf("Invalid user ID: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + log.Printf(ErrInvalidUserID+": %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } exists, err := db.UserExists(userId) if err != nil { - log.Printf("Error checking user existence: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) + log.Printf(ErrCheckingUserExist, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } if !exists { - log.Printf("Error checking user existence: %v", err) - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + log.Printf(ErrCheckingUserExist, err) + c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) return } goals, err := db.GetUserGoals(userId) if err != nil { log.Printf("Error getting user goals: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) return } c.IndentedJSON(http.StatusOK, goals) @@ -97,8 +104,8 @@ func createUserGoal(c *gin.Context) { userIdStr := c.Param("userId") userId, err := strconv.Atoi(userIdStr) if err != nil { - log.Printf("Invalid user ID: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + log.Printf(ErrInvalidUserID+": %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) return } @@ -121,7 +128,7 @@ func createUserGoal(c *gin.Context) { if err != nil { log.Printf("Error creating goal: %v", err) if err.Error() == "user does not exist" { - c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": "Could not create goal"}) } @@ -133,6 +140,49 @@ func createUserGoal(c *gin.Context) { c.IndentedJSON(http.StatusCreated, response) } +func deleteUserGoal(c *gin.Context) { + userIdStr := c.Param("userId") + goalIdStr := c.Param("goalId") + + userId, err := strconv.Atoi(userIdStr) + if err != nil { + log.Printf(ErrInvalidUserID+": %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID}) + return + } + + goalId, err := strconv.Atoi(goalIdStr) + if err != nil { + log.Printf("Invalid goal ID: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"}) + return + } + + exists, err := db.UserExists(userId) + if err != nil { + log.Printf(ErrCheckingUserExist, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) + return + } + if !exists { + c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound}) + return + } + + err = db.DeleteGoal(userId, goalId) + if err != nil { + if err.Error() == "goal not found" { + c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"}) + } else { + log.Printf("Error deleting goal: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"}) +} + /* * Initialises the database, and then starts the server. @@ -147,6 +197,7 @@ func start(ctx context.Context, config *ServerConfig) { router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId/goals", getUserGoals) router.POST("/api/user/:userId/goals", createUserGoal) + router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal) srv := &http.Server{ Addr: ":" + config.Port,