Initial Commit

This commit is contained in:
Steve White 2024-10-04 20:10:35 -05:00
commit 228e60f5b9
14 changed files with 1292 additions and 0 deletions

105
ApplicationDescription.md Normal file
View File

@ -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 doesnt 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"
```

22
Dockerfile Normal file
View File

@ -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"]

41
config.go Normal file
View File

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

6
config.yaml Normal file
View File

@ -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"

BIN
data/boxes.db Normal file

Binary file not shown.

BIN
data/my_test_database.db Normal file

Binary file not shown.

42
db.go Normal file
View File

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

19
go.mod Normal file
View File

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

48
go.sum Normal file
View File

@ -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=

254
handlers.go Normal file
View File

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

57
main.go Normal file
View File

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

348
main_test.go Normal file
View File

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

174
test2.bash Executable file
View File

@ -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."

176
tests.bash Executable file
View File

@ -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."