added some individual scripts to test things

This commit is contained in:
Steve White 2024-10-08 17:34:49 -05:00
parent 7ced1b9e8d
commit f5a953763a
9 changed files with 194 additions and 354 deletions

View File

@ -1,30 +1,35 @@
# Application Overview: # 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 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. 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: ## Database Tables
- `boxes`: A table containing an ID and a name. - `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. - `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. - `users`: A table containing usernames and passwords (hashed) for authentication.
# API Endpoints: ## API Endpoints
1. Authentication: 1. Authentication:
- POST `/login`: Authenticates a user and returns a JWT. - POST `/login`: Authenticates a user and returns a JWT.
2. Boxes: 2. Boxes:
- GET `/boxes`: Retrieves all boxes. - GET `/boxes`: Retrieves all boxes.
- POST `/boxes`: Creates a new box. - POST `/boxes`: Creates a new box.
- DELETE :`/boxes/{id}`: Deletes a box by its ID. - DELETE :`/boxes/{id}`: Deletes a box by its ID.
- GET `/boxes/{id}/items`: Retrieves all items in box with this id.
3. Items: 3. Items:
- GET `/items`: Retrieves all items, optionally searchable by description. - GET `/items`: Retrieves all items, optionally searchable by description.
- POST `/items`: Adds a new item to a box. - POST `/items`: Adds a new item to a box.
- GET `/items/{id}`: Retrieves an item by its ID. - GET `/items/{id}`: Retrieves an item by its ID.
- PUT `/items/{id}`: Updates an existing item. - 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. - DELETE `/items/{id}`: Deletes an item by its ID.
- GET `/items/{id}/image`: Retrieves the image of an item. - GET `/items/{id}/image`: Retrieves the image of an item.
# Additional Details: ## Additional Details
- If the database doesnt exist, it should be created automatically when the app starts. - 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. - 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'. - The default user for the app should be 'boxuser' with a password of 'boxuser'.

81
README.md Normal file
View File

@ -0,0 +1,81 @@
# Boxes API
## Overview
This is a back-end application written in Go that provides an API for managing boxes and items stored within those boxes. It uses SQLite3 as its database and supports JWT-based authentication.
## Features
- **Box Management:** Create, retrieve, and delete boxes.
- **Item Management:** Add, retrieve, update, and delete items within boxes.
- **Image Storage:** Store images associated with items.
- **JWT Authentication:** Secure API access with JSON Web Tokens.
- **Configuration:** Manage settings via a `config.yaml` file.
- **Logging:** Logs user logins and box/item actions to a file.
## API Documentation
See the [API Specification](api_specification.md) for detailed information on endpoints, request/response formats, and authentication.
## Getting Started
1. **Clone the repository:**
```bash
git clone https://github.com/your-username/boxes-api.git
```
2. Configuration:
- Create a config.yaml file in the project root directory.
- Refer to the config.yaml.example file for available settings.
3. Database Setup:
- The database will be automatically created if it doesn't exist.
- Configure the database path in the config.yaml file.
4. Running the Application:
- Build and run the Go application:
```bash
go run boxes-api .
```
5. Run the test script:
``` bash
./tests.bash
```
## Podman Support
Build the podman image:
```bash
podman build -t boxes-api .
```
Run the podman container:
```bash
podman run \
-e CONFIG="/app/config/config.yaml" \
-v /Users/stwhite/CODE/boxes/data:/app/data \
-v /Users/stwhite/CODE/boxes/images:/app/images \
-v /Users/stwhite/CODE/boxes/config:/app/config \
-p 8080:8080 \
--name boxes-api-container \
localhost/boxes-api
```

29
addItemToBox.bash Normal file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# API base URL
API_BASE_URL="http://localhost:8080" # Replace with your actual API base URL
# Box ID
BOX_ID=96 # Replace with the actual box ID
# Login credentials
USERNAME="boxuser" # Replace with your actual username
PASSWORD="boxuser" # Replace with your actual password
# Item data
ITEM_NAME="New Item"
ITEM_DESCRIPTION="This is a new item"
# 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')
# Create the item data in JSON format
ITEM_DATA=$(echo "{\"name\":\"$ITEM_NAME\",\"description\":\"$ITEM_DESCRIPTION\",\"box_id\":$BOX_ID}" | jq -r '.')
# Add the item to the box using the obtained token
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$ITEM_DATA" \
"$API_BASE_URL/items"

28
deleteItemsInBox.bash Normal file
View File

