From 3c3eff2d00377cfe357ad76e1c2cd77e6019f3c3 Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Tue, 13 May 2025 10:35:30 +0200
Subject: [PATCH] Add get tasks endpoint

---
 backend/api_test.go | 58 +++++++++++++++++++++++++++++++++++++++++----
 backend/db.go       | 18 ++++++++++++++
 backend/main.go     | 37 ++++++++++++++++++++++-------
 3 files changed, 101 insertions(+), 12 deletions(-)

diff --git a/backend/api_test.go b/backend/api_test.go
index ebb1f66..ef66dde 100644
--- a/backend/api_test.go
+++ b/backend/api_test.go
@@ -1,10 +1,10 @@
 package main
 
 import (
+	"fmt"
+	"github.com/gavv/httpexpect/v2"
 	"strconv"
 	"testing"
-
-	"github.com/gavv/httpexpect/v2"
 )
 
 const (
@@ -14,12 +14,12 @@ const (
 func startServer(t *testing.T) *httpexpect.Expect {
 	config := ServerConfig{
 		Datasource: ":memory:",
-		Port:       "8181",
+		Addr:       ":0",
 		Started:    make(chan bool),
 	}
 	go start(t.Context(), &config)
 	<-config.Started
-	return httpexpect.Default(t, "http://localhost:8181/api")
+	return httpexpect.Default(t, fmt.Sprintf("http://localhost:%d/api", config.Port))
 }
 
 func TestGetUsers(t *testing.T) {
@@ -53,6 +53,29 @@ func TestGetUserGoalsWhenNoGoalsPresent(t *testing.T) {
 	result.Length().IsEqual(0)
 }
 
+func TestGetUserGoals(t *testing.T) {
+	e := startServer(t)
+
+	// Create a new goal
+	requestBody := map[string]interface{}{
+		"name":   TestGoalName,
+		"target": 5000,
+		"weight": 10,
+	}
+	e.POST("/user/1/goals").WithJSON(requestBody).Expect().Status(201)
+
+	// Validate goal
+	result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
+	result.Length().IsEqual(1)
+	item := result.Value(0).Object()
+	item.Value("id").IsEqual(1)
+	item.Value("name").IsEqual(TestGoalName)
+	item.Value("target").IsEqual(5000)
+	item.Value("weight").IsEqual(10)
+	item.Value("progress").IsEqual(0)
+	item.NotContainsKey("user_id")
+}
+
 func TestGetUserGoalsNoUser(t *testing.T) {
 	e := startServer(t)
 	e.GET("/user/999/goals").Expect().Status(404)
@@ -282,3 +305,30 @@ func TestCreateTaskInvalidRequestBody(t *testing.T) {
 		Expect().
 		Status(400)
 }
+
+func TestGetTaskWhenNoTasks(t *testing.T) {
+	e := startServer(t)
+
+	result := e.GET("/tasks").Expect().Status(200).JSON().Array()
+	result.Length().IsEqual(0)
+}
+
+func TestGetTaskWhenTasks(t *testing.T) {
+	e := startServer(t)
+
+	// Create a new task
+	requestBody := map[string]interface{}{
+		"name":   "Test Task",
+		"reward": 100,
+	}
+	e.POST("/tasks").WithJSON(requestBody).Expect().Status(201)
+
+	// Get the task
+	result := e.GET("/tasks").Expect().Status(200).JSON().Array()
+	result.Length().IsEqual(1)
+	item := result.Value(0).Object()
+	item.Value("id").IsEqual(1)
+	item.Value("name").IsEqual("Test Task")
+	item.Value("reward").IsEqual(100)
+	item.Value("assigned").IsNull()
+}
diff --git a/backend/db.go b/backend/db.go
index a5ce545..9a7b934 100644
--- a/backend/db.go
+++ b/backend/db.go
@@ -182,3 +182,21 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
 
 	return lastId, nil
 }
+
+func (db *Db) GetTasks() ([]Task, error) {
+	tasks := make([]Task, 0)
+	var err error
+
+	for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) {
+		task := Task{}
+		err = row.Scan(&task.ID, &task.Name, &task.Reward, &task.Assigned)
+		if err != nil {
+			return nil, err
+		}
+		tasks = append(tasks, task)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return tasks, nil
+}
diff --git a/backend/main.go b/backend/main.go
index e4c36c7..c8506f9 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -3,8 +3,8 @@ package main
 import (
 	"context"
 	"embed"
-	"errors"
 	"log"
+	"net"
 	"net/http"
 	"os"
 	"strconv"
@@ -31,7 +31,10 @@ type ServerConfig struct {
 
 	// The port to listen on.
 	// Use an empty string to listen on a random port.
-	Port string
+	Addr string
+
+	// The port that is actually being listened on.
+	Port int
 
 	// The channel that gets signaled when the server has started.
 	Started chan bool
@@ -221,6 +224,16 @@ func createTask(c *gin.Context) {
 	c.IndentedJSON(http.StatusCreated, response)
 }
 
+func getTasks(c *gin.Context) {
+	response, err := db.GetTasks()
+	if err != nil {
+		log.Printf("Error getting tasks: %v", err)
+		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
+		return
+	}
+	c.JSON(http.StatusOK, &response)
+}
+
 /*
 *
 Initialises the database, and then starts the server.
@@ -237,21 +250,29 @@ func start(ctx context.Context, config *ServerConfig) {
 	router.POST("/api/user/:userId/goals", createUserGoal)
 	router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal)
 	router.POST("/api/tasks", createTask)
+	router.GET("/api/tasks", getTasks)
 
 	srv := &http.Server{
-		Addr:    ":" + config.Port,
+		Addr:    config.Addr,
 		Handler: router.Handler(),
 	}
 	go func() {
-		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+		l, err := net.Listen("tcp", srv.Addr)
+		if err != nil {
+			log.Fatalf("listen: %s\n", err)
+		}
+		config.Port = l.Addr().(*net.TCPAddr).Port
+
+		log.Printf("Running server on port %s\n", l.Addr().String())
+		if config.Started != nil {
+			config.Started <- true
+		}
+
+		if err := http.Serve(l, router.Handler()); err != nil {
 			log.Fatalf("listen: %s\n", err)
 		}
 	}()
 
-	log.Printf("Running server on port %s\n", config.Port)
-	if config.Started != nil {
-		config.Started <- true
-	}
 	<-ctx.Done()
 	log.Println("Shutting down")
 	if err := srv.Shutdown(context.Background()); err != nil {