diff --git a/go-todo-app/.gitignore b/go-todo-app/.gitignore
new file mode 100644
index 0000000..b9df8d7
--- /dev/null
+++ b/go-todo-app/.gitignore
@@ -0,0 +1,32 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+# IDE specific files
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# SQLite database
+*.db
+*.sqlite
+*.sqlite3
+
+# Binary
+/go-todo-app
\ No newline at end of file
diff --git a/go-todo-app/Makefile b/go-todo-app/Makefile
new file mode 100644
index 0000000..21b1ae0
--- /dev/null
+++ b/go-todo-app/Makefile
@@ -0,0 +1,36 @@
+.PHONY: build run clean test
+
+# Build variables
+BINARY_NAME=go-todo-app
+MAIN_PATH=./cmd/api
+
+# Default target
+all: build
+
+# Build the application
+build:
+ go build -o $(BINARY_NAME) $(MAIN_PATH)
+
+# Run the application in development mode
+run:
+ go run $(MAIN_PATH)/main.go
+
+# Clean build artifacts
+clean:
+ go clean
+ rm -f $(BINARY_NAME)
+
+# Run tests
+test:
+ go test -v ./...
+
+# Format code
+fmt:
+ go fmt ./...
+
+# Check for issues
+vet:
+ go vet ./...
+
+# All-in-one check
+check: fmt vet test
\ No newline at end of file
diff --git a/go-todo-app/README.md b/go-todo-app/README.md
new file mode 100644
index 0000000..465f5fc
--- /dev/null
+++ b/go-todo-app/README.md
@@ -0,0 +1,162 @@
+# Go Todo App
+
+A simple Todo application API built with Go, Gin, and SQLite. This is a Go rewrite of the original FastAPI Python application.
+
+## Features
+
+- RESTful API for managing todo items
+- SQLite database for persistent storage
+- CORS support for cross-origin requests
+- Health check endpoint
+- Pagination and filtering support
+
+## Technology Stack
+
+- [Go](https://golang.org/) - Programming language
+- [Gin](https://github.com/gin-gonic/gin) - Web framework
+- [GORM](https://gorm.io/) - ORM library
+- [SQLite](https://www.sqlite.org/) - Database
+- [Viper](https://github.com/spf13/viper) - Configuration management
+
+## API Endpoints
+
+| Method | URL | Description |
+|--------|--------------------|-----------------------------|
+| GET | /health | Health check |
+| GET | /api/todos | List all todos |
+| POST | /api/todos | Create a new todo |
+| GET | /api/todos/:id | Get a specific todo |
+| PUT | /api/todos/:id | Update a todo |
+| DELETE | /api/todos/:id | Delete a todo |
+
+## Query Parameters
+
+The `GET /api/todos` endpoint supports the following query parameters:
+
+- `skip` - Number of items to skip (default: 0)
+- `limit` - Maximum number of items to return (default: 100)
+- `completed` - Filter by completion status (boolean, optional)
+
+## Prerequisites
+
+- Go 1.18 or higher
+- Git
+
+## Getting Started
+
+### Clone the repository
+
+```bash
+git clone https://github.com/yourusername/go-todo-app.git
+cd go-todo-app
+```
+
+### Build and run the application
+
+```bash
+# Build the application
+go build -o go-todo-app ./cmd/api
+
+# Run the application
+./go-todo-app
+```
+
+The API will be available at http://localhost:8000
+
+### Development mode
+
+```bash
+go run cmd/api/main.go
+```
+
+## Configuration
+
+The application can be configured using environment variables or a config file. The following settings are available:
+
+| Environment Variable | Description | Default Value |
+|----------------------|-------------------------------|---------------------------------------------|
+| APP_NAME | Application name | Go Todo App |
+| APP_DESCRIPTION | Application description | A simple Todo application API... |
+| APP_VERSION | Application version | 0.1.0 |
+| SERVER_PORT | HTTP server port | 8000 |
+| DB_PATH | Database directory path | /app/storage/db |
+| DB_NAME | Database file name | db.sqlite |
+
+## Project Structure
+
+```
+.
+├── cmd/
+│ └── api/
+│ └── main.go # Application entry point
+├── internal/
+│ ├── config/ # Application configuration
+│ │ └── config.go
+│ ├── api/ # API routes and handlers
+│ │ ├── api.go # API initialization
+│ │ ├── middleware.go # Middleware functions
+│ │ ├── todo.go # Todo route handlers
+│ │ └── health.go # Health check route handler
+│ ├── database/ # Database connection and initialization
+│ │ └── db.go
+│ ├── model/ # Database models (GORM)
+│ │ └── todo.go
+│ ├── dto/ # Data transfer objects
+│ │ └── todo.go
+│ └── service/ # Business logic
+│ └── todo.go
+├── migrations/ # Database migration files
+├── storage/ # Storage directory
+│ └── db/ # Database files location
+├── go.mod # Go module file
+├── go.sum # Go dependencies checksums
+└── README.md # Project documentation
+```
+
+## Example Usage
+
+### Create a todo
+
+```bash
+curl -X POST http://localhost:8000/api/todos \
+ -H "Content-Type: application/json" \
+ -d '{"title": "Learn Go", "description": "Learn Go programming language", "completed": false}'
+```
+
+### Get all todos
+
+```bash
+curl http://localhost:8000/api/todos
+```
+
+### Get a specific todo
+
+```bash
+curl http://localhost:8000/api/todos/1
+```
+
+### Update a todo
+
+```bash
+curl -X PUT http://localhost:8000/api/todos/1 \
+ -H "Content-Type: application/json" \
+ -d '{"completed": true}'
+```
+
+### Delete a todo
+
+```bash
+curl -X DELETE http://localhost:8000/api/todos/1
+```
+
+### Get completed todos
+
+```bash
+curl http://localhost:8000/api/todos?completed=true
+```
+
+### Health check
+
+```bash
+curl http://localhost:8000/health
+```
\ No newline at end of file
diff --git a/go-todo-app/cmd/api/main.go b/go-todo-app/cmd/api/main.go
new file mode 100644
index 0000000..cfbff86
--- /dev/null
+++ b/go-todo-app/cmd/api/main.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "path/filepath"
+
+ "github.com/simpletodoapp/go-todo-app/internal/api"
+ "github.com/simpletodoapp/go-todo-app/internal/config"
+ "github.com/simpletodoapp/go-todo-app/internal/database"
+)
+
+func main() {
+ // Load configuration
+ cfg, err := config.LoadConfig()
+ if err != nil {
+ log.Fatalf("Failed to load config: %v", err)
+ }
+
+ // Connect to database
+ db, err := database.NewDatabase(cfg)
+ if err != nil {
+ log.Fatalf("Failed to initialize database: %v", err)
+ }
+
+ // Run migrations
+ migrationRunner := database.NewMigrationRunner(db, cfg)
+ migrationsPath := filepath.Join(".", "migrations")
+ if err := migrationRunner.RunMigrations(migrationsPath); err != nil {
+ log.Fatalf("Failed to run migrations: %v", err)
+ }
+
+ // Setup router
+ router := api.SetupRouter(cfg, db)
+
+ // Start server
+ serverAddr := fmt.Sprintf(":%s", cfg.ServerPort)
+ log.Printf("Starting server on %s", serverAddr)
+ if err := router.Run(serverAddr); err != nil {
+ log.Fatalf("Failed to start server: %v", err)
+ }
+}
\ No newline at end of file
diff --git a/go-todo-app/go.mod b/go-todo-app/go.mod
new file mode 100644
index 0000000..51bebdc
--- /dev/null
+++ b/go-todo-app/go.mod
@@ -0,0 +1,52 @@
+module github.com/simpletodoapp/go-todo-app
+
+go 1.20
+
+require (
+ github.com/gin-contrib/cors v1.4.0
+ github.com/gin-gonic/gin v1.9.1
+ github.com/spf13/viper v1.16.0
+ gorm.io/driver/sqlite v1.5.3
+ gorm.io/gorm v1.25.4
+)
+
+require (
+ github.com/bytedance/sonic v1.10.0 // indirect
+ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
+ github.com/chenzhuoyu/iasm v0.9.0 // indirect
+ github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.15.3 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+ github.com/leodido/go-urn v1.2.4 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/mattn/go-sqlite3 v1.14.17 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+ github.com/spf13/afero v1.9.5 // indirect
+ github.com/spf13/cast v1.5.1 // indirect
+ github.com/spf13/jwalterweatherman v1.1.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.11 // indirect
+ golang.org/x/arch v0.5.0 // indirect
+ golang.org/x/crypto v0.13.0 // indirect
+ golang.org/x/net v0.15.0 // indirect
+ golang.org/x/sys v0.12.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
\ No newline at end of file
diff --git a/go-todo-app/internal/api/api.go b/go-todo-app/internal/api/api.go
new file mode 100644
index 0000000..f40de67
--- /dev/null
+++ b/go-todo-app/internal/api/api.go
@@ -0,0 +1,32 @@
+package api
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/simpletodoapp/go-todo-app/internal/config"
+ "github.com/simpletodoapp/go-todo-app/internal/database"
+ "github.com/simpletodoapp/go-todo-app/internal/service"
+)
+
+// SetupRouter initializes the API router and routes
+func SetupRouter(cfg *config.Config, db *database.Database) *gin.Engine {
+ router := gin.Default()
+
+ // Setup middleware
+ SetupMiddleware(router)
+
+ // Setup API documentation
+ SetupDocs(router, cfg)
+
+ // Create services
+ todoService := service.NewTodoService(db.DB)
+
+ // Register handlers
+ healthHandler := NewHealthHandler(db)
+ healthHandler.RegisterRoutes(router)
+
+ todoHandler := NewTodoHandler(todoService)
+ api := router.Group("/api")
+ todoHandler.RegisterRoutes(api)
+
+ return router
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/api/docs.go b/go-todo-app/internal/api/docs.go
new file mode 100644
index 0000000..c829232
--- /dev/null
+++ b/go-todo-app/internal/api/docs.go
@@ -0,0 +1,154 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/simpletodoapp/go-todo-app/internal/config"
+)
+
+// SetupDocs sets up the API documentation
+func SetupDocs(router *gin.Engine, cfg *config.Config) {
+ // Simple documentation page
+ router.GET("/docs", func(c *gin.Context) {
+ html := fmt.Sprintf(`
+
+
+
+ %s API Documentation
+
+
+
+ %s API Documentation
+ %s
+ Version: %s
+
+ Endpoints
+
+
+
Health Check
+
GET /health
+
Check the health of the application and database.
+
Response
+
{
+ "status": "healthy",
+ "db_status": "healthy"
+}
+
+
+
+
List Todos
+
GET /api/todos
+
Retrieve a list of todos with optional filtering and pagination.
+
Query Parameters
+
+ skip
- Number of items to skip (default: 0)
+ limit
- Maximum number of items to return (default: 100)
+ completed
- Filter by completion status (boolean, optional)
+
+
Response
+
[
+ {
+ "id": 1,
+ "title": "Learn Go",
+ "description": "Learn Go programming language",
+ "completed": false,
+ "created_at": "2023-09-10T12:00:00Z",
+ "updated_at": "2023-09-10T12:00:00Z"
+ }
+]
+
+
+
+
Get Todo
+
GET /api/todos/:id
+
Retrieve a specific todo by ID.
+
Path Parameters
+
+
Response
+
{
+ "id": 1,
+ "title": "Learn Go",
+ "description": "Learn Go programming language",
+ "completed": false,
+ "created_at": "2023-09-10T12:00:00Z",
+ "updated_at": "2023-09-10T12:00:00Z"
+}
+
+
+
+
Create Todo
+
POST /api/todos
+
Create a new todo item.
+
Request Body
+
{
+ "title": "Learn Go",
+ "description": "Learn Go programming language",
+ "completed": false
+}
+
Response
+
{
+ "id": 1,
+ "title": "Learn Go",
+ "description": "Learn Go programming language",
+ "completed": false,
+ "created_at": "2023-09-10T12:00:00Z",
+ "updated_at": "2023-09-10T12:00:00Z"
+}
+
+
+
+
Update Todo
+
PUT /api/todos/:id
+
Update an existing todo.
+
Path Parameters
+
+
Request Body
+
{
+ "title": "Updated title", // Optional
+ "description": "Updated description", // Optional
+ "completed": true // Optional
+}
+
Response
+
{
+ "id": 1,
+ "title": "Updated title",
+ "description": "Updated description",
+ "completed": true,
+ "created_at": "2023-09-10T12:00:00Z",
+ "updated_at": "2023-09-10T12:05:00Z"
+}
+
+
+
+
Delete Todo
+
DELETE /api/todos/:id
+
Delete a todo.
+
Path Parameters
+
+
Response
+
No content (HTTP 204)
+
+
+
+ `, cfg.AppName, cfg.AppName, cfg.AppDescription, cfg.AppVersion)
+
+ c.Header("Content-Type", "text/html")
+ c.String(http.StatusOK, html)
+ })
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/api/health.go b/go-todo-app/internal/api/health.go
new file mode 100644
index 0000000..103dc7b
--- /dev/null
+++ b/go-todo-app/internal/api/health.go
@@ -0,0 +1,41 @@
+package api
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/simpletodoapp/go-todo-app/internal/database"
+ "github.com/simpletodoapp/go-todo-app/internal/dto"
+)
+
+// HealthHandler handles health check requests
+type HealthHandler struct {
+ db *database.Database
+}
+
+// NewHealthHandler creates a new health check handler
+func NewHealthHandler(db *database.Database) *HealthHandler {
+ return &HealthHandler{db: db}
+}
+
+// RegisterRoutes registers the health check routes
+func (h *HealthHandler) RegisterRoutes(router *gin.Engine) {
+ router.GET("/health", h.HealthCheck)
+}
+
+// HealthCheck handles GET /health
+// Returns the health status of the application
+func (h *HealthHandler) HealthCheck(c *gin.Context) {
+ // Check database health
+ dbStatus := "healthy"
+ if err := h.db.Health(); err != nil {
+ dbStatus = "unhealthy"
+ }
+
+ response := dto.HealthResponse{
+ Status: "healthy",
+ DBStatus: dbStatus,
+ }
+
+ c.JSON(http.StatusOK, response)
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/api/middleware.go b/go-todo-app/internal/api/middleware.go
new file mode 100644
index 0000000..49a637c
--- /dev/null
+++ b/go-todo-app/internal/api/middleware.go
@@ -0,0 +1,27 @@
+package api
+
+import (
+ "time"
+
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+)
+
+// SetupMiddleware configures middleware for the router
+func SetupMiddleware(router *gin.Engine) {
+ // CORS middleware
+ router.Use(cors.New(cors.Config{
+ AllowOrigins: []string{"*"},
+ AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
+ AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
+ ExposeHeaders: []string{"Content-Length"},
+ AllowCredentials: true,
+ MaxAge: 12 * time.Hour,
+ }))
+
+ // Request logging middleware
+ router.Use(gin.Logger())
+
+ // Recovery middleware
+ router.Use(gin.Recovery())
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/api/todo.go b/go-todo-app/internal/api/todo.go
new file mode 100644
index 0000000..b45b98d
--- /dev/null
+++ b/go-todo-app/internal/api/todo.go
@@ -0,0 +1,150 @@
+package api
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/simpletodoapp/go-todo-app/internal/dto"
+ "github.com/simpletodoapp/go-todo-app/internal/service"
+)
+
+// TodoHandler handles todo-related API requests
+type TodoHandler struct {
+ todoService *service.TodoService
+}
+
+// NewTodoHandler creates a new todo handler
+func NewTodoHandler(todoService *service.TodoService) *TodoHandler {
+ return &TodoHandler{todoService: todoService}
+}
+
+// RegisterRoutes registers the todo routes
+func (h *TodoHandler) RegisterRoutes(router *gin.RouterGroup) {
+ todos := router.Group("/todos")
+ {
+ todos.GET("", h.GetTodos)
+ todos.POST("", h.CreateTodo)
+ todos.GET("/:id", h.GetTodo)
+ todos.PUT("/:id", h.UpdateTodo)
+ todos.DELETE("/:id", h.DeleteTodo)
+ }
+}
+
+// GetTodos handles GET /api/todos
+// Returns a list of todos with optional filtering and pagination
+func (h *TodoHandler) GetTodos(c *gin.Context) {
+ // Parse query parameters
+ skip, _ := strconv.Atoi(c.DefaultQuery("skip", "0"))
+ limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100"))
+
+ var completed *bool
+ if completedStr, exists := c.GetQuery("completed"); exists {
+ completedBool, err := strconv.ParseBool(completedStr)
+ if err == nil {
+ completed = &completedBool
+ }
+ }
+
+ // Get todos from service
+ todos, err := h.todoService.GetTodos(skip, limit, completed)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, todos)
+}
+
+// GetTodo handles GET /api/todos/:id
+// Returns a specific todo by ID
+func (h *TodoHandler) GetTodo(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
+ return
+ }
+
+ todo, err := h.todoService.GetTodoByID(uint(id))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ if todo == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
+ return
+ }
+
+ c.JSON(http.StatusOK, todo)
+}
+
+// CreateTodo handles POST /api/todos
+// Creates a new todo
+func (h *TodoHandler) CreateTodo(c *gin.Context) {
+ var todoCreate dto.TodoCreate
+ if err := c.ShouldBindJSON(&todoCreate); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ todo, err := h.todoService.CreateTodo(todoCreate)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusCreated, todo)
+}
+
+// UpdateTodo handles PUT /api/todos/:id
+// Updates an existing todo
+func (h *TodoHandler) UpdateTodo(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
+ return
+ }
+
+ var todoUpdate dto.TodoUpdate
+ if err := c.ShouldBindJSON(&todoUpdate); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ todo, err := h.todoService.UpdateTodo(uint(id), todoUpdate)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ if todo == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
+ return
+ }
+
+ c.JSON(http.StatusOK, todo)
+}
+
+// DeleteTodo handles DELETE /api/todos/:id
+// Deletes a todo
+func (h *TodoHandler) DeleteTodo(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID format"})
+ return
+ }
+
+ deleted, err := h.todoService.DeleteTodo(uint(id))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ if !deleted {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
+ return
+ }
+
+ c.Status(http.StatusNoContent)
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/api/todo_test.go b/go-todo-app/internal/api/todo_test.go
new file mode 100644
index 0000000..1d1c1dd
--- /dev/null
+++ b/go-todo-app/internal/api/todo_test.go
@@ -0,0 +1,222 @@
+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)
+ }
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/config/config.go b/go-todo-app/internal/config/config.go
new file mode 100644
index 0000000..c49fff3
--- /dev/null
+++ b/go-todo-app/internal/config/config.go
@@ -0,0 +1,69 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/spf13/viper"
+)
+
+// Config holds the application configuration
+type Config struct {
+ AppName string `mapstructure:"APP_NAME"`
+ AppDescription string `mapstructure:"APP_DESCRIPTION"`
+ AppVersion string `mapstructure:"APP_VERSION"`
+ ServerPort string `mapstructure:"SERVER_PORT"`
+ DBPath string `mapstructure:"DB_PATH"`
+ DBName string `mapstructure:"DB_NAME"`
+}
+
+// LoadConfig reads the configuration from environment variables or config file
+func LoadConfig() (*Config, error) {
+ // Set default values
+ config := &Config{
+ AppName: "Go Todo App",
+ AppDescription: "A simple Todo application API built with Go, Gin and SQLite",
+ AppVersion: "0.1.0",
+ ServerPort: "8000",
+ DBPath: "/app/storage/db",
+ DBName: "db.sqlite",
+ }
+
+ // Set up viper
+ v := viper.New()
+ v.SetConfigName("config")
+ v.SetConfigType("yaml")
+ v.AddConfigPath(".")
+ v.AddConfigPath("./config")
+
+ // Match environment variables with config fields
+ v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+ v.AutomaticEnv()
+
+ // Try to read from config file (optional)
+ if err := v.ReadInConfig(); err != nil {
+ if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
+ return nil, fmt.Errorf("error reading config file: %v", err)
+ }
+ }
+
+ // Override config from environment variables
+ if err := v.Unmarshal(config); err != nil {
+ return nil, fmt.Errorf("error unmarshaling config: %v", err)
+ }
+
+ // Ensure DB directory exists
+ dbDir := config.DBPath
+ if err := os.MkdirAll(dbDir, 0755); err != nil {
+ return nil, fmt.Errorf("error creating DB directory: %v", err)
+ }
+
+ return config, nil
+}
+
+// GetDatabaseURL returns the full SQLite database connection string
+func (c *Config) GetDatabaseURL() string {
+ return fmt.Sprintf("file:%s", filepath.Join(c.DBPath, c.DBName))
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/database/db.go b/go-todo-app/internal/database/db.go
new file mode 100644
index 0000000..28fb9fc
--- /dev/null
+++ b/go-todo-app/internal/database/db.go
@@ -0,0 +1,48 @@
+package database
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/simpletodoapp/go-todo-app/internal/config"
+ "github.com/simpletodoapp/go-todo-app/internal/model"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+// Database holds the database connection
+type Database struct {
+ DB *gorm.DB
+}
+
+// NewDatabase initializes a new database connection
+func NewDatabase(cfg *config.Config) (*Database, error) {
+ // Configure GORM
+ gormConfig := &gorm.Config{
+ Logger: logger.Default.LogMode(logger.Info),
+ }
+
+ // Connect to SQLite database
+ log.Printf("Connecting to database: %s", cfg.GetDatabaseURL())
+ db, err := gorm.Open(sqlite.Open(cfg.GetDatabaseURL()), gormConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to connect to database: %v", err)
+ }
+
+ // Auto-migrate the schema
+ if err := db.AutoMigrate(&model.Todo{}); err != nil {
+ return nil, fmt.Errorf("failed to migrate database: %v", err)
+ }
+
+ return &Database{DB: db}, nil
+}
+
+// Health checks the database connection
+func (d *Database) Health() error {
+ sqlDB, err := d.DB.DB()
+ if err != nil {
+ return err
+ }
+ return sqlDB.Ping()
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/database/migrations.go b/go-todo-app/internal/database/migrations.go
new file mode 100644
index 0000000..91c82b5
--- /dev/null
+++ b/go-todo-app/internal/database/migrations.go
@@ -0,0 +1,152 @@
+package database
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/simpletodoapp/go-todo-app/internal/config"
+)
+
+// MigrationRunner handles database migrations
+type MigrationRunner struct {
+ db *Database
+ config *config.Config
+}
+
+// NewMigrationRunner creates a new migration runner
+func NewMigrationRunner(db *Database, cfg *config.Config) *MigrationRunner {
+ return &MigrationRunner{
+ db: db,
+ config: cfg,
+ }
+}
+
+// RunMigrations executes all pending migrations
+func (m *MigrationRunner) RunMigrations(migrationsPath string) error {
+ log.Println("Running migrations...")
+
+ // Get all migration files
+ files, err := getMigrationFiles(migrationsPath)
+ if err != nil {
+ return err
+ }
+
+ if len(files) == 0 {
+ log.Println("No migration files found")
+ return nil
+ }
+
+ // Create migrations table if it doesn't exist
+ err = m.createMigrationsTable()
+ if err != nil {
+ return err
+ }
+
+ // Get applied migrations
+ appliedMigrations, err := m.getAppliedMigrations()
+ if err != nil {
+ return err
+ }
+
+ // Apply pending migrations
+ for _, file := range files {
+ migrationName := filepath.Base(file)
+ if !contains(appliedMigrations, migrationName) {
+ log.Printf("Applying migration: %s", migrationName)
+
+ // Read migration file
+ content, err := ioutil.ReadFile(file)
+ if err != nil {
+ return fmt.Errorf("failed to read migration file: %v", err)
+ }
+
+ // Split into up and down migrations
+ parts := strings.Split(string(content), "-- Down migration")
+ upMigration := parts[0]
+
+ // Execute migration
+ tx := m.db.DB.Begin()
+ if err := tx.Exec(upMigration).Error; err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to apply migration: %v", err)
+ }
+
+ // Record applied migration
+ if err := tx.Exec("INSERT INTO migrations (name, applied_at) VALUES (?, CURRENT_TIMESTAMP)", migrationName).Error; err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to record migration: %v", err)
+ }
+
+ tx.Commit()
+ log.Printf("Migration applied: %s", migrationName)
+ } else {
+ log.Printf("Migration already applied: %s", migrationName)
+ }
+ }
+
+ log.Println("Migrations completed successfully")
+ return nil
+}
+
+// createMigrationsTable creates the migrations table if it doesn't exist
+func (m *MigrationRunner) createMigrationsTable() error {
+ query := `
+ CREATE TABLE IF NOT EXISTS migrations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ applied_at DATETIME NOT NULL
+ )
+ `
+
+ return m.db.DB.Exec(query).Error
+}
+
+// getAppliedMigrations gets the list of already applied migrations
+func (m *MigrationRunner) getAppliedMigrations() ([]string, error) {
+ var migrations []string
+ var records []struct {
+ Name string
+ }
+
+ result := m.db.DB.Raw("SELECT name FROM migrations ORDER BY id ASC").Scan(&records)
+ if result.Error != nil {
+ return nil, fmt.Errorf("failed to get applied migrations: %v", result.Error)
+ }
+
+ for _, record := range records {
+ migrations = append(migrations, record.Name)
+ }
+
+ return migrations, nil
+}
+
+// getMigrationFiles gets all migration files sorted by filename
+func getMigrationFiles(migrationsPath string) ([]string, error) {
+ files, err := filepath.Glob(filepath.Join(migrationsPath, "*.sql"))
+ if err != nil {
+ return nil, fmt.Errorf("failed to read migration files: %v", err)
+ }
+
+ // Check if directory exists
+ if _, err := os.Stat(migrationsPath); os.IsNotExist(err) {
+ return []string{}, nil
+ }
+
+ sort.Strings(files)
+ return files, nil
+}
+
+// contains checks if a string is in a slice
+func contains(slice []string, item string) bool {
+ for _, s := range slice {
+ if s == item {
+ return true
+ }
+ }
+ return false
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/dto/todo.go b/go-todo-app/internal/dto/todo.go
new file mode 100644
index 0000000..63d322b
--- /dev/null
+++ b/go-todo-app/internal/dto/todo.go
@@ -0,0 +1,40 @@
+package dto
+
+import (
+ "time"
+)
+
+// TodoBase contains the common fields for todo operations
+type TodoBase struct {
+ Title string `json:"title" binding:"required"`
+ Description string `json:"description"`
+ Completed bool `json:"completed" default:"false"`
+}
+
+// TodoCreate represents the data needed to create a new todo
+type TodoCreate struct {
+ TodoBase
+}
+
+// TodoUpdate represents the data that can be updated for a todo
+type TodoUpdate struct {
+ Title *string `json:"title"`
+ Description *string `json:"description"`
+ Completed *bool `json:"completed"`
+}
+
+// TodoResponse represents the todo data sent back to the client
+type TodoResponse struct {
+ ID uint `json:"id"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Completed bool `json:"completed"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// HealthResponse represents the health check response
+type HealthResponse struct {
+ Status string `json:"status"`
+ DBStatus string `json:"db_status"`
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/model/todo.go b/go-todo-app/internal/model/todo.go
new file mode 100644
index 0000000..0e60844
--- /dev/null
+++ b/go-todo-app/internal/model/todo.go
@@ -0,0 +1,20 @@
+package model
+
+import (
+ "time"
+)
+
+// Todo represents a todo item in the database
+type Todo struct {
+ ID uint `gorm:"primaryKey" json:"id"`
+ Title string `gorm:"index" json:"title"`
+ Description string `gorm:"default:null" json:"description"`
+ Completed bool `gorm:"default:false" json:"completed"`
+ CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
+ UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
+}
+
+// TableName overrides the table name
+func (Todo) TableName() string {
+ return "todos"
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/model/todo_test.go b/go-todo-app/internal/model/todo_test.go
new file mode 100644
index 0000000..e8d8f38
--- /dev/null
+++ b/go-todo-app/internal/model/todo_test.go
@@ -0,0 +1,51 @@
+package model
+
+import (
+ "testing"
+ "time"
+)
+
+func TestTodoTableName(t *testing.T) {
+ todo := Todo{}
+ if todo.TableName() != "todos" {
+ t.Errorf("Expected table name to be 'todos', got '%s'", todo.TableName())
+ }
+}
+
+func TestTodoFields(t *testing.T) {
+ // Create a todo
+ now := time.Now()
+ todo := Todo{
+ ID: 1,
+ Title: "Test Todo",
+ Description: "Test Description",
+ Completed: false,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ // Check field values
+ if todo.ID != 1 {
+ t.Errorf("Expected ID to be 1, got %d", todo.ID)
+ }
+
+ if todo.Title != "Test Todo" {
+ t.Errorf("Expected Title to be 'Test Todo', got '%s'", todo.Title)
+ }
+
+ if todo.Description != "Test Description" {
+ t.Errorf("Expected Description to be 'Test Description', got '%s'", todo.Description)
+ }
+
+ if todo.Completed != false {
+ t.Errorf("Expected Completed to be false, got %t", todo.Completed)
+ }
+
+ if !todo.CreatedAt.Equal(now) {
+ t.Errorf("Expected CreatedAt to be %v, got %v", now, todo.CreatedAt)
+ }
+
+ if !todo.UpdatedAt.Equal(now) {
+ t.Errorf("Expected UpdatedAt to be %v, got %v", now, todo.UpdatedAt)
+ }
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/service/todo.go b/go-todo-app/internal/service/todo.go
new file mode 100644
index 0000000..c0c33ce
--- /dev/null
+++ b/go-todo-app/internal/service/todo.go
@@ -0,0 +1,126 @@
+package service
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/simpletodoapp/go-todo-app/internal/dto"
+ "github.com/simpletodoapp/go-todo-app/internal/model"
+ "gorm.io/gorm"
+)
+
+// TodoService handles todo operations
+type TodoService struct {
+ db *gorm.DB
+}
+
+// NewTodoService creates a new todo service
+func NewTodoService(db *gorm.DB) *TodoService {
+ return &TodoService{db: db}
+}
+
+// GetTodos retrieves todos with optional filters and pagination
+func (s *TodoService) GetTodos(skip, limit int, completed *bool) ([]model.Todo, error) {
+ var todos []model.Todo
+ query := s.db
+
+ // Apply completed filter if provided
+ if completed != nil {
+ query = query.Where("completed = ?", *completed)
+ }
+
+ // Apply pagination
+ result := query.Offset(skip).Limit(limit).Find(&todos)
+ if result.Error != nil {
+ return nil, fmt.Errorf("error getting todos: %v", result.Error)
+ }
+
+ return todos, nil
+}
+
+// GetTodoByID retrieves a specific todo by ID
+func (s *TodoService) GetTodoByID(id uint) (*model.Todo, error) {
+ var todo model.Todo
+ result := s.db.First(&todo, id)
+
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return nil, nil // Todo not found, but not an error
+ }
+ return nil, fmt.Errorf("error getting todo: %v", result.Error)
+ }
+
+ return &todo, nil
+}
+
+// CreateTodo creates a new todo
+func (s *TodoService) CreateTodo(todoCreate dto.TodoCreate) (*model.Todo, error) {
+ todo := model.Todo{
+ Title: todoCreate.Title,
+ Description: todoCreate.Description,
+ Completed: todoCreate.Completed,
+ }
+
+ result := s.db.Create(&todo)
+ if result.Error != nil {
+ return nil, fmt.Errorf("error creating todo: %v", result.Error)
+ }
+
+ return &todo, nil
+}
+
+// UpdateTodo updates an existing todo
+func (s *TodoService) UpdateTodo(id uint, todoUpdate dto.TodoUpdate) (*model.Todo, error) {
+ // Get existing todo
+ todo, err := s.GetTodoByID(id)
+ if err != nil {
+ return nil, err
+ }
+ if todo == nil {
+ return nil, nil // Not found
+ }
+
+ // Update fields if provided
+ updates := map[string]interface{}{}
+
+ if todoUpdate.Title != nil {
+ updates["title"] = *todoUpdate.Title
+ }
+
+ if todoUpdate.Description != nil {
+ updates["description"] = *todoUpdate.Description
+ }
+
+ if todoUpdate.Completed != nil {
+ updates["completed"] = *todoUpdate.Completed
+ }
+
+ // Apply updates
+ result := s.db.Model(&todo).Updates(updates)
+ if result.Error != nil {
+ return nil, fmt.Errorf("error updating todo: %v", result.Error)
+ }
+
+ // Refresh the todo from database
+ return s.GetTodoByID(id)
+}
+
+// DeleteTodo deletes a todo by ID
+func (s *TodoService) DeleteTodo(id uint) (bool, error) {
+ // Check if todo exists
+ todo, err := s.GetTodoByID(id)
+ if err != nil {
+ return false, err
+ }
+ if todo == nil {
+ return false, nil // Not found
+ }
+
+ // Delete the todo
+ result := s.db.Delete(&model.Todo{}, id)
+ if result.Error != nil {
+ return false, fmt.Errorf("error deleting todo: %v", result.Error)
+ }
+
+ return true, nil
+}
\ No newline at end of file
diff --git a/go-todo-app/internal/service/todo_test.go b/go-todo-app/internal/service/todo_test.go
new file mode 100644
index 0000000..5cf6714
--- /dev/null
+++ b/go-todo-app/internal/service/todo_test.go
@@ -0,0 +1,228 @@
+package service
+
+import (
+ "testing"
+
+ "github.com/simpletodoapp/go-todo-app/internal/dto"
+ "github.com/simpletodoapp/go-todo-app/internal/model"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+)
+
+// setupTestDB sets up an in-memory SQLite database for testing
+func setupTestDB(t *testing.T) *gorm.DB {
+ 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)
+ }
+
+ return db
+}
+
+// populateTestData adds test data to the database
+func populateTestData(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 data: %v", err)
+ }
+ }
+}
+
+func TestGetTodos(t *testing.T) {
+ db := setupTestDB(t)
+ populateTestData(t, db)
+
+ service := NewTodoService(db)
+
+ // Test getting all todos
+ todos, err := service.GetTodos(0, 100, nil)
+ if err != nil {
+ t.Fatalf("Failed to get todos: %v", err)
+ }
+
+ if len(todos) != 2 {
+ t.Errorf("Expected 2 todos, got %d", len(todos))
+ }
+
+ // Test filtering by completed
+ completed := true
+ todos, err = service.GetTodos(0, 100, &completed)
+ if err != nil {
+ t.Fatalf("Failed to get completed todos: %v", err)
+ }
+
+ if len(todos) != 1 {
+ t.Errorf("Expected 1 completed todo, got %d", len(todos))
+ }
+
+ if !todos[0].Completed {
+ t.Errorf("Expected todo to be completed, got %t", todos[0].Completed)
+ }
+
+ // Test pagination
+ todos, err = service.GetTodos(1, 1, nil)
+ if err != nil {
+ t.Fatalf("Failed to get paginated todos: %v", err)
+ }
+
+ if len(todos) != 1 {
+ t.Errorf("Expected 1 todo with pagination, got %d", len(todos))
+ }
+}
+
+func TestCreateTodo(t *testing.T) {
+ db := setupTestDB(t)
+ service := NewTodoService(db)
+
+ // Create a new todo
+ todoCreate := dto.TodoCreate{
+ TodoBase: dto.TodoBase{
+ Title: "New Todo",
+ Description: "New Description",
+ Completed: false,
+ },
+ }
+
+ todo, err := service.CreateTodo(todoCreate)
+ if err != nil {
+ t.Fatalf("Failed to create todo: %v", err)
+ }
+
+ if todo.ID == 0 {
+ t.Errorf("Expected todo ID to be set, got %d", todo.ID)
+ }
+
+ if todo.Title != "New Todo" {
+ t.Errorf("Expected title to be 'New Todo', got '%s'", todo.Title)
+ }
+
+ if todo.Description != "New Description" {
+ t.Errorf("Expected description to be 'New Description', got '%s'", todo.Description)
+ }
+
+ if todo.Completed != false {
+ t.Errorf("Expected completed to be false, got %t", todo.Completed)
+ }
+}
+
+func TestGetTodoByID(t *testing.T) {
+ db := setupTestDB(t)
+ populateTestData(t, db)
+ service := NewTodoService(db)
+
+ // Get a todo by ID
+ todo, err := service.GetTodoByID(1)
+ if err != nil {
+ t.Fatalf("Failed to get todo by ID: %v", err)
+ }
+
+ if todo == nil {
+ t.Fatalf("Expected todo to be found, got nil")
+ }
+
+ if todo.ID != 1 {
+ t.Errorf("Expected todo ID to be 1, got %d", todo.ID)
+ }
+
+ // Test non-existent todo
+ todo, err = service.GetTodoByID(999)
+ if err != nil {
+ t.Fatalf("Expected no error for non-existent todo, got %v", err)
+ }
+
+ if todo != nil {
+ t.Errorf("Expected nil for non-existent todo, got %+v", todo)
+ }
+}
+
+func TestUpdateTodo(t *testing.T) {
+ db := setupTestDB(t)
+ populateTestData(t, db)
+ service := NewTodoService(db)
+
+ // Update a todo
+ title := "Updated Title"
+ completed := true
+ todoUpdate := dto.TodoUpdate{
+ Title: &title,
+ Completed: &completed,
+ }
+
+ todo, err := service.UpdateTodo(1, todoUpdate)
+ if err != nil {
+ t.Fatalf("Failed to update todo: %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)
+ }
+
+ // Test updating non-existent todo
+ todo, err = service.UpdateTodo(999, todoUpdate)
+ if err != nil {
+ t.Fatalf("Expected no error for non-existent todo, got %v", err)
+ }
+
+ if todo != nil {
+ t.Errorf("Expected nil for non-existent todo, got %+v", todo)
+ }
+}
+
+func TestDeleteTodo(t *testing.T) {
+ db := setupTestDB(t)
+ populateTestData(t, db)
+ service := NewTodoService(db)
+
+ // Delete a todo
+ deleted, err := service.DeleteTodo(1)
+ if err != nil {
+ t.Fatalf("Failed to delete todo: %v", err)
+ }
+
+ if !deleted {
+ t.Errorf("Expected todo to be deleted")
+ }
+
+ // Try to get the deleted todo
+ todo, err := service.GetTodoByID(1)
+ if err != nil {
+ t.Fatalf("Unexpected error after deletion: %v", err)
+ }
+
+ if todo != nil {
+ t.Errorf("Expected todo to be nil after deletion, got %+v", todo)
+ }
+
+ // Test deleting non-existent todo
+ deleted, err = service.DeleteTodo(999)
+ if err != nil {
+ t.Fatalf("Expected no error for non-existent todo, got %v", err)
+ }
+
+ if deleted {
+ t.Errorf("Expected false for deleting non-existent todo, got %t", deleted)
+ }
+}
\ No newline at end of file
diff --git a/go-todo-app/migrations/00001_create_todos_table.sql b/go-todo-app/migrations/00001_create_todos_table.sql
new file mode 100644
index 0000000..cf5ace2
--- /dev/null
+++ b/go-todo-app/migrations/00001_create_todos_table.sql
@@ -0,0 +1,14 @@
+-- Up migration
+CREATE TABLE todos (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ description TEXT,
+ completed BOOLEAN DEFAULT FALSE,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX idx_todos_title ON todos(title);
+
+-- Down migration
+DROP TABLE IF EXISTS todos;
\ No newline at end of file
diff --git a/go-todo-app/storage/db/.gitkeep b/go-todo-app/storage/db/.gitkeep
new file mode 100644
index 0000000..e69de29