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) } }