@ -0,0 +1,28 @@
#!/bin/bash
# API base URL
API_BASE_URL="http://localhost:8080" # Replace with your actual API base URL
# Box ID to delete items from
BOX_ID=0 # Replace with the actual box ID
# Login credentials
USERNAME="boxuser" # Replace with your actual username
PASSWORD="boxuser" # Replace with your actual password
# 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')
# Get a list of all item IDs associated with the specified box ID
ITEM_IDS=$(curl -s -X GET -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE_URL/boxes/$BOX_ID/items" | jq -r '.[].ID')
# Loop through each item ID and send a DELETE request
for ITEM_ID in $ITEM_IDS; do
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE_URL/items/$ITEM_ID"
done

21
getitems.bash Normal file
View File

@ -0,0 +1,21 @@
#!/bin/bash
# API base URL
API_BASE_URL="http://localhost:8080"
# Login credentials
USERNAME="boxuser"
PASSWORD="boxuser"
# 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')
# Request all items using the obtained token
response=$(curl -s -X GET -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE_URL/items")
# Print the response
echo "$response"

21
getitemsinbox.bash Normal file
View File

@ -0,0 +1,21 @@
#!/bin/bash
# API base URL
API_BASE_URL="http://localhost:8080" # Replace with your actual API base URL
# Box ID
BOX_ID=0 # Replace with the actual box ID
# Login credentials
USERNAME="boxuser" # Replace with your actual username
PASSWORD="boxuser" # Replace with your actual password
# 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')
# Request items in the specified box using the obtained token
curl -s -X GET -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE_URL/boxes/$BOX_ID/items"

View File

@ -68,6 +68,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
// getBoxesHandler handles the GET /boxes endpoint. // getBoxesHandler handles the GET /boxes endpoint.
func GetBoxesHandler(w http.ResponseWriter, r *http.Request) { func GetBoxesHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Received %s request to %s\n", r.Method, r.URL)
var boxes []Box var boxes []Box
db.Find(&boxes) db.Find(&boxes)
json.NewEncoder(w).Encode(boxes) json.NewEncoder(w).Encode(boxes)
@ -129,11 +130,13 @@ func GetItemsHandler(w http.ResponseWriter, r *http.Request) {
// createItemHandler handles the POST /items endpoint. // createItemHandler handles the POST /items endpoint.
func CreateItemHandler(w http.ResponseWriter, r *http.Request) { func CreateItemHandler(w http.ResponseWriter, r *http.Request) {
var item Item var item Item
err := json.NewDecoder(r.Body).Decode(&item) err := json.NewDecoder(r.Body).Decode(&item)
if err != nil { if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) http.Error(w, "Invalid request body", http.StatusBadRequest)
return return
} }
fmt.Println(item)
db.Create(&item) db.Create(&item)
@ -234,7 +237,7 @@ func GetItemHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(item) json.NewEncoder(w).Encode(item)
} }
// getItemsInBoxHandler handles the GET /items/{id}/items endpoint. // getItemsInBoxHandler handles the GET /boxes/{id}/items endpoint.
func GetItemsInBoxHandler(w http.ResponseWriter, r *http.Request) { func GetItemsInBoxHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]

View File

@ -54,7 +54,7 @@ func main() {
router.Handle("/items", AuthMiddleware(http.HandlerFunc(GetItemsHandler))).Methods("GET", "OPTIONS") router.Handle("/items", AuthMiddleware(http.HandlerFunc(GetItemsHandler))).Methods("GET", "OPTIONS")
router.Handle("/items", AuthMiddleware(http.HandlerFunc(CreateItemHandler))).Methods("POST", "OPTIONS") router.Handle("/items", AuthMiddleware(http.HandlerFunc(CreateItemHandler))).Methods("POST", "OPTIONS")
router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(GetItemHandler))).Methods("GET", "OPTIONS") router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(GetItemHandler))).Methods("GET", "OPTIONS")
router.Handle("/items/{id}/items", AuthMiddleware(http.HandlerFunc(GetItemsInBoxHandler))).Methods("GET", "OPTIONS") router.Handle("/boxes/{id}/items", AuthMiddleware(http.HandlerFunc(GetItemsInBoxHandler))).Methods("GET", "OPTIONS")
router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(UpdateItemHandler))).Methods("PUT", "OPTIONS") router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(UpdateItemHandler))).Methods("PUT", "OPTIONS")
router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(DeleteItemHandler))).Methods("DELETE", "OPTIONS") router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(DeleteItemHandler))).Methods("DELETE", "OPTIONS")
// Add a new route for uploading an image with AuthMiddleware // Add a new route for uploading an image with AuthMiddleware

View File

@ -1,348 +0,0 @@
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)
}
}