package main import ( "fmt" "github.com/gavv/httpexpect/v2" "strconv" "testing" "time" ) const ( TestAllowanceName = "Test History" ) func startServer(t *testing.T) *httpexpect.Expect { config := ServerConfig{ Datasource: ":memory:", Addr: ":0", Started: make(chan bool), } go start(t.Context(), &config) <-config.Started return httpexpect.Default(t, fmt.Sprintf("http://localhost:%d/api", config.Port)) } func TestGetUsers(t *testing.T) { e := startServer(t) result := e.GET("/users").Expect().Status(200).JSON() result.Array().Length().IsEqual(2) result.Path("$[0].name").InList("Seeseemelk", "Huffle") result.Path("$[1].name").InList("Seeseemelk", "Huffle") } func TestGetUser(t *testing.T) { e := startServer(t) result := e.GET("/user/1").Expect().Status(200).JSON().Object() result.Value("name").IsEqual("Seeseemelk") result.Value("id").IsEqual(1) result.Value("allowance").IsEqual(0) } func TestGetUserUnknown(t *testing.T) { e := startServer(t) e.GET("/user/999").Expect().Status(404) } func TestGetUserBadId(t *testing.T) { e := startServer(t) e.GET("/user/bad-id").Expect().Status(400) } func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) { e := startServer(t) result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result.Length().IsEqual(1) item := result.Value(0).Object() item.Value("id").IsEqual(0) } func TestGetUserAllowance(t *testing.T) { e := startServer(t) // Create a new allowance requestBody := map[string]interface{}{ "name": TestAllowanceName, "target": 5000, "weight": 10, } e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201) // Validate allowance result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result.Length().IsEqual(2) item := result.Value(1).Object() item.Value("id").IsEqual(1) item.Value("name").IsEqual(TestAllowanceName) item.Value("target").IsEqual(5000) item.Value("weight").IsEqual(10) item.Value("progress").IsEqual(0) item.NotContainsKey("user_id") } func TestGetUserAllowanceNoUser(t *testing.T) { e := startServer(t) e.GET("/user/999/allowance").Expect().Status(404) } func TestGetUserAllowanceBadId(t *testing.T) { e := startServer(t) e.GET("/user/bad-id/allowance").Expect().Status(400) } func TestCreateUserAllowance(t *testing.T) { e := startServer(t) // Create a new allowance requestBody := map[string]interface{}{ "name": TestAllowanceName, "target": 5000, "weight": 10, } response := e.POST("/user/1/allowance"). WithJSON(requestBody). Expect(). Status(201). JSON().Object() // Verify the response has an ID response.ContainsKey("id") allowanceId := response.Value("id").Number().Raw() // Verify the allowance exists in the list of allowances allowances := e.GET("/user/1/allowance"). Expect(). Status(200). JSON().Array() allowances.Length().IsEqual(2) allowance := allowances.Value(1).Object() allowance.Value("id").IsEqual(allowanceId) allowance.Value("name").IsEqual(TestAllowanceName) allowance.Value("target").IsEqual(5000) allowance.Value("weight").IsEqual(10) allowance.Value("progress").IsEqual(0) } func TestCreateUserAllowanceNoUser(t *testing.T) { e := startServer(t) requestBody := map[string]interface{}{ "name": TestAllowanceName, "target": 5000, "weight": 10, } e.POST("/user/999/allowance"). WithJSON(requestBody). Expect(). Status(404) } func TestCreateUserAllowanceInvalidInput(t *testing.T) { e := startServer(t) // Test with empty name requestBody := map[string]interface{}{ "name": "", "target": 5000, "weight": 10, } e.POST("/user/1/allowance"). WithJSON(requestBody). Expect(). Status(400) // Test with missing fields invalidRequest := map[string]interface{}{ "target": 5000, } e.POST("/user/1/allowance"). WithJSON(invalidRequest). Expect(). Status(400) } func TestCreateUserAllowanceBadId(t *testing.T) { e := startServer(t) requestBody := map[string]interface{}{ "name": TestAllowanceName, "target": 5000, "weight": 10, } e.POST("/user/bad-id/allowance"). WithJSON(requestBody). Expect(). Status(400) } func TestDeleteUserAllowance(t *testing.T) { e := startServer(t) // Create a new allowance to delete createRequest := map[string]interface{}{ "name": TestAllowanceName, "target": 1000, "weight": 5, } response := e.POST("/user/1/allowance"). WithJSON(createRequest). Expect(). Status(201). JSON().Object() allowanceId := response.Value("id").Number().Raw() // Delete the allowance e.DELETE("/user/1/allowance/" + strconv.Itoa(int(allowanceId))). Expect(). Status(200). JSON().Object().Value("message").IsEqual("History deleted successfully") // Verify the allowance no longer exists allowances := e.GET("/user/1/allowance"). Expect(). Status(200). JSON().Array() allowances.Length().IsEqual(1) } func TestDeleteUserRestAllowance(t *testing.T) { e := startServer(t) e.DELETE("/user/1/allowance/0").Expect().Status(400) } func TestDeleteUserAllowanceNotFound(t *testing.T) { e := startServer(t) // Attempt to delete a non-existent allowance e.DELETE("/user/1/allowance/999"). Expect(). Status(404). JSON().Object().Value("error").IsEqual("History not found") } func TestDeleteUserAllowanceInvalidId(t *testing.T) { e := startServer(t) // Attempt to delete an allowance with an invalid ID e.DELETE("/user/1/allowance/invalid-id"). Expect(). Status(400). JSON().Object().Value("error").IsEqual("Invalid allowance ID") } func TestCreateTask(t *testing.T) { e := startServer(t) // Create a new task without assigned user requestBody := map[string]interface{}{ "name": "Test Task", "reward": 100, } response := e.POST("/tasks"). WithJSON(requestBody). Expect(). Status(201). // Expect Created status JSON().Object() // Verify the response has an ID response.ContainsKey("id") response.Value("id").Number().IsEqual(1) e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1) // Get task result := e.GET("/task/1").Expect().Status(200).JSON().Object() result.Value("id").IsEqual(1) result.Value("name").IsEqual("Test Task") result.Value("reward").IsEqual(100) result.Value("assigned").IsNull() // Create a new task with assigned user assignedUserId := 1 requestBodyWithUser := map[string]interface{}{ "name": "Test Task Assigned", "reward": 200, "assigned": assignedUserId, } responseWithUser := e.POST("/tasks"). WithJSON(requestBodyWithUser). Expect(). Status(201). JSON().Object() responseWithUser.ContainsKey("id") responseWithUser.Value("id").Number().IsEqual(2) } func TestDeleteTask(t *testing.T) { e := startServer(t) // Create a new task without assigned user requestBody := map[string]interface{}{ "name": "Test Task", "reward": 100, } response := e.POST("/tasks"). WithJSON(requestBody). Expect(). Status(201). // Expect Created status JSON().Object() // Verify the response has an ID response.ContainsKey("id") taskId := response.Value("id").Number().Raw() // Delete the task e.DELETE("/task/" + strconv.Itoa(int(taskId))).Expect().Status(200) // Verify the task no longer exists e.GET("/task/" + strconv.Itoa(int(taskId))).Expect().Status(404) } func TestDeleteTaskNotFound(t *testing.T) { e := startServer(t) e.DELETE("/task/1").Expect().Status(404) } func TestCreateTaskNoName(t *testing.T) { e := startServer(t) requestBody := map[string]interface{}{ "reward": 100, } e.POST("/tasks").WithJSON(requestBody).Expect().Status(400) } func TestCreateTaskInvalidAssignedUser(t *testing.T) { e := startServer(t) requestBody := map[string]interface{}{ "name": "Test Task Invalid User", "reward": 100, "assigned": 999, // Non-existent user ID } e.POST("/tasks"). WithJSON(requestBody). Expect(). Status(404). // Expect Not Found JSON().Object().Value("error").IsEqual(ErrUserNotFound) } func TestCreateTaskInvalidRequestBody(t *testing.T) { e := startServer(t) // Test with missing fields (name is required) invalidRequest := map[string]interface{}{ "reward": 5000, } e.POST("/tasks"). WithJSON(invalidRequest). 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 createTestTaskWithAmount(e *httpexpect.Expect, amount int) int { requestBody := map[string]interface{}{ "name": "Test Task", "reward": amount, } return int(e.POST("/tasks").WithJSON(requestBody).Expect().Status(201).JSON().Object().Value("id").Number().Raw()) } func TestGetTasksWhenTasks(t *testing.T) { e := startServer(t) createTestTask(e) // 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() } func TestGetTask(t *testing.T) { e := startServer(t) createTestTask(e) result := e.GET("/task/1").Expect().Status(200).JSON().Object() result.Value("id").IsEqual(1) result.Value("name").IsEqual("Test Task") result.Value("reward").IsEqual(100) result.Value("assigned").IsNull() } func TestGetTaskInvalidId(t *testing.T) { e := startServer(t) createTestTask(e) e.GET("/task/2").Expect().Status(404) } func TestGetTaskBadId(t *testing.T) { e := startServer(t) createTestTask(e) e.GET("/task/invalid").Expect().Status(400) } func TestPutTaskModifiesTask(t *testing.T) { e := startServer(t) createTestTask(e) requestBody := map[string]interface{}{ "name": "Updated Task", "reward": 100, } e.PUT("/task/1").WithJSON(requestBody).Expect(). Status(200). JSON().Object() // Verify the task is updated result := e.GET("/task/1").Expect().Status(200).JSON().Object() result.Value("id").IsEqual(1) result.Value("name").IsEqual("Updated Task") result.Value("reward").IsEqual(100) } func TestPutTaskInvalidTaskId(t *testing.T) { e := startServer(t) createTestTask(e) requestBody := map[string]interface{}{ "name": "Updated Task", } e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404) } func TestPostAllowance(t *testing.T) { e := startServer(t) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200) response := e.GET("/user/1").Expect().Status(200).JSON().Object() response.Value("allowance").Number().IsEqual(100 + 20 - 10) } func TestPostAllowanceInvalidUserId(t *testing.T) { e := startServer(t) e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).Expect(). Status(404) } func TestGetHistory(t *testing.T) { e := startServer(t) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200) response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() response.Length().IsEqual(3) response.Value(0).Object().Value("allowance").Number().IsEqual(100) response.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) response.Value(1).Object().Value("allowance").Number().IsEqual(20) response.Value(2).Object().Value("allowance").Number().IsEqual(-10) } func TestGetUserAllowanceById(t *testing.T) { e := startServer(t) // Create a new allowance requestBody := map[string]interface{}{ "name": TestAllowanceName, "target": 5000, "weight": 10, } resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() allowanceId := int(resp.Value("id").Number().Raw()) // Retrieve the created allowance by ID result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object() result.Value("id").IsEqual(allowanceId) result.Value("name").IsEqual(TestAllowanceName) result.Value("target").IsEqual(5000) result.Value("weight").IsEqual(10) result.Value("progress").IsEqual(0) } func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) { e := startServer(t) e.GET("/user/1/allowance/9999").Expect().Status(404) } func TestGetUserByAllowanceByIdInvalidUserId(t *testing.T) { e := startServer(t) e.GET("/user/999/allowance/1").Expect().Status(404) } func TestGetUserByAllowanceByIdBadUserId(t *testing.T) { e := startServer(t) e.GET("/user/bad/allowance/1").Expect().Status(400) } func TestGetUserByAllowanceByIdBadAllowanceId(t *testing.T) { e := startServer(t) e.GET("/user/1/allowance/bad").Expect().Status(400) } func TestPutAllowanceById(t *testing.T) { e := startServer(t) // Create a new allowance requestBody := map[string]interface{}{ "name": TestAllowanceName, "target": 5000, "weight": 10, "colour": "#FF5733", } resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() allowanceId := int(resp.Value("id").Number().Raw()) // Update the allowance updateRequest := map[string]interface{}{ "name": "Updated Allowance", "target": 6000, "weight": 15, "colour": "#3357FF", } e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200) // Verify the allowance is updated result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object() result.Value("id").IsEqual(allowanceId) result.Value("name").IsEqual("Updated Allowance") result.Value("target").IsEqual(6000) result.Value("weight").IsEqual(15) result.Value("colour").IsEqual("#3357FF") } func TestCompleteTask(t *testing.T) { e := startServer(t) taskId := createTestTaskWithAmount(e, 101) e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1) // Update rest allowance e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{ Weight: 25, }).Expect().Status(200) // Create two allowance goals e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ Name: "Test Allowance 1", Target: 100, Weight: 50, }).Expect().Status(201) e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ Name: "Test Allowance 1", Target: 10, Weight: 25, }).Expect().Status(201) // Complete the task e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200) // Verify the task is marked as completed e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404) // Verify the allowances are updated for user 1 allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() allowances.Length().IsEqual(3) allowances.Value(0).Object().Value("id").Number().IsEqual(0) allowances.Value(0).Object().Value("progress").Number().InDelta(30.34, 0.01) allowances.Value(1).Object().Value("id").Number().IsEqual(1) allowances.Value(1).Object().Value("progress").Number().InDelta(60.66, 0.01) allowances.Value(2).Object().Value("id").Number().IsEqual(2) allowances.Value(2).Object().Value("progress").Number().IsEqual(10) // And also for user 2 allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array() allowances.Length().IsEqual(1) allowances.Value(0).Object().Value("id").Number().IsEqual(0) allowances.Value(0).Object().Value("progress").Number().IsEqual(101) for userId := 1; userId <= 2; userId++ { userIdStr := strconv.Itoa(userId) // Ensure the history got updated history := e.GET("/user/" + userIdStr + "/history").Expect().Status(200).JSON().Array() history.Length().IsEqual(1) history.Value(0).Object().Value("allowance").Number().IsEqual(101) history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) } } func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) { e := startServer(t) taskId := createTestTaskWithAmount(e, 101) e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1) // Update rest allowance e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{ Weight: 0, }).Expect().Status(200) // Create an allowance goal createTestAllowance(e, "Test Allowance 1", 1000, 0) // Complete the task e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200) // Verify the task is marked as completed e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404) // Verify the allowances are updated for user 1 allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() allowances.Length().IsEqual(2) allowances.Value(0).Object().Value("id").Number().IsEqual(0) allowances.Value(0).Object().Value("progress").Number().IsEqual(101) allowances.Value(1).Object().Value("id").Number().IsEqual(1) allowances.Value(1).Object().Value("progress").Number().IsEqual(0) } func TestCompleteTaskInvalidId(t *testing.T) { e := startServer(t) e.POST("/task/999/complete").Expect().Status(404) } func TestCompleteAllowance(t *testing.T) { e := startServer(t) createTestTaskWithAmount(e, 100) createTestAllowance(e, "Test Allowance 1", 100, 50) // Complete the task e.POST("/task/1/complete").Expect().Status(200) // Complete allowance goal e.POST("/user/1/allowance/1/complete").Expect().Status(200) // Verify the allowance no longer exists e.GET("/user/1/allowance/1").Expect().Status(404) // Verify history is updated history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() history.Length().IsEqual(2) history.Value(0).Object().Value("allowance").Number().IsEqual(100) history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) history.Value(1).Object().Value("allowance").Number().IsEqual(-100) history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) } func TestCompleteAllowanceInvalidUserId(t *testing.T) { e := startServer(t) e.POST("/user/999/allowance/1/complete").Expect().Status(404) } func TestCompleteAllowanceInvalidAllowanceId(t *testing.T) { e := startServer(t) e.POST("/user/1/allowance/999/complete").Expect().Status(404) } func TestPutBulkAllowance(t *testing.T) { e := startServer(t) createTestAllowance(e, "Test Allowance 1", 1000, 1) createTestAllowance(e, "Test Allowance 2", 1000, 2) // Bulk edit request := []map[string]interface{}{ { "id": 1, "weight": 5, }, { "id": 0, "weight": 99, }, { "id": 2, "weight": 10, }, } e.PUT("/user/1/allowance").WithJSON(request).Expect().Status(200) // Verify the allowances are updated allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() allowances.Length().IsEqual(3) allowances.Value(0).Object().Value("id").Number().IsEqual(0) allowances.Value(0).Object().Value("weight").Number().IsEqual(99) allowances.Value(1).Object().Value("id").Number().IsEqual(1) allowances.Value(1).Object().Value("weight").Number().IsEqual(5) allowances.Value(2).Object().Value("id").Number().IsEqual(2) allowances.Value(2).Object().Value("weight").Number().IsEqual(10) } func getDelta(base time.Time, delta float64) (time.Time, time.Time) { start := base.Add(-time.Duration(delta) * time.Second) end := base.Add(time.Duration(delta) * time.Second) return start, end } func createTestAllowance(e *httpexpect.Expect, name string, target float64, weight float64) { e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ Name: name, Target: target, Weight: weight, }).Expect().Status(201) } func createTestTask(e *httpexpect.Expect) int { return createTestTaskWithAmount(e, 100) }