diff --git a/.env b/.env index e69de29..734e151 100644 --- a/.env +++ b/.env @@ -0,0 +1,4 @@ +DB_USER=mx +DB_NAME=crud_db +DB_HOST=localhost +DB_PORT=5432 \ No newline at end of file diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..9a17644 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,61 @@ +name: User Service CI + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: userdb + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.21 + + - name: Install Dependencies + run: go mod tidy + + - name: Create .env file for Testing + run: | + echo "JWT_SECRET=testsecret" > .env + echo "DB_HOST=localhost" >> .env + echo "DB_PORT=5432" >> .env + echo "DB_USER=postgres" >> .env + echo "DB_PASSWORD=postgres" >> .env + echo "DB_NAME=userdb" >> .env + + - name: Run Tests + env: + JWT_SECRET: testsecret + DB_HOST: localhost + DB_PORT: 5432 + DB_USER: postgres + DB_PASSWORD: postgres + DB_NAME: userdb + run: go test ./tests/ -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65c62aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +.galus.toml +go.mod +go.sum \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b9c819 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.21 + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod tidy + +COPY . . + +RUN go build -o main ./cmd/main.go + +CMD ["./main"] diff --git a/README.md b/README.md index ff681de..69bac00 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ +[![Go Build and Test with Database](https://github.com/mx-gp/user_crud/actions/workflows/workflow.yml/badge.svg?branch=develop)](https://github.com/mx-gp/user_crud/actions/workflows/workflow.yml) + [![develop](https://github.com/mx-gp/user_crud/actions/workflows/workflow.yml/badge.svg)](https://github.com/mx-gp/user_crud/actions/workflows/workflow.yml) diff --git a/main.go b/cmd/main.go similarity index 78% rename from main.go rename to cmd/main.go index a3aeac5..d3be97e 100644 --- a/main.go +++ b/cmd/main.go @@ -1,7 +1,8 @@ -package main +package cmd import ( "user_crud/config" + "user_crud/routes" "github.com/gin-gonic/gin" ) @@ -13,7 +14,7 @@ func main() { config.ConnectDB() // Register Routes - UserRoutes(r) + routes.UserRoutes(r) // Start Server r.Run(":8080") diff --git a/controllers/user.go b/controllers/user.go index 3561acc..4b58440 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" "user_crud/models" + "user_crud/repository" "user_crud/utils" "github.com/gin-gonic/gin" @@ -17,7 +18,7 @@ func CreateUser(c *gin.Context) { return } - err := models.CreateUser(user) + err := repository.CreateUser(user) if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to create user") return @@ -28,7 +29,7 @@ func CreateUser(c *gin.Context) { // Get All Users func GetAllUsers(c *gin.Context) { - users, err := models.GetAllUsers() + users, err := repository.GetAllUsers() if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to fetch users") return @@ -39,7 +40,7 @@ func GetAllUsers(c *gin.Context) { // Get User by ID func GetUserByID(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - user, err := models.GetUserByID(id) + user, err := repository.GetUserByID(id) if err != nil { utils.SendErrorResponse(c, http.StatusNotFound, "User not found") return @@ -55,7 +56,7 @@ func UpdateUser(c *gin.Context) { return } - err := models.UpdateUser(user) + err := repository.UpdateUser(user) if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to update user") return @@ -68,7 +69,7 @@ func UpdateUser(c *gin.Context) { func DeleteUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - err := models.DeleteUser(id) + err := repository.DeleteUser(id) if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to delete user") return diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9f2330a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.8" + +services: + user-service: + build: . + ports: + - "8080:8080" + depends_on: + - db + env_file: + - .env + + db: + image: postgres:15 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: userdb + ports: + - "5432:5432" diff --git a/go-build.yml b/go-build.yml deleted file mode 100644 index ebfd0dc..0000000 --- a/go-build.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Go Build Check - -on: - push: - branches: - - "master" - - "develop" - pull_request: - branches: - - "master" - - "develop" - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.23.4 # Adjust the version as needed - - - name: Install Dependencies - run: go mod tidy - - - name: Build Project - run: go build -v ./... diff --git a/go.mod b/go.mod index 354061c..0793674 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,49 @@ module user_crud -go 1.23.4 +go 1.24.4 + +toolchain go1.24.5 require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-gonic/gin v1.10.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + golang.org/x/time v0.10.0 ) require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/aliftech/galus v0.0.0-20250726090607-302d48f29cd0 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // 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.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // 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.2.2 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.15.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e9d3e92..a30cf6f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/aliftech/galus v0.0.0-20250726090607-302d48f29cd0 h1:C8DDNoVqUMxoB+fdQVuIgRd3u8WC47VSG/Bovv3XbCU= +github.com/aliftech/galus v0.0.0-20250726090607-302d48f29cd0/go.mod h1:nElRcG5JIPLyaZOYuKKa3xCp5JErL3kqY3bdixdm74c= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -6,9 +10,16 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -28,6 +39,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -40,6 +53,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -51,6 +67,11 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -74,12 +95,17 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..7e8cc65 --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" + "user_crud/utils" + + "github.com/gin-gonic/gin" +) + +// AuthMiddleware - Protect routes with JWT authentication +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" || !utils.ValidateJWT(token) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go new file mode 100644 index 0000000..baea2a0 --- /dev/null +++ b/middleware/ratelimit.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +var limiter = rate.NewLimiter(1, 5) // 1 request per second, burst up to 5 + +func RateLimit() gin.HandlerFunc { + return func(c *gin.Context) { + if !limiter.Allow() { + c.AbortWithStatusJSON(429, gin.H{"error": "Too many requests"}) + return + } + c.Next() + } +} diff --git a/models/user.go b/models/user.go index 7bb6639..010ae0c 100644 --- a/models/user.go +++ b/models/user.go @@ -1,56 +1,8 @@ package models -import "user_crud/config" - type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` } - -// Create User -func CreateUser(user User) error { - _, err := config.DB.Exec("INSERT INTO users (name, email, age) VALUES ($1, $2, $3)", user.Name, user.Email, user.Age) - return err -} - -// Get All Users -func GetAllUsers() ([]User, error) { - rows, err := config.DB.Query("SELECT id, name, email, age FROM users") - if err != nil { - return nil, err - } - defer rows.Close() - - var users []User - for rows.Next() { - var user User - err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age) - if err != nil { - return nil, err - } - users = append(users, user) - } - - return users, nil -} - -// Get User by ID -func GetUserByID(id int) (User, error) { - var user User - err := config.DB.QueryRow("SELECT id, name, email, age FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email, &user.Age) - return user, err -} - -// Update User -func UpdateUser(user User) error { - _, err := config.DB.Exec("UPDATE users SET name=$1, email=$2, age=$3 WHERE id=$4", user.Name, user.Email, user.Age, user.ID) - return err -} - -// Delete User -func DeleteUser(id int) error { - _, err := config.DB.Exec("DELETE FROM users WHERE id = $1", id) - return err -} diff --git a/repository/user_repository.go b/repository/user_repository.go new file mode 100644 index 0000000..044c119 --- /dev/null +++ b/repository/user_repository.go @@ -0,0 +1,52 @@ +package repository + +import ( + "user_crud/config" + "user_crud/models" +) + +// Create User +func CreateUser(user models.User) error { + _, err := config.DB.Exec("INSERT INTO users (name, email, age) VALUES ($1, $2, $3)", user.Name, user.Email, user.Age) + return err +} + +// Get All Users +func GetAllUsers() ([]models.User, error) { + rows, err := config.DB.Query("SELECT id, name, email, age FROM users") + if err != nil { + return nil, err + } + defer rows.Close() + + var users []models.User + for rows.Next() { + var user models.User + err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age) + if err != nil { + return nil, err + } + users = append(users, user) + } + + return users, nil +} + +// Get User by ID +func GetUserByID(id int) (models.User, error) { + var user models.User + err := config.DB.QueryRow("SELECT id, name, email, age FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email, &user.Age) + return user, err +} + +// Update User +func UpdateUser(user models.User) error { + _, err := config.DB.Exec("UPDATE users SET name=$1, email=$2, age=$3 WHERE id=$4", user.Name, user.Email, user.Age, user.ID) + return err +} + +// Delete User +func DeleteUser(id int) error { + _, err := config.DB.Exec("DELETE FROM users WHERE id = $1", id) + return err +} diff --git a/routes.go b/routes/routes.go similarity index 96% rename from routes.go rename to routes/routes.go index eb94245..4a151da 100644 --- a/routes.go +++ b/routes/routes.go @@ -1,4 +1,4 @@ -package main +package routes import ( "user_crud/controllers" diff --git a/tests/user_test.go b/tests/user_test.go new file mode 100644 index 0000000..30afa5e --- /dev/null +++ b/tests/user_test.go @@ -0,0 +1,189 @@ +package tests + +import ( + "database/sql" + "fmt" + "log" + "os" + "strings" + "testing" + + "user_crud/config" + "user_crud/models" + "user_crud/repository" + + "github.com/joho/godotenv" + _ "github.com/lib/pq" // PostgreSQL driver +) + +func setupTestDB() { + currentDir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + dir := strings.Replace(currentDir, "/tests", "", 1) + fmt.Println("PRint: ", dir) + // Load environment variables + err = godotenv.Load(dir + "/.env") + if err != nil { + log.Fatal("Error loading .env file: ", err) + } + dbUser := os.Getenv("DB_USER") + dbName := os.Getenv("DB_NAME") + dbPassword := os.Getenv("DB_PASSWORD") + dbHost := os.Getenv("DB_HOST") + dbPort := os.Getenv("DB_PORT") + + dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName) + fmt.Printf("%+v", dsn) + config.DB, err = sql.Open("postgres", dsn) + if err != nil { + panic(err) + } + + // Create a test table (optional) + _, err = config.DB.Exec(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100), + email VARCHAR(100) UNIQUE, + age INT + ); + `) + // fmt.Println(r.RowsAffected()) + if err != nil { + panic(err) + } +} + +func teardownTestDB() { + // Drop test data after each test + config.DB.Exec("DELETE FROM users") + config.DB.Close() +} + +func TestCreateUser(t *testing.T) { + setupTestDB() + defer teardownTestDB() + + user := models.User{Name: "Alice", Email: "alice@example.com", Age: 28} + err := repository.CreateUser(user) + if err != nil { + t.Errorf("Failed to create user: %v", err) + } + + var count int + err = config.DB.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", user.Email).Scan(&count) + if err != nil || count != 1 { + t.Errorf("User was not inserted into the database") + } + + // Delete user + err = repository.DeleteUser(user.ID) + if err != nil { + t.Errorf("Failed to delete user: %v", err) + } +} + +func TestGetAllUsers(t *testing.T) { + setupTestDB() + defer teardownTestDB() + + // Insert test users + config.DB.Exec("INSERT INTO users (name, email, age) VALUES ('Bob', 'bob@example.com', 25), ('Charlie', 'charlie@example.com', 29)") + + users, err := repository.GetAllUsers() + if err != nil { + t.Errorf("Failed to fetch users: %v", err) + } + + if len(users) < 2 { + t.Errorf("Expected at least 2 users, got %d", len(users)) + } + + for _, user := range users { + // Delete user + err = repository.DeleteUser(user.ID) + if err != nil { + t.Errorf("Failed to delete user: %v", err) + } + } +} + +func TestGetUserByID(t *testing.T) { + setupTestDB() + defer teardownTestDB() + + // Insert a test user + var id int + config.DB.QueryRow("INSERT INTO users (name, email, age) VALUES ('David', 'david@example.com', 40) RETURNING id").Scan(&id) + + user, err := repository.GetUserByID(id) + if err != nil { + t.Errorf("Failed to get user by ID: %v", err) + } + + if user.Email != "david@example.com" { + t.Errorf("Expected email 'david@example.com', got %s", user.Email) + } + // Delete user + err = repository.DeleteUser(id) + if err != nil { + t.Errorf("Failed to delete user: %v", err) + } +} + +func TestUpdateUser(t *testing.T) { + setupTestDB() + defer teardownTestDB() + + // Insert test user + var id int + config.DB.QueryRow("INSERT INTO users (name, email, age) VALUES ('Eve', 'eve@example.com', 22) RETURNING id").Scan(&id) + + // Update the user + updatedUser := models.User{ID: id, Name: "Eve Adams", Email: "eve@example.com", Age: 30} + err := repository.UpdateUser(updatedUser) + if err != nil { + t.Errorf("Failed to update user: %v", err) + } + + // Verify update + var name string + var age int + config.DB.QueryRow("SELECT name, age FROM users WHERE id = $1", id).Scan(&name, &age) + + if name != "Eve Adams" || age != 30 { + t.Errorf("User was not updated correctly: got name=%s, age=%d", name, age) + } + + // Delete user + err = repository.DeleteUser(id) + if err != nil { + t.Errorf("Failed to delete user: %v", err) + } +} + +func TestDeleteUser(t *testing.T) { + setupTestDB() + defer teardownTestDB() + + // Insert test user + var id int + config.DB.QueryRow("INSERT INTO users (name, email, age) VALUES ('Frank', 'frank@example.com', 45) RETURNING id").Scan(&id) + + // Delete user + err := repository.DeleteUser(id) + if err != nil { + t.Errorf("Failed to delete user: %v", err) + } + + // Verify deletion + var count int + config.DB.QueryRow("SELECT COUNT(*) FROM users WHERE id = $1", id).Scan(&count) + + if count != 0 { + t.Errorf("User was not deleted") + } +} diff --git a/utils/jwt.go b/utils/jwt.go new file mode 100644 index 0000000..8c8989e --- /dev/null +++ b/utils/jwt.go @@ -0,0 +1,39 @@ +package utils + +import ( + "errors" + "os" + "time" + + "github.com/dgrijalva/jwt-go" +) + +// Secret key (Use environment variables in production) +var jwtSecret = []byte(os.Getenv("JWT_SECRET")) + +// Claims structure +type Claims struct { + UserID uint `json:"user_id"` + jwt.StandardClaims +} + +// ValidateJWT verifies and parses the token +func ValidateJWT(tokenString string) bool { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return jwtSecret, nil + }) + + if err != nil { + return false + } + + // Check if token is valid and not expired + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims.ExpiresAt > time.Now().Unix() + } + + return false +}