From 228e60f5b9eb6b6ab9705ec13d628521950ca2a1 Mon Sep 17 00:00:00 2001 From: Steve White Date: Fri, 4 Oct 2024 20:10:35 -0500 Subject: [PATCH] Initial Commit --- ApplicationDescription.md | 105 ++++++++++++ Dockerfile | 22 +++ config.go | 41 +++++ config.yaml | 6 + data/boxes.db | Bin 0 -> 49152 bytes data/my_test_database.db | Bin 0 -> 32768 bytes db.go | 42 +++++ go.mod | 19 +++ go.sum | 48 ++++++ handlers.go | 254 ++++++++++++++++++++++++++++ main.go | 57 +++++++ main_test.go | 348 ++++++++++++++++++++++++++++++++++++++ test2.bash | 174 +++++++++++++++++++ tests.bash | 176 +++++++++++++++++++ 14 files changed, 1292 insertions(+) create mode 100644 ApplicationDescription.md create mode 100644 Dockerfile create mode 100644 config.go create mode 100644 config.yaml create mode 100644 data/boxes.db create mode 100644 data/my_test_database.db create mode 100644 db.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers.go create mode 100644 main.go create mode 100644 main_test.go create mode 100755 test2.bash create mode 100755 tests.bash diff --git a/ApplicationDescription.md b/ApplicationDescription.md new file mode 100644 index 0000000..254e646 --- /dev/null +++ b/ApplicationDescription.md @@ -0,0 +1,105 @@ +# Application Overview: +I want to build a back-end application using Go that provides an API for managing boxes and items stored in those boxes. The app should be hosted in a Docker container and use SQLite3 as the database. Additionally, I want a config.yaml file to manage configuration (database path, JWT secret, and image storage directory). It should also support JWT-based authentication with a default user of 'boxuser' and password 'boxuser'. + +I would like it to log all logins, box creation/deletion, and item creation/deletion to a local log file, specified in config.yaml. + +# Database Tables: +- `boxes`: A table containing an ID and a name. +- `items`: A table containing an item name, description, the ID of the box it is stored in, and an optional path to an image of the item. +- `users`: A table containing usernames and passwords (hashed) for authentication. + +# API Endpoints: +1. Authentication: + - POST `/login`: Authenticates a user and returns a JWT. +2. Boxes: + - GET `/boxes`: Retrieves all boxes. + - POST `/boxes`: Creates a new box. +3. Items: + - GET `/items`: Retrieves all items, optionally searchable by description. + - POST `/items`: Adds a new item to a box. + - GET `/items/{id}`: Retrieves an item by its ID. + - PUT `/items/{id}`: Updates an existing item. + - GET `/items/{id}/items`: Retrieves all items in box with this id. + - DELETE `/items/{id}`: Deletes an item by its ID. + - GET `/items/{id}/image`: Retrieves the image of an item. + +# Additional Details: +- If the database doesn’t exist, it should be created automatically when the app starts. +- Images should be stored locally, and their paths should be saved in the database. +- The default user for the app should be 'boxuser' with a password of 'boxuser'. + +Here's clarification in yaml format: + +```yaml +app_overview: + language: Go + database: SQLite3 + docker: true + authentication: JWT + config_file: config.yaml + +database_tables: + boxes: + columns: + - id + - name + items: + columns: + - id + - name + - description + - box_id + - image_path + users: + columns: + - id + - username + - password + +api_endpoints: + login: + method: POST + path: /login + description: "Authenticate a user and return a JWT." + boxes: + - method: GET + path: /boxes + description: "Retrieve all boxes." + - method: POST + path: /boxes + description: "Create a new box." + items: + - method: GET + path: /items + description: "Retrieve all items, searchable by description." + - method: POST + path: /items + description: "Add a new item to a box." + - method: GET + path: /items/{id} + description: "Retrieve an item by its ID." + - method: GET + path: /items/{id}/items + description: "Retrieve all items in box with this id." + - method: PUT + path: /items/{id} + description: "Update an existing item." + - method: DELETE + path: /items/{id} + description: "Delete an item by its ID." + - method: GET + path: /items/{id}/image + description: "Retrieve the image of an item." + +config_file: + database_path: "data/boxes.db" + jwt_secret: "super_secret_key" + image_storage_dir: "images/" + listening_port: 8080 + log_file: "boxes.log" + +default_user: + username: "boxuser" + password: "boxuser" + +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1433338 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# Copy the source code +COPY . . + +# Build the Go application +RUN go mod tidy +RUN go build -o main . + +# Mount the data directory +VOLUME /app/data + +# Copy config.yaml from the application directory +COPY config.yaml /app/ + +# Expose the port your application listens on +EXPOSE 8080 + +# Command to run your application +CMD ["/app/main"] \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..3d55751 --- /dev/null +++ b/config.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "gopkg.in/yaml.v2" +) + +// Define the Config struct +type Config struct { + DatabasePath string `yaml:"database_path"` + TestDatabasePath string `yaml:"test_database_path"` + JWTSecret string `yaml:"jwt_secret"` + ImageStorageDir string `yaml:"image_storage_dir"` + ListeningPort int `yaml:"listening_port"` + LogFile string `yaml:"log_file"` +} + +func LoadConfig(configPath string) (*Config, error) { + data, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %v", err) + } + + var config Config + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal config file: %v", err) + } + + // Get the database path from environment variables or use the default + dbPath := os.Getenv("TEST_DATABASE_PATH") + if dbPath != "" { + fmt.Println("Using test database path from environment variable:", dbPath) + config.DatabasePath = dbPath + } + + return &config, nil +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..9713c08 --- /dev/null +++ b/config.yaml @@ -0,0 +1,6 @@ +database_path: "data/boxes.db" +test_database_path: "data/test_database.db" +jwt_secret: "super_secret_key" +image_storage_dir: "images/" +listening_port: 8080 +log_file: "boxes.log" \ No newline at end of file diff --git a/data/boxes.db b/data/boxes.db new file mode 100644 index 0000000000000000000000000000000000000000..b93a314d1dd9b3ed22a7648a90b9382ce7c6aaa7 GIT binary patch literal 49152 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 cjw|!b^*z)NA0zGBpG5`Po literal 0 HcmV?d00001 diff --git a/data/my_test_database.db b/data/my_test_database.db new file mode 100644 index 0000000000000000000000000000000000000000..14975b39a6fc2eba4f3e215f5c78a47ebbaf70f8 GIT binary patch literal 32768 zcmeI)J5Sp{00;1M67r}>oy}9#(@lv;kOMIVrt*j?B%m!t6|NW1rFTcuIOb6?v>^09C6{~W{BsuP&VKcB)r~ma?F613(P>g5nns>d zN(h+|@4R>qLRKU)gN*ppV(F`rGh}kjW=w;35 z^L5HpTUH)+uF&IW*_>e*+KCt0I|p2~P*LvpkedsOtIa`IT4t`psOv|3j|a3HxSkyx z(YO4F+P$db`t5*w+>feEB<%AEGU|v_{8(~g%8=MyiLv+!+?Gy2XB+WU4; zp0cb;m38efeBTMgG~(s7vOSiN5?5AUTuSA$7o?KSl$0t=NF8RB)W-!SrCO+HY*gyJ z>;IIxOv&M4J8-*^+wn)Cs%)pzZ^h1Hm5GzM$6H-H+8=EyOw$Qd(U-?fRX%gUG}%m< zs=`sz7pbfOtqT%6zuxo|mE#HR{P%3BG1YgfC^gbw zBJhyi?pdN0d2OE3UlaYSh!7wE0SG_<0uX=z1Rwwb2tWV=5V(QBRw+x?)<}KIoNCnS zX3cESx;0;)n{U)7O>@p_%+yS4-ZcMKzsbXhE_V8IeV(q!f0NdAHS(j&d(KW;HRvf^ldNfB*y_009U<00Izz00bZa0U_`U D-R`Sf literal 0 HcmV?d00001 diff --git a/db.go b/db.go new file mode 100644 index 0000000..0b0efb4 --- /dev/null +++ b/db.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/sqlite" +) + +// Define the Box model +type Box struct { + gorm.Model + Name string `json:"name"` +} + +// Define the Item model +type Item struct { + gorm.Model + Name string `json:"name"` + Description string `json:"description"` + BoxID uint `json:"box_id"` + ImagePath *string `json:"image_path"` +} + +// Define the User model +type User struct { + gorm.Model + Username string `json:"username"` + Password string `json:"password"` +} + +func ConnectDB(dbPath string) (*gorm.DB, error) { + db, err := gorm.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %v", err) + } + + // AutoMigrate will create the tables if they don't exist + db.AutoMigrate(&Box{}, &Item{}, &User{}) + + return db, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..89b266c --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module boxes-api + +go 1.21.1 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gorilla/mux v1.8.1 + github.com/jinzhu/gorm v1.9.16 + github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/mattn/go-sqlite3 v1.14.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5a571ab --- /dev/null +++ b/go.sum @@ -0,0 +1,48 @@ +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +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/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +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/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..7f74c4b --- /dev/null +++ b/handlers.go @@ -0,0 +1,254 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" +) + +// Define contextKey globally within the package +type contextKey string + +// Define your key as a constant of the custom type +const userKey contextKey = "user" + +// LoginRequest represents the request body for the /login endpoint. +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// LoginResponse represents the response body for the /login endpoint. +type LoginResponse struct { + Token string `json:"token"` +} + +// loginHandler handles the /login endpoint. +func LoginHandler(w http.ResponseWriter, r *http.Request) { + var req LoginRequest + fmt.Println(db, config) + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Check if the user exists and the password matches + var user User + db.Where("username = ?", req.Username).First(&user) + if user.ID == 0 || user.Password != req.Password { + http.Error(w, "Invalid username or password", http.StatusUnauthorized) + return + } + + // Generate JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "username": user.Username, + "exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours + }) + + tokenString, err := token.SignedString([]byte(config.JWTSecret)) + if err != nil { + http.Error(w, "Failed to generate token", http.StatusInternalServerError) + return + } + + // Return the token in the response + json.NewEncoder(w).Encode(LoginResponse{Token: tokenString}) +} + +// getBoxesHandler handles the GET /boxes endpoint. +func GetBoxesHandler(w http.ResponseWriter, r *http.Request) { + var boxes []Box + db.Find(&boxes) + json.NewEncoder(w).Encode(boxes) +} + +// createBoxHandler handles the POST /boxes endpoint. +func CreateBoxHandler(w http.ResponseWriter, r *http.Request) { + var box Box + err := json.NewDecoder(r.Body).Decode(&box) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + db.Create(&box) + + // Create a response struct to include the ID + type createBoxResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + } + + response := createBoxResponse{ + ID: box.ID, + Name: box.Name, + } + + json.NewEncoder(w).Encode(response) +} + +// deleteBoxHandler handles the DELETE /boxes/{id} endpoint. +func DeleteBoxHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + // Retrieve the box from the database + var box Box + if err := db.First(&box, id).Error; err != nil { + http.Error(w, "Box not found", http.StatusNotFound) + return + } + + // Optionally, delete associated items (if you want cascading delete) + // db.Where("box_id = ?", id).Delete(&Item{}) + + // Delete the box + db.Delete(&box) + + w.WriteHeader(http.StatusNoContent) // 204 No Content +} + +// getItemsHandler handles the GET /items endpoint. +func GetItemsHandler(w http.ResponseWriter, r *http.Request) { + var items []Item + db.Find(&items) + json.NewEncoder(w).Encode(items) +} + +// createItemHandler handles the POST /items endpoint. +func CreateItemHandler(w http.ResponseWriter, r *http.Request) { + var item Item + err := json.NewDecoder(r.Body).Decode(&item) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + db.Create(&item) + + // Create a response struct to include the ID + type createItemResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + } + + response := createItemResponse{ + ID: item.ID, + Name: item.Name, + } + + json.NewEncoder(w).Encode(response) +} + +// getItemHandler handles the GET /items/{id} endpoint. +func GetItemHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + var item Item + if err := db.First(&item, id).Error; err != nil { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(item) +} + +// getItemsInBoxHandler handles the GET /items/{id}/items endpoint. +func GetItemsInBoxHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + var items []Item + if err := db.Where("box_id = ?", id).Find(&items).Error; err != nil { + http.Error(w, "Items not found", http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(items) +} + +// updateItemHandler handles the PUT /items/{id} endpoint. +func UpdateItemHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + var item Item + if err := db.First(&item, id).Error; err != nil { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + + err := json.NewDecoder(r.Body).Decode(&item) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + db.Save(&item) + json.NewEncoder(w).Encode(item) +} + +// deleteItemHandler handles the DELETE /items/{id} endpoint. +func DeleteItemHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + var item Item + if err := db.First(&item, id).Error; err != nil { + http.Error(w, "Item not found", http.StatusNotFound) + return + } + + db.Delete(&item) + w.WriteHeader(http.StatusNoContent) +} + +// authMiddleware is a middleware function that checks for a valid JWT token in the request header. +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get the token from the request header + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + http.Error(w, "Authorization header missing", http.StatusUnauthorized) + return + } + + // Remove "Bearer " prefix from token string + tokenString = strings.Replace(tokenString, "Bearer ", "", 1) + + // Parse and validate the JWT token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Make sure that the signing method is HMAC + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(config.JWTSecret), nil + }) + if err != nil || !token.Valid { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Extract the user claims from the token + if claims, ok := token.Claims.(jwt.MapClaims); ok { + // Add the "user" claim to the request context + newCtx := context.WithValue(r.Context(), userKey, claims["username"]) + r = r.WithContext(newCtx) + } else { + http.Error(w, "Invalid token claims", http.StatusUnauthorized) + return + } + + // Call the next handler in the chain + next.ServeHTTP(w, r) + }) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..11ee2be --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/jinzhu/gorm" +) + +var ( + db *gorm.DB // Declare db globally + config *Config +) + +func main() { + var err error + config, err = LoadConfig("config.yaml") + fmt.Println(config.DatabasePath) + fmt.Println(config.ImageStorageDir) + fmt.Println(config.JWTSecret) + fmt.Println(config.LogFile) + fmt.Println(config.ListeningPort) + if err != nil || config == nil { + log.Fatalf("Failed to load config: %v", err) + } + + // Conne:ct to the database + db, err = ConnectDB(config.DatabasePath) + fmt.Println("DB Connection String:", db.DB().Ping()) + if err != nil || db == nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer db.Close() + + fmt.Println("Default user 'boxuser' created successfully!") + + // Create the router + router := mux.NewRouter() + + // Apply JWT authentication middleware to protected endpoints + router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST") + router.Handle("/boxes", AuthMiddleware(http.HandlerFunc(GetBoxesHandler))).Methods("GET") + router.Handle("/boxes", AuthMiddleware(http.HandlerFunc(CreateBoxHandler))).Methods("POST") + router.Handle("/boxes/{id}", AuthMiddleware(http.HandlerFunc(DeleteBoxHandler))).Methods("DELETE") + router.Handle("/items", AuthMiddleware(http.HandlerFunc(GetItemsHandler))).Methods("GET") + router.Handle("/items", AuthMiddleware(http.HandlerFunc(CreateItemHandler))).Methods("POST") + router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(GetItemHandler))).Methods("GET") + router.Handle("/items/{id}/items", AuthMiddleware(http.HandlerFunc(GetItemsInBoxHandler))).Methods("GET") + router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(UpdateItemHandler))).Methods("PUT") + router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(DeleteItemHandler))).Methods("DELETE") + + // Start the server + fmt.Printf("Server listening on port %d\n", config.ListeningPort) + http.ListenAndServe(fmt.Sprintf(":%d", config.ListeningPort), router) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..c19817c --- /dev/null +++ b/main_test.go @@ -0,0 +1,348 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +func TestMain(m *testing.M) { + // Load the configuration + var err error + config, err = LoadConfig("config.yaml") + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + // Set the environment variable for the test database + os.Setenv("TEST_DATABASE_PATH", "data/my_test_database.db") + config.DatabasePath = os.Getenv("TEST_DATABASE_PATH") + + // Connect to the database using the test database path + db, err = ConnectDB(config.DatabasePath) + if err != nil { + log.Fatalf("Failed to connect to test database: %v", err) + } + + fmt.Println("DB is connected") + + defer db.Close() + + // Truncate tables before running tests + for _, table := range []string{"boxes", "items", "users"} { // Add all your table names here + if err := db.Exec(fmt.Sprintf("DELETE FROM %s", table)).Error; err != nil { + log.Fatalf("Failed to truncate table %s: %v", table, err) + } + } + + db.LogMode(true) + + // Run the tests + exitCode := m.Run() + + os.Exit(exitCode) +} +func TestGetBoxes(t *testing.T) { + + // 1. Create a request (no need for a real token in testing) + req := httptest.NewRequest("GET", "/boxes", nil) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + + // 2. Create a recorder + rr := httptest.NewRecorder() + + // 3. Initialize your router + router := mux.NewRouter() + router.Handle("/boxes", http.HandlerFunc(GetBoxesHandler)).Methods("GET") + + // 4. Serve the request + router.ServeHTTP(rr, req) + + // 5. Assert the response + assert.Equal(t, http.StatusOK, rr.Code) + // Add more assertions to check response body, headers, etc. +} + +func TestCreateBox(t *testing.T) { + // 1. Create a request with a new box in the body + newBox := Box{Name: "Test Box"} + reqBody, _ := json.Marshal(newBox) + req := httptest.NewRequest("POST", "/boxes", bytes.NewBuffer(reqBody)) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + req.Header.Set("Content-Type", "application/json") + + // 2. Create a recorder + rr := httptest.NewRecorder() + + // 3. Initialize your router + router := mux.NewRouter() + router.Handle("/boxes", http.HandlerFunc(CreateBoxHandler)).Methods("POST") + + // 4. Serve the request + router.ServeHTTP(rr, req) + + // 5. Assert the response + assert.Equal(t, http.StatusOK, rr.Code) + + // 6. Decode the response body + var createdBox Box + json.Unmarshal(rr.Body.Bytes(), &createdBox) + + // 7. Assert the created box + assert.Equal(t, newBox.Name, createdBox.Name) +} + +func TestGetItem(t *testing.T) { + // Create a test item in the database + testItem := Item{Name: "Test Item", Description: "Test Description", BoxID: 1} + db.Create(&testItem) + + // Create a request to get the test item + req := httptest.NewRequest("GET", fmt.Sprintf("/items/%d", testItem.ID), nil) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + + // Create a response recorder + rr := httptest.NewRecorder() + + // Initialize the router + router := mux.NewRouter() + router.Handle("/items/{id}", http.HandlerFunc(GetItemHandler)).Methods("GET") + + // Serve the request + router.ServeHTTP(rr, req) + + // Check the response status code + assert.Equal(t, http.StatusOK, rr.Code) + + // Decode the response body + var retrievedItem Item + err := json.Unmarshal(rr.Body.Bytes(), &retrievedItem) + assert.NoError(t, err) + + // Check if the retrieved item matches the test item + assert.Equal(t, testItem.ID, retrievedItem.ID) + assert.Equal(t, testItem.Name, retrievedItem.Name) + assert.Equal(t, testItem.Description, retrievedItem.Description) + assert.Equal(t, testItem.BoxID, retrievedItem.BoxID) + + fmt.Println("TestGetItem") +} + +func TestGetItemsInBox(t *testing.T) { + // Create test items associated with a specific box + testBox := Box{Name: "Test Box for Items"} + fmt.Println("testBox.ID (before create):", testBox.ID) // Should be 0 + + if err := db.Create(&testBox).Error; err != nil { // Check for errors! + t.Fatalf("Failed to create test box: %v", err) + } + + // temporarily disable callbacks + db.Callback().Create().Replace("gorm:create", nil) + + fmt.Println("testBox.ID (after create):", testBox.ID) // Should be a non-zero value + + defaultImagePath := "default.jpg" + + testItems := []Item{ + {Name: "Item 1", Description: "Description 1", BoxID: testBox.ID, ImagePath: &defaultImagePath}, // Use "" for empty string + {Name: "Item 2", Description: "Description 2", BoxID: testBox.ID, ImagePath: &defaultImagePath}, // Use "" for empty string + } + + fmt.Println("Right before creating test items in database") + + // Marshal the testItems slice to JSON + jsonData, err := json.MarshalIndent(testItems, "", " ") // Use " " for indentation + if err != nil { + t.Fatalf("Failed to marshal testItems to JSON: %v", err) + } + + // Print the formatted JSON + fmt.Println("testItems:", string(jsonData)) + + if err := db.Create(&testItems).Error; err != nil { // Check for errors! + t.Fatalf("Failed to create test items: %v", err) + } + fmt.Println("Right AFTER creating test items in database") + + // Create a request to get items in the test box + req := httptest.NewRequest("GET", fmt.Sprintf("/items/%d/items", testBox.ID), nil) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + + // Create a response recorder + rr := httptest.NewRecorder() + + // Initialize the router + router := mux.NewRouter() + router.Handle("/items/{id}/items", http.HandlerFunc(GetItemsInBoxHandler)).Methods("GET") + + // Serve the request + router.ServeHTTP(rr, req) + + // Check the response status code + assert.Equal(t, http.StatusOK, rr.Code) + + // Decode the response body + var retrievedItems []Item + err = json.Unmarshal(rr.Body.Bytes(), &retrievedItems) + assert.NoError(t, err) + + // Check if the correct number of items is retrieved + assert.Equal(t, len(testItems), len(retrievedItems)) + + // You can add more assertions to check the content of retrievedItems +} + +func TestUpdateItem(t *testing.T) { + // Create a test item in the database + testItem := Item{Name: "Test Item", Description: "Test Description", BoxID: 1} + db.Create(&testItem) + + // Create a request to update the test item + updatedItem := Item{Name: "Updated Item", Description: "Updated Description"} + reqBody, _ := json.Marshal(updatedItem) + req := httptest.NewRequest("PUT", fmt.Sprintf("/items/%d", testItem.ID), bytes.NewBuffer(reqBody)) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + req.Header.Set("Content-Type", "application/json") + + // Create a response recorder + rr := httptest.NewRecorder() + + // Initialize the router + router := mux.NewRouter() + router.Handle("/items/{id}", http.HandlerFunc(UpdateItemHandler)).Methods("PUT") + + // Serve the request + router.ServeHTTP(rr, req) + + // Check the response status code + assert.Equal(t, http.StatusOK, rr.Code) + + // Retrieve the updated item from the database + var dbItem Item + db.First(&dbItem, testItem.ID) + + // Check if the item is updated in the database + assert.Equal(t, updatedItem.Name, dbItem.Name) + assert.Equal(t, updatedItem.Description, dbItem.Description) +} + +func TestDeleteItem(t *testing.T) { + // Create a test item in the database + testItem := Item{Name: "Test Item", Description: "Test Description", BoxID: 1} + db.Create(&testItem) + + // Create a request to delete the test item + req := httptest.NewRequest("DELETE", fmt.Sprintf("/items/%d", testItem.ID), nil) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + + // Create a response recorder + rr := httptest.NewRecorder() + + // Initialize the router + router := mux.NewRouter() + router.Handle("/items/{id}", http.HandlerFunc(DeleteItemHandler)).Methods("DELETE") + + // Serve the request + router.ServeHTTP(rr, req) + + // Check the response status code + assert.Equal(t, http.StatusNoContent, rr.Code) + + // Try to retrieve the deleted item from the database + var deletedItem Item + err := db.First(&deletedItem, testItem.ID).Error + assert.Error(t, err) // Expect an error because the item should be deleted +} + +func TestCreateItem(t *testing.T) { + // 1. Create a request with a new item in the body + newItem := Item{Name: "Test Item", Description: "Test Description", BoxID: 1} + reqBody, _ := json.Marshal(newItem) + req := httptest.NewRequest("POST", "/items", bytes.NewBuffer(reqBody)) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + req.Header.Set("Content-Type", "application/json") + + // 2. Create a recorder + rr := httptest.NewRecorder() + + // 3. Initialize your router + router := mux.NewRouter() + router.Handle("/items", http.HandlerFunc(CreateItemHandler)).Methods("POST") + + // 4. Serve the request + router.ServeHTTP(rr, req) + + // 5. Assert the response + assert.Equal(t, http.StatusOK, rr.Code) + + // 6. Decode the response body + var createdItem Item + json.Unmarshal(rr.Body.Bytes(), &createdItem) + + // 7. Assert the created item + assert.Equal(t, newItem.Name, createdItem.Name) + assert.Equal(t, newItem.Description, createdItem.Description) + assert.Equal(t, newItem.BoxID, createdItem.BoxID) +} + +func TestGetItems(t *testing.T) { + // 1. Create a request (no need for a real token in testing) + req := httptest.NewRequest("GET", "/items", nil) + req = req.WithContext(context.WithValue(req.Context(), userKey, "testuser")) // Simulate authenticated user + + // 2. Create a recorder + rr := httptest.NewRecorder() + + // 3. Initialize your router + router := mux.NewRouter() + router.Handle("/items", http.HandlerFunc(GetItemsHandler)).Methods("GET") + + // 4. Serve the request + router.ServeHTTP(rr, req) + + // 5. Assert the response + assert.Equal(t, http.StatusOK, rr.Code) + // Add more assertions to check response body, headers, etc. +} + +func ExampleLoginHandler() { + // Create a request with login credentials + loginReq := LoginRequest{ + Username: "testuser", + Password: "testpassword", + } + reqBody, _ := json.Marshal(loginReq) + + req := httptest.NewRequest("POST", "/login", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + + // Create a response recorder + rr := httptest.NewRecorder() + + // Create a test handler (usually your LoginHandler) + handler := http.HandlerFunc(LoginHandler) + + // Serve the request + handler.ServeHTTP(rr, req) + + // Check the response status code + if rr.Code != http.StatusOK { + fmt.Printf("Login failed with status code: %d\n", rr.Code) + } else { + // Decode the response body to get the token + var loginResp LoginResponse + json.Unmarshal(rr.Body.Bytes(), &loginResp) + + fmt.Println("Login successful! Token:", loginResp.Token) + } +} diff --git a/test2.bash b/test2.bash new file mode 100755 index 0000000..3740270 --- /dev/null +++ b/test2.bash @@ -0,0 +1,174 @@ +#!/bin/bash + +# API base URL +API_BASE_URL="http://localhost:8080" + +# Login credentials +USERNAME="boxuser" +PASSWORD="boxuser" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Function to make an authenticated request +function authenticated_request() { + local method=$1 + local endpoint=$2 + local data=$3 + + # Get a new JWT token + TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ + "$API_BASE_URL/login" | jq -r '.token') + + # Make the authenticated request + curl -s -X $method -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "$API_BASE_URL$endpoint" +} + +# --- Test Cases --- + +# 1. Login +echo +echo +echo "Testing /login..." +response=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ + "$API_BASE_URL/login") + +if [[ $(echo "$response" | jq -r '.token') != "null" ]]; then + echo -e " /login: ${GREEN}PASS${NC}" +else + echo -e " /login: ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 2. Get Boxes +echo +echo +echo "Testing /boxes (GET)..." +response=$(authenticated_request "GET" "/boxes" "") + +if [[ $(echo "$response" | jq -r '. | length') -ge 0 ]]; then + echo -e " /boxes (GET): ${GREEN}PASS${NC}" +else + echo -e " /boxes (GET): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 3. Create Box +echo +echo +echo "Testing /boxes (POST)..." +response=$(authenticated_request "POST" "/boxes" "{\"name\":\"Test Box\"}") +echo $response | jq '.' + +if [[ $(echo "$response" | jq -r '.name') == "Test Box" ]]; then + echo -e " /boxes (POST): ${GREEN}PASS${NC}" + echo $response + BOX_ID=$(echo "$response" | jq -r '.id') # Extract and save the box ID + echo $BOX_ID | jq . +else + echo -e " /boxes (POST): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 5. Create Item (Assuming a box with ID $BOX_ID exists) +echo +echo +echo "Testing /items (POST)..." +echo $BOX_ID +response=$(authenticated_request "POST" "/items" "{\"name\":\"Test Item\", \"description\":\"Test Description\", \"box_id\":$BOX_ID}") +if [[ $(echo "$response" | jq -r '.name') == "Test Item" ]]; then + echo -e " /items (POST): ${GREEN}PASS${NC}" + ITEM_ID=$(echo "$response" | jq -r '.id') # Extract and save the item ID + echo $response +else + echo -e " /items (POST): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 6. Get Items +echo +echo +echo "Testing /items (GET)..." +response=$(authenticated_request "GET" "/items" "") + +if [[ $(echo "$response" | jq -r '. | length') -ge 0 ]]; then + echo -e " /items (GET): ${GREEN}PASS${NC}" +else + echo -e " /items (GET): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 7. Get Item by ID (Using the saved ITEM_ID) +echo +echo +echo "Testing /items/{id} (GET)..." +response=$(authenticated_request "GET" "/items/$ITEM_ID" "") +echo $response | jq . + +if [[ $(echo "$response" | jq -r '.ID') == "$ITEM_ID" ]]; then + echo -e " /items/{id} (GET): ${GREEN}PASS${NC}" +else + echo -e " /items/{id} (GET): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 8. Get Items in Box (Using the saved BOX_ID) +echo +echo +echo "Testing /items/{id}/items (GET)..." +response=$(authenticated_request "GET" "/items/$BOX_ID/items" "") + +if [[ $(echo "$response" | jq -r '. | length') -ge 1 ]]; then # Expecting at least one item + echo -e " /items/{id}/items (GET): ${GREEN}PASS${NC}" +else + echo -e " /items/{id}/items (GET): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 9. Update Item (Using the saved ITEM_ID) +echo +echo +echo "Testing /items/{id} (PUT)..." +response=$(authenticated_request "PUT" "/items/$ITEM_ID" "{\"name\":\"Updated Item\", \"description\":\"Updated Description\"}") + +if [[ $(echo "$response" | jq -r '.name') == "Updated Item" ]]; then + echo -e " /items/{id} (PUT): ${GREEN}PASS${NC}" +else + echo -e " /items/{id} (PUT): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 10. Delete Item (Using the saved ITEM_ID) +echo +echo +echo "Testing /items/{id} (DELETE)..." +response=$(authenticated_request "DELETE" "/items/$ITEM_ID" "") + +if [[ "$response" == "" ]]; then # Expecting 204 No Content (empty response) + echo -e " /items/{id} (DELETE): ${GREEN}PASS${NC}" +else + echo -e " /items/{id} (DELETE): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +# 4. Delete Box (Using the saved BOX_ID) +echo +echo +echo "Testing /boxes/{id} (DELETE)..." +response=$(authenticated_request "DELETE" "/boxes/$BOX_ID" "") + +if [[ "$response" == "" ]]; then # Expecting 204 No Content (empty response) + echo -e " /boxes/{id} (DELETE): ${GREEN}PASS${NC}" +else + echo -e " /boxes/{id} (DELETE): ${RED}FAIL${NC} (Invalid response)" + echo "$response" +fi + +echo "Tests completed." diff --git a/tests.bash b/tests.bash new file mode 100755 index 0000000..53312ce --- /dev/null +++ b/tests.bash @@ -0,0 +1,176 @@ +#!/bin/bash + +# API base URL +API_BASE_URL="http://localhost:8080" + +# Login credentials +USERNAME="boxuser" +PASSWORD="boxuser" + +# Function to make an authenticated request +function authenticated_request() { + local method=$1 + local endpoint=$2 + local data=$3 + + # Get a new JWT token + TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ + "$API_BASE_URL/login" | jq -r '.token') + + # Make the authenticated request + curl -s -X $method -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "$API_BASE_URL$endpoint" +} + +# --- Test Cases --- + +# 1. Login +echo +echo +echo "Testing /login..." +response=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ + "$API_BASE_URL/login") + +if [[ $(echo "$response" | jq -r '.token') != "null" ]]; then + echo -e "\033[32m /login: PASS\033[0m" # Green PASS +else + echo -e "\033[31m /login: FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 2. Get Boxes +echo +echo +echo "Testing /boxes (GET)..." +response=$(authenticated_request "GET" "/boxes" "") + +if [[ $(echo "$response" | jq -r '. | length') -ge 0 ]]; then + echo -e "\033[32m /boxes (GET): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /boxes (GET): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 3. Create Box +echo +echo +echo "Testing /boxes (POST)..." +response=$(authenticated_request "POST" "/boxes" "{\"name\":\"Test Box\"}") +echo $response | jq '.' + +if [[ $(echo "$response" | jq -r '.name') == "Test Box" ]]; then + echo -e "\033[32m /boxes (POST): PASS\033[0m" # Green PASS + echo $response + BOX_ID=$(echo "$response" | jq -r '.id') # Extract and save the box ID + echo $BOX_ID | jq . +else + echo -e "\033[31m /boxes (POST): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 5. Create Item (Assuming a box with ID $BOX_ID exists) +echo +echo +echo "Testing /items (POST)..." +echo $BOX_ID +response=$(authenticated_request "POST" "/items" "{\"name\":\"Test Item\", \"description\":\"Test Description\", \"box_id\":$BOX_ID}") +if [[ $(echo "$response" | jq -r '.name') == "Test Item" ]]; then + echo -e "\033[32m /items (POST): PASS\033[0m" # Green PASS + ITEM_ID=$(echo "$response" | jq -r '.id') # Extract and save the item ID + echo $response +else + echo -e "\033[31m /items (POST): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 6. Get Items +echo +echo +echo "Testing /items (GET)..." +response=$(authenticated_request "GET" "/items" "") + +if [[ $(echo "$response" | jq -r '. | length') -ge 0 ]]; then + echo -e "\033[32m /items (GET): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /items (GET): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 7. Get Item by ID (Using the saved ITEM_ID) +echo +echo +echo "Testing /items/{id} (GET)..." +response=$(authenticated_request "GET" "/items/$ITEM_ID" "") +echo $response | jq . + +if [[ $(echo "$response" | jq -r '.ID') == "$ITEM_ID" ]]; then + echo -e "\033[32m /items/{id} (GET): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /items/{id} (GET): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 8. Get Items in Box (Using the saved BOX_ID) +echo +echo +echo "Testing /items/{id}/items (GET)..." +response=$(authenticated_request "GET" "/items/$BOX_ID/items" "") + +if [[ $(echo "$response" | jq -r '. | length') -ge 1 ]]; then # Expecting at least one item + echo -e "\033[32m /items/{id}/items (GET): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /items/{id}/items (GET): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 9. Update Item (Using the saved ITEM_ID) +echo +echo +echo "Testing /items/{id} (PUT)..." +response=$(authenticated_request "PUT" "/items/$ITEM_ID" "{\"name\":\"Updated Item\", \"description\":\"Updated Description\"}") + +if [[ $(echo "$response" | jq -r '.name') == "Updated Item" ]]; then + echo -e "\033[32m /items/{id} (PUT): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /items/{id} (PUT): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 10. Delete Item (Using the saved ITEM_ID) +echo +echo +echo "Testing /items/{id} (DELETE)..." +response=$(authenticated_request "DELETE" "/items/$ITEM_ID" "") + +if [[ "$response" == "" ]]; then # Expecting 204 No Content (empty response) + echo -e "\033[32m /items/{id} (DELETE): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /items/{id} (DELETE): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# 4. Delete Box (Using the saved BOX_ID) +echo +echo +echo "Testing /boxes/{id} (DELETE)..." +response=$(authenticated_request "DELETE" "/boxes/$BOX_ID" "") + +if [[ "$response" == "" ]]; then # Expecting 204 No Content (empty response) + echo -e "\033[32m /boxes/{id} (DELETE): PASS\033[0m" # Green PASS +else + echo -e "\033[31m /boxes/{id} (DELETE): FAIL (Invalid response)\033[0m" # Red FAIL + echo "$response" +fi + +# --- Add more test cases for other endpoints --- + +# Example for GET /items/{id} +# echo "Testing /items/{id} (GET)..." +# response=$(authenticated_request "GET" "/items/1" "") +# # ... (Add assertions based on the expected response) + +echo "Tests completed."