commit 228e60f5b9eb6b6ab9705ec13d628521950ca2a1 Author: Steve White Date: Fri Oct 4 20:10:35 2024 -0500 Initial Commit 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 0000000..b93a314 Binary files /dev/null and b/data/boxes.db differ diff --git a/data/my_test_database.db b/data/my_test_database.db new file mode 100644 index 0000000..14975b3 Binary files /dev/null and b/data/my_test_database.db differ 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."