209 lines
5.4 KiB
Go
209 lines
5.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.georgsen.dk/hwlab/internal/netbox"
|
|
"git.georgsen.dk/hwlab/internal/usb"
|
|
)
|
|
|
|
// --- mock NetBox client for label tests ---
|
|
|
|
type mockLabelNetBox struct {
|
|
device *netbox.Device
|
|
err error
|
|
}
|
|
|
|
func (m *mockLabelNetBox) GetDevice(_ context.Context, id int) (*netbox.Device, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return m.device, nil
|
|
}
|
|
|
|
// --- mock printer ---
|
|
|
|
type mockPrinter struct {
|
|
called int
|
|
returnErr error
|
|
}
|
|
|
|
func (m *mockPrinter) Print(bitmap []byte, width, height int) error {
|
|
m.called++
|
|
return m.returnErr
|
|
}
|
|
|
|
// --- helper: build chi request with deviceID param ---
|
|
|
|
func labelRequest(deviceID string) *http.Request {
|
|
r := httptest.NewRequest(http.MethodPost, "/api/labels/"+deviceID+"/print", nil)
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("deviceID", deviceID)
|
|
return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
|
}
|
|
|
|
// Test 1: valid device — Print() called once, 200 {"status":"printed"}
|
|
func TestLabelPrintSuccess(t *testing.T) {
|
|
dev := &netbox.Device{
|
|
ID: 42,
|
|
Name: "Test Switch",
|
|
CustomFields: netbox.CustomFields{
|
|
HWID: "HW-00042",
|
|
},
|
|
}
|
|
nb := &mockLabelNetBox{device: dev}
|
|
p := &mockPrinter{}
|
|
h := NewLabelHandler(nb, p)
|
|
|
|
w := httptest.NewRecorder()
|
|
h.PrintLabel(w, labelRequest("42"))
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp["status"] != "printed" {
|
|
t.Errorf("expected status=printed, got %q", resp["status"])
|
|
}
|
|
if p.called != 1 {
|
|
t.Errorf("expected Print called once, got %d", p.called)
|
|
}
|
|
}
|
|
|
|
// Test 2: device not found → 404
|
|
func TestLabelPrintDeviceNotFound(t *testing.T) {
|
|
nb := &mockLabelNetBox{err: errors.New("404 not found")}
|
|
p := &mockPrinter{}
|
|
h := NewLabelHandler(nb, p)
|
|
|
|
w := httptest.NewRecorder()
|
|
h.PrintLabel(w, labelRequest("99"))
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Test 3: printer returns error → 500 with {"error":"..."}
|
|
func TestLabelPrintPrinterError(t *testing.T) {
|
|
dev := &netbox.Device{
|
|
ID: 42,
|
|
Name: "Test Switch",
|
|
CustomFields: netbox.CustomFields{
|
|
HWID: "HW-00042",
|
|
},
|
|
}
|
|
nb := &mockLabelNetBox{device: dev}
|
|
p := &mockPrinter{returnErr: errors.New("ink jam")}
|
|
h := NewLabelHandler(nb, p)
|
|
|
|
w := httptest.NewRecorder()
|
|
h.PrintLabel(w, labelRequest("42"))
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "error") {
|
|
t.Errorf("expected error field in response, got %q", body)
|
|
}
|
|
}
|
|
|
|
// --- USB events tests ---
|
|
|
|
// mockUSBEventSource is a buffered channel that implements USBEventSource.
|
|
type mockUSBEventSource struct {
|
|
ch chan usb.DeviceEvent
|
|
}
|
|
|
|
func newMockUSBEventSource(bufSize int) *mockUSBEventSource {
|
|
return &mockUSBEventSource{ch: make(chan usb.DeviceEvent, bufSize)}
|
|
}
|
|
|
|
func (m *mockUSBEventSource) Events() <-chan usb.DeviceEvent {
|
|
return m.ch
|
|
}
|
|
|
|
// Test 4: GET /api/usb/events sets Content-Type: text/event-stream and
|
|
// writes "data: {...}\n\n" on DeviceEvent receive.
|
|
func TestUSBEventsSSEContentType(t *testing.T) {
|
|
src := newMockUSBEventSource(1)
|
|
h := NewUSBEventsHandler(src)
|
|
|
|
// Send one event, then cancel context.
|
|
evt := usb.DeviceEvent{
|
|
VIDPID: "0525:a4a7",
|
|
Spec: usb.DeviceSpec{VID: "0525", PID: "a4a7", Name: "PRT Qutie", Role: usb.RolePrinter},
|
|
State: usb.StateConnected,
|
|
}
|
|
src.ch <- evt
|
|
|
|
// Use a context with timeout so the handler exits.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/usb/events", nil).WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
h.ServeEvents(w, req)
|
|
|
|
if ct := w.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") {
|
|
t.Errorf("expected Content-Type text/event-stream, got %q", ct)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "data:") {
|
|
t.Errorf("expected SSE data line in body, got: %q", body)
|
|
}
|
|
// Verify JSON contains the VIDPID field.
|
|
if !strings.Contains(body, "0525:a4a7") {
|
|
t.Errorf("expected VIDPID in SSE data, got: %q", body)
|
|
}
|
|
}
|
|
|
|
// Test 5: GET /api/usb/events closes cleanly when client disconnects
|
|
// (r.Context() cancels). No goroutine leak.
|
|
func TestUSBEventsNoGoroutineLeak(t *testing.T) {
|
|
src := newMockUSBEventSource(0) // unbuffered — no events
|
|
h := NewUSBEventsHandler(src)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
req := httptest.NewRequest(http.MethodGet, "/api/usb/events", nil).WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
h.ServeEvents(w, req)
|
|
close(done)
|
|
}()
|
|
|
|
// Cancel after brief delay to simulate client disconnect.
|
|
time.Sleep(10 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
// Handler returned — no goroutine leak.
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("handler did not return after context cancellation (goroutine leak)")
|
|
}
|
|
|
|
// Verify SSE headers were set before the disconnect.
|
|
if ct := w.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") {
|
|
t.Errorf("expected Content-Type text/event-stream, got %q", ct)
|
|
}
|
|
|
|
_ = fmt.Sprintf("body: %s", w.Body.String()) // consume body, no assertion needed
|
|
}
|