From d251d4165084b337e77be518ed71d90bb25a7a0b Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Thu, 8 May 2025 14:02:58 +0200 Subject: [PATCH] Add POST /user/{userId}/goals (#26) Closes #4 Reviewed-on: https://gitea.seeseepuff.be/seeseemelk/allowance_planner_2000/pulls/26 --- backend/api_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++++- backend/db.go | 44 ++++++++++++++++++++- backend/dto.go | 10 +++++ backend/main.go | 44 ++++++++++++++++++++- 4 files changed, 190 insertions(+), 3 deletions(-) diff --git a/backend/api_test.go b/backend/api_test.go index e3de9f0..48a35c6 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -2,8 +2,9 @@ package main import ( "testing" + + "github.com/gavv/httpexpect/v2" ) -import "github.com/gavv/httpexpect/v2" func startServer(t *testing.T) *httpexpect.Expect { config := ServerConfig{ @@ -56,3 +57,95 @@ func TestGetUserGoalsBadId(t *testing.T) { e := startServer(t) e.GET("/user/bad-id/goals").Expect().Status(400) } + +func TestCreateUserGoal(t *testing.T) { + e := startServer(t) + + // Create a new goal + requestBody := map[string]interface{}{ + "name": "Test Goal", + "target": 5000, + "weight": 10, + } + + response := e.POST("/user/1/goals"). + WithJSON(requestBody). + Expect(). + Status(201). + JSON().Object() + + // Verify the response has an ID + response.ContainsKey("id") + goalId := response.Value("id").Number().Raw() + + // Verify the goal exists in the list of goals + goals := e.GET("/user/1/goals"). + Expect(). + Status(200). + JSON().Array() + + goals.Length().IsEqual(1) + + goal := goals.Value(0).Object() + goal.Value("id").IsEqual(goalId) + goal.Value("name").IsEqual("Test Goal") + goal.Value("target").IsEqual(5000) + goal.Value("weight").IsEqual(10) + goal.Value("progress").IsEqual(0) +} + +func TestCreateUserGoalNoUser(t *testing.T) { + e := startServer(t) + + requestBody := map[string]interface{}{ + "name": "Test Goal", + "target": 5000, + "weight": 10, + } + + e.POST("/user/999/goals"). + WithJSON(requestBody). + Expect(). + Status(404) +} + +func TestCreateUserGoalInvalidInput(t *testing.T) { + e := startServer(t) + + // Test with empty name + requestBody := map[string]interface{}{ + "name": "", + "target": 5000, + "weight": 10, + } + + e.POST("/user/1/goals"). + WithJSON(requestBody). + Expect(). + Status(400) + + // Test with missing fields + invalidRequest := map[string]interface{}{ + "target": 5000, + } + + e.POST("/user/1/goals"). + WithJSON(invalidRequest). + Expect(). + Status(400) +} + +func TestCreateUserGoalBadId(t *testing.T) { + e := startServer(t) + + requestBody := map[string]interface{}{ + "name": "Test Goal", + "target": 5000, + "weight": 10, + } + + e.POST("/user/bad-id/goals"). + WithJSON(requestBody). + Expect(). + Status(400) +} diff --git a/backend/db.go b/backend/db.go index 06fe17a..9f2c0f6 100644 --- a/backend/db.go +++ b/backend/db.go @@ -2,8 +2,9 @@ package main import ( "errors" - "gitea.seeseepuff.be/seeseemelk/mysqlite" "log" + + "gitea.seeseepuff.be/seeseemelk/mysqlite" ) type Db struct { @@ -86,3 +87,44 @@ func (db *Db) GetUserGoals(userId int) ([]Goal, error) { } return goals, nil } + +func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) { + // Check if user exists before attempting to create a goal + exists, err := db.UserExists(userId) + if err != nil { + return 0, err + } + if !exists { + return 0, errors.New("user does not exist") + } + + tx, err := db.db.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + // Insert the new goal + err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)"). + Bind(userId, goal.Name, goal.Target, goal.Weight). + Exec() + + if err != nil { + return 0, err + } + + // Get the last inserted ID + var lastId int + err = tx.Query("select last_insert_rowid()").ScanSingle(&lastId) + if err != nil { + return 0, err + } + + // Commit the transaction + err = tx.Commit() + if err != nil { + return 0, err + } + + return lastId, nil +} diff --git a/backend/dto.go b/backend/dto.go index 9d9ab5c..1b7ae67 100644 --- a/backend/dto.go +++ b/backend/dto.go @@ -24,3 +24,13 @@ type Goal struct { Progress int `json:"progress"` Weight int `json:"weight"` } + +type CreateGoalRequest struct { + Name string `json:"name"` + Target int `json:"target"` + Weight int `json:"weight"` +} + +type CreateGoalResponse struct { + ID int `json:"id"` +} diff --git a/backend/main.go b/backend/main.go index 3b30693..848d130 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,11 +4,12 @@ import ( "context" "embed" "errors" - "github.com/gin-gonic/gin" "log" "net/http" "os" "strconv" + + "github.com/gin-gonic/gin" ) //go:embed migrations/*.sql @@ -92,6 +93,46 @@ func getUserGoals(c *gin.Context) { c.IndentedJSON(http.StatusOK, goals) } +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"}) + return + } + + // Parse request body + var goalRequest CreateGoalRequest + if err := c.ShouldBindJSON(&goalRequest); err != nil { + log.Printf("Error parsing request body: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // Validate request + if goalRequest.Name == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Goal name cannot be empty"}) + return + } + + // Create goal in database + goalId, err := db.CreateGoal(userId, &goalRequest) + 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"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": "Could not create goal"}) + } + return + } + + // Return created goal ID + response := CreateGoalResponse{ID: goalId} + c.IndentedJSON(http.StatusCreated, response) +} + /* * Initialises the database, and then starts the server. @@ -105,6 +146,7 @@ func start(ctx context.Context, config *ServerConfig) { router.GET("/api/users", getUsers) router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId/goals", getUserGoals) + router.POST("/api/user/:userId/goals", createUserGoal) srv := &http.Server{ Addr: ":" + config.Port,