- InventoryNetBoxClient interface (ListDevices + GetDevice) for testability - ListInventory returns 200 JSON array (limit=200, 502 on NetBox error) - GetInventoryItem returns 200/404/400/502 based on ID validity and NetBox response - deviceToResponse maps netbox.Device to InventoryItemResponse (nil PhotoURLs → []) - 7 TDD tests: empty list, 2-item list, NetBox error, found, not found, invalid ID, server error
110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.georgsen.dk/hwlab/internal/netbox"
|
|
)
|
|
|
|
// InventoryNetBoxClient is the narrow interface the inventory handler needs.
|
|
// *netbox.Client satisfies this interface.
|
|
type InventoryNetBoxClient interface {
|
|
ListDevices(ctx context.Context, limit int) ([]netbox.Device, error)
|
|
GetDevice(ctx context.Context, id int) (*netbox.Device, error)
|
|
}
|
|
|
|
// InventoryHandler handles GET /api/inventory and GET /api/inventory/{id}.
|
|
type InventoryHandler struct {
|
|
nb InventoryNetBoxClient
|
|
}
|
|
|
|
// NewInventoryHandler creates an InventoryHandler.
|
|
func NewInventoryHandler(nb InventoryNetBoxClient) *InventoryHandler {
|
|
return &InventoryHandler{nb: nb}
|
|
}
|
|
|
|
// InventoryItemResponse is the JSON shape the frontend consumes.
|
|
type InventoryItemResponse struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
AssetTag string `json:"asset_tag"`
|
|
HWID string `json:"hw_id"`
|
|
CatalogStatus string `json:"catalog_status"`
|
|
ProductURL string `json:"product_url,omitempty"`
|
|
Firmware string `json:"firmware_version,omitempty"`
|
|
TestDate string `json:"test_date,omitempty"`
|
|
TestData string `json:"test_data,omitempty"`
|
|
AINotes string `json:"ai_notes,omitempty"`
|
|
PhotoURLs []string `json:"photo_urls"`
|
|
}
|
|
|
|
func deviceToResponse(d netbox.Device) InventoryItemResponse {
|
|
urls := d.CustomFields.PhotoURLs
|
|
if urls == nil {
|
|
urls = []string{}
|
|
}
|
|
return InventoryItemResponse{
|
|
ID: d.ID,
|
|
Name: d.Name,
|
|
AssetTag: d.AssetTag,
|
|
HWID: d.CustomFields.HWID,
|
|
CatalogStatus: d.CustomFields.CatalogStatus,
|
|
ProductURL: d.CustomFields.ProductURL,
|
|
Firmware: d.CustomFields.FirmwareVersion,
|
|
TestDate: d.CustomFields.TestDate,
|
|
TestData: d.CustomFields.TestData,
|
|
AINotes: d.CustomFields.AINotes,
|
|
PhotoURLs: urls,
|
|
}
|
|
}
|
|
|
|
// ListInventory handles GET /api/inventory — returns up to 200 items.
|
|
func (h *InventoryHandler) ListInventory(w http.ResponseWriter, r *http.Request) {
|
|
devices, err := h.nb.ListDevices(r.Context(), 200)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("netbox unavailable: %s", err)})
|
|
return
|
|
}
|
|
items := make([]InventoryItemResponse, 0, len(devices))
|
|
for _, d := range devices {
|
|
items = append(items, deviceToResponse(d))
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(items)
|
|
}
|
|
|
|
// GetInventoryItem handles GET /api/inventory/{id}.
|
|
func (h *InventoryHandler) GetInventoryItem(w http.ResponseWriter, r *http.Request) {
|
|
rawID := chi.URLParam(r, "id")
|
|
id, err := strconv.Atoi(rawID)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": "id must be an integer"})
|
|
return
|
|
}
|
|
|
|
device, err := h.nb.GetDevice(r.Context(), id)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if strings.Contains(err.Error(), "404") || strings.Contains(strings.ToLower(err.Error()), "not found") {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": "item not found"})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("netbox unavailable: %s", err)})
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(deviceToResponse(*device))
|
|
}
|