Automated Action 887703b6a2 Add Go Todo App implementation
This commit adds a complete Go implementation of the Todo application. The
application uses Gin framework for the web server, GORM for database access,
and SQLite for storage.

Key features:
- Todo CRUD operations with the same API endpoints
- Health check endpoint
- Database migrations
- Tests for models, services, and API handlers
- Documentation for the API
- Configurable settings
2025-05-17 22:20:05 +00:00

222 lines
5.5 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gin-gonic/gin"
"github.com/simpletodoapp/go-todo-app/internal/dto"
"github.com/simpletodoapp/go-todo-app/internal/model"
"github.com/simpletodoapp/go-todo-app/internal/service"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// setupTestRouter sets up a test router and database
func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
gin.SetMode(gin.TestMode)
router := gin.Default()
// Create an in-memory database
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("Failed to connect to in-memory database: %v", err)
}
// Auto-migrate the schema
err = db.AutoMigrate(&model.Todo{})
if err != nil {
t.Fatalf("Failed to migrate test database: %v", err)
}
// Create services and handlers
todoService := service.NewTodoService(db)
todoHandler := NewTodoHandler(todoService)
// Register routes
api := router.Group("/api")
todoHandler.RegisterRoutes(api)
return router, db
}
// populateTestTodos adds test todos to the database
func populateTestTodos(t *testing.T, db *gorm.DB) {
todos := []model.Todo{
{
Title: "Test Todo 1",
Description: "Test Description 1",
Completed: false,
},
{
Title: "Test Todo 2",
Description: "Test Description 2",
Completed: true,
},
}
for _, todo := range todos {
if err := db.Create(&todo).Error; err != nil {
t.Fatalf("Failed to create test todo: %v", err)
}
}
}
func TestGetTodosHandler(t *testing.T) {
router, db := setupTestRouter(t)
populateTestTodos(t, db)
// Test GET /api/todos
req, _ := http.NewRequest("GET", "/api/todos", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, resp.Code)
}
var todos []model.Todo
if err := json.Unmarshal(resp.Body.Bytes(), &todos); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if len(todos) != 2 {
t.Errorf("Expected 2 todos, got %d", len(todos))
}
}
func TestGetTodoHandler(t *testing.T) {
router, db := setupTestRouter(t)
populateTestTodos(t, db)
// Test GET /api/todos/:id
req, _ := http.NewRequest("GET", "/api/todos/1", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, resp.Code)
}
var todo model.Todo
if err := json.Unmarshal(resp.Body.Bytes(), &todo); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if todo.ID != 1 {
t.Errorf("Expected todo ID to be 1, got %d", todo.ID)
}
// Test non-existent todo
req, _ = http.NewRequest("GET", "/api/todos/999", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Errorf("Expected status code %d, got %d", http.StatusNotFound, resp.Code)
}
}
func TestCreateTodoHandler(t *testing.T) {
router, _ := setupTestRouter(t)
// Create request body
todoCreate := dto.TodoCreate{
TodoBase: dto.TodoBase{
Title: "New Todo",
Description: "New Description",
Completed: false,
},
}
body, _ := json.Marshal(todoCreate)
// Test POST /api/todos
req, _ := http.NewRequest("POST", "/api/todos", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusCreated {
t.Errorf("Expected status code %d, got %d", http.StatusCreated, resp.Code)
}
var todo model.Todo
if err := json.Unmarshal(resp.Body.Bytes(), &todo); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if todo.Title != "New Todo" {
t.Errorf("Expected title to be 'New Todo', got '%s'", todo.Title)
}
}
func TestUpdateTodoHandler(t *testing.T) {
router, db := setupTestRouter(t)
populateTestTodos(t, db)
// Create request body
title := "Updated Title"
completed := true
todoUpdate := dto.TodoUpdate{
Title: &title,
Completed: &completed,
}
body, _ := json.Marshal(todoUpdate)
// Test PUT /api/todos/:id
req, _ := http.NewRequest("PUT", "/api/todos/1", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, resp.Code)
}
var todo model.Todo
if err := json.Unmarshal(resp.Body.Bytes(), &todo); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if todo.Title != "Updated Title" {
t.Errorf("Expected title to be 'Updated Title', got '%s'", todo.Title)
}
if !todo.Completed {
t.Errorf("Expected completed to be true, got %t", todo.Completed)
}
}
func TestDeleteTodoHandler(t *testing.T) {
router, db := setupTestRouter(t)
populateTestTodos(t, db)
// Test DELETE /api/todos/:id
req, _ := http.NewRequest("DELETE", "/api/todos/1", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusNoContent {
t.Errorf("Expected status code %d, got %d", http.StatusNoContent, resp.Code)
}
// Verify the todo was deleted
var count int64
db.Model(&model.Todo{}).Where("id = ?", 1).Count(&count)
if count != 0 {
t.Errorf("Expected todo to be deleted, but it still exists")
}
// Test deleting non-existent todo
req, _ = http.NewRequest("DELETE", "/api/todos/999", nil)
resp = httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Errorf("Expected status code %d, got %d", http.StatusNotFound, resp.Code)
}
}