mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
Embed web resources in soundtouch-web (#182)
Relates to https://github.com/gesellix/Bose-SoundTouch/issues/181
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/cmd/soundtouch-web/webtypes"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
bmxpkg "github.com/gesellix/bose-soundtouch/pkg/service/bmx"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
@@ -61,7 +62,7 @@ func (app *WebApp) HandleAPIDevices(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
// HandleAPIDevice returns a specific device as JSON
|
||||
func (app *WebApp) HandleAPIDevice(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID := strings.TrimPrefix(r.URL.Path, "/api/device/")
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
if deviceID == "" {
|
||||
app.sendError(w, "Device ID required", http.StatusBadRequest)
|
||||
return
|
||||
@@ -98,18 +99,9 @@ func (app *WebApp) HandleAPIDevice(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// HandleAPIControl handles device control commands
|
||||
func (app *WebApp) HandleAPIControl(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/control/")
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
action := chi.URLParam(r, "action")
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 {
|
||||
app.sendError(w, "Invalid control path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := parts[0]
|
||||
action := parts[1]
|
||||
|
||||
// Check for empty device ID
|
||||
if deviceID == "" {
|
||||
app.sendError(w, "Device ID required", http.StatusBadRequest)
|
||||
return
|
||||
@@ -323,19 +315,8 @@ func (app *WebApp) sendError(w http.ResponseWriter, message string, statusCode i
|
||||
|
||||
// HandleDeviceKey handles sending key commands to devices
|
||||
func (app *WebApp) HandleDeviceKey(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
app.sendError(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 5 || pathParts[1] != "api" || pathParts[2] != "device-key" {
|
||||
app.sendError(w, "Invalid path format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := pathParts[3]
|
||||
key := pathParts[4]
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
key := chi.URLParam(r, "key")
|
||||
|
||||
device, exists := app.Devices[deviceID]
|
||||
if !exists {
|
||||
@@ -361,20 +342,9 @@ func (app *WebApp) HandleDeviceKey(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// HandleDirectVolumeControl handles direct volume setting via URL parameter
|
||||
func (app *WebApp) HandleDirectVolumeControl(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
app.sendError(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 5 || pathParts[1] != "api" || pathParts[2] != "device-volume" {
|
||||
app.sendError(w, "Invalid path format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := pathParts[3]
|
||||
|
||||
volumeLevel, err := strconv.Atoi(pathParts[4])
|
||||
volumeLevel, err := strconv.Atoi(chi.URLParam(r, "volume"))
|
||||
if err != nil || volumeLevel < 0 || volumeLevel > 100 {
|
||||
app.sendError(w, "Invalid volume level (0-100)", http.StatusBadRequest)
|
||||
return
|
||||
@@ -404,18 +374,7 @@ func (app *WebApp) HandleDirectVolumeControl(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// HandleDevicePower handles power toggle commands for devices
|
||||
func (app *WebApp) HandleDevicePower(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
app.sendError(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 4 || pathParts[1] != "api" || pathParts[2] != "device-power" {
|
||||
app.sendError(w, "Invalid path format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := pathParts[3]
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
|
||||
device, exists := app.Devices[deviceID]
|
||||
if !exists {
|
||||
@@ -442,18 +401,7 @@ func (app *WebApp) HandleDevicePower(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// HandleDevicePowerStatus handles lightweight power status check
|
||||
func (app *WebApp) HandleDevicePowerStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
app.sendError(w, "GET required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 4 || pathParts[1] != "api" || pathParts[2] != "device-power-status" {
|
||||
app.sendError(w, "Invalid path format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := pathParts[3]
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
|
||||
device, exists := app.Devices[deviceID]
|
||||
if !exists {
|
||||
@@ -587,14 +535,7 @@ func (app *WebApp) HandleTuneInSearch(w http.ResponseWriter, r *http.Request) {
|
||||
// - /sub/{n}/{encodedURI} → single subsection
|
||||
// - /profiles/{type}/{id}/{encodedURI} → artist/program profile
|
||||
func (app *WebApp) HandleTuneInNavigate(w http.ResponseWriter, r *http.Request) {
|
||||
const navPrefix = "/api/tunein/navigate"
|
||||
|
||||
path := r.URL.Path
|
||||
wildcard := ""
|
||||
|
||||
if len(path) > len(navPrefix) {
|
||||
wildcard = strings.TrimPrefix(path[len(navPrefix):], "/")
|
||||
}
|
||||
wildcard := chi.URLParam(r, "*")
|
||||
|
||||
var (
|
||||
resp interface{}
|
||||
@@ -651,7 +592,7 @@ func (app *WebApp) HandleTuneInNavigate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// HandlePlayTuneIn plays a TuneIn content item on a specific device via POST /select.
|
||||
func (app *WebApp) HandlePlayTuneIn(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID := strings.TrimPrefix(r.URL.Path, "/api/tunein/play/")
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
if deviceID == "" {
|
||||
app.sendError(w, "Device ID required", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/cmd/soundtouch-web/webtypes"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/client"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func createTestApp() *WebApp {
|
||||
@@ -42,6 +44,14 @@ func createTestApp() *WebApp {
|
||||
return app
|
||||
}
|
||||
|
||||
func withChiParams(r *http.Request, params map[string]string) *http.Request {
|
||||
rctx := chi.NewRouteContext()
|
||||
for k, v := range params {
|
||||
rctx.URLParams.Add(k, v)
|
||||
}
|
||||
return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||
}
|
||||
|
||||
func TestNewWebApp(t *testing.T) {
|
||||
app := NewWebApp()
|
||||
|
||||
@@ -102,24 +112,28 @@ func TestHandleAPIDevice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
chiID string
|
||||
expectedStatus int
|
||||
expectSuccess bool
|
||||
}{
|
||||
{
|
||||
name: "valid device",
|
||||
path: "/api/device/test-device",
|
||||
chiID: "test-device",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectSuccess: true,
|
||||
},
|
||||
{
|
||||
name: "missing device ID",
|
||||
path: "/api/device/",
|
||||
chiID: "",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectSuccess: false,
|
||||
},
|
||||
{
|
||||
name: "unknown device",
|
||||
path: "/api/device/unknown",
|
||||
chiID: "unknown",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectSuccess: false,
|
||||
},
|
||||
@@ -128,6 +142,9 @@ func TestHandleAPIDevice(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tt.path, nil)
|
||||
if tt.chiID != "" {
|
||||
req = withChiParams(req, map[string]string{"id": tt.chiID})
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.HandleAPIDevice(w, req)
|
||||
@@ -157,6 +174,7 @@ func TestHandleAPIControl_InvalidDevice(t *testing.T) {
|
||||
app := createTestApp()
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/control/unknown-device/play", nil)
|
||||
req = withChiParams(req, map[string]string{"id": "unknown-device", "action": "play"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.HandleAPIControl(w, req)
|
||||
@@ -261,6 +279,7 @@ func TestHandleAPIControl_VolumeValidation(t *testing.T) {
|
||||
} else {
|
||||
req = httptest.NewRequest(tt.method, "/api/control/test-device/volume", nil)
|
||||
}
|
||||
req = withChiParams(req, map[string]string{"id": "test-device", "action": "volume"})
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -311,6 +330,7 @@ func TestHandleAPIControl_BassValidation(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tt.method, "/api/control/test-device/bass", strings.NewReader(tt.body))
|
||||
req = withChiParams(req, map[string]string{"id": "test-device", "action": "bass"})
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -358,6 +378,7 @@ func TestHandleAPIControl_PresetValidation(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/control/test-device/preset"+tt.query, nil)
|
||||
req = withChiParams(req, map[string]string{"id": "test-device", "action": "preset"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.HandleAPIControl(w, req)
|
||||
@@ -382,6 +403,7 @@ func TestHandleAPIControl_SourceValidation(t *testing.T) {
|
||||
app := createTestApp()
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/control/test-device/source", nil)
|
||||
req = withChiParams(req, map[string]string{"id": "test-device", "action": "source"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.HandleAPIControl(w, req)
|
||||
@@ -497,6 +519,7 @@ func TestHandleAPIControl_UnsupportedAction(t *testing.T) {
|
||||
app := createTestApp()
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/control/test-device/unsupported", nil)
|
||||
req = withChiParams(req, map[string]string{"id": "test-device", "action": "unsupported"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.HandleAPIControl(w, req)
|
||||
@@ -545,6 +568,7 @@ func BenchmarkHandleAPIDevices(b *testing.B) {
|
||||
func BenchmarkHandleAPIDevice(b *testing.B) {
|
||||
app := createTestApp()
|
||||
req := httptest.NewRequest("GET", "/api/device/test-device", nil)
|
||||
req = withChiParams(req, map[string]string{"id": "test-device"})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/cmd/soundtouch-web/webtypes"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
@@ -224,13 +224,7 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
|
||||
|
||||
// HandleDeviceWebSocket handles individual device WebSocket connections for real-time device-specific updates
|
||||
func (app *WebApp) HandleDeviceWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 4 || pathParts[1] != "api" || pathParts[2] != "device-ws" {
|
||||
http.Error(w, "Invalid path format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
deviceID := pathParts[3]
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
if deviceID == "" {
|
||||
http.Error(w, "Device ID required", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
+43
-42
@@ -3,10 +3,11 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/cmd/soundtouch-web/handlers"
|
||||
@@ -14,8 +15,12 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/pkg/client"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/config"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/discovery"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
var staticFS embed.FS
|
||||
|
||||
var (
|
||||
port = flag.String("port", "8080", "Web server port")
|
||||
_ = flag.String("host", "", "Specific SoundTouch device host (optional)")
|
||||
@@ -56,29 +61,35 @@ func main() {
|
||||
}()
|
||||
|
||||
// Setup HTTP routes
|
||||
setupRoutes(app, discoveryService)
|
||||
r := setupRoutes(app, discoveryService)
|
||||
|
||||
// Start web server
|
||||
log.Printf("SoundTouch Web UI starting on http://localhost:%s", *port)
|
||||
log.Fatal(http.ListenAndServe(":"+*port, nil))
|
||||
log.Fatal(http.ListenAndServe(":"+*port, r))
|
||||
}
|
||||
|
||||
func setupRoutes(app *handlers.WebApp, discoveryService *discovery.UnifiedDiscoveryService) {
|
||||
// Static files - try both relative paths
|
||||
staticDir := "cmd/soundtouch-web/static/"
|
||||
if _, err := os.Stat(staticDir); os.IsNotExist(err) {
|
||||
staticDir = "static/"
|
||||
func setupRoutes(app *handlers.WebApp, discoveryService *discovery.UnifiedDiscoveryService) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Static assets (embedded in binary)
|
||||
subFS, _ := fs.Sub(staticFS, "static")
|
||||
r.Get("/static/*", http.StripPrefix("/static", http.FileServer(http.FS(subFS))).ServeHTTP)
|
||||
|
||||
// Serve index.html for SPA routes
|
||||
serveIndex := func(w http.ResponseWriter, _ *http.Request) {
|
||||
data, _ := staticFS.ReadFile("static/index.html")
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
|
||||
|
||||
// WebSocket endpoint
|
||||
http.HandleFunc("/ws", app.HandleWebSocket)
|
||||
r.Get("/ws", app.HandleWebSocket)
|
||||
|
||||
// API endpoints
|
||||
http.HandleFunc("/api/devices", app.HandleAPIDevices)
|
||||
http.HandleFunc("/api/device/", app.HandleAPIDevice)
|
||||
http.HandleFunc("/api/discover", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Get("/api/devices", app.HandleAPIDevices)
|
||||
r.Get("/api/device/{id}", app.HandleAPIDevice)
|
||||
r.Post("/api/discover", func(w http.ResponseWriter, r *http.Request) {
|
||||
app.HandleAPIDiscover(w, r)
|
||||
// Trigger discovery
|
||||
//nolint:contextcheck // Context is created within goroutine
|
||||
@@ -97,39 +108,29 @@ func setupRoutes(app *handlers.WebApp, discoveryService *discovery.UnifiedDiscov
|
||||
}()
|
||||
})
|
||||
|
||||
// Device control endpoints
|
||||
http.HandleFunc("/api/control/", app.HandleAPIControl)
|
||||
// Device control endpoints (GET for most actions, POST for volume/bass)
|
||||
r.Get("/api/control/{id}/{action}", app.HandleAPIControl)
|
||||
r.Post("/api/control/{id}/{action}", app.HandleAPIControl)
|
||||
|
||||
// TuneIn browse, search, and playback
|
||||
http.HandleFunc("/api/tunein/search", app.HandleTuneInSearch)
|
||||
http.HandleFunc("/api/tunein/navigate", app.HandleTuneInNavigate)
|
||||
http.HandleFunc("/api/tunein/navigate/", app.HandleTuneInNavigate)
|
||||
http.HandleFunc("/api/tunein/play/", app.HandlePlayTuneIn)
|
||||
r.Get("/api/tunein/search", app.HandleTuneInSearch)
|
||||
r.Get("/api/tunein/navigate", app.HandleTuneInNavigate)
|
||||
r.Get("/api/tunein/navigate/*", app.HandleTuneInNavigate)
|
||||
r.Post("/api/tunein/play/{id}", app.HandlePlayTuneIn)
|
||||
|
||||
// Enhanced device control endpoints with specific patterns
|
||||
http.HandleFunc("/api/device-key/", app.HandleDeviceKey)
|
||||
http.HandleFunc("/api/device-volume/", app.HandleDirectVolumeControl)
|
||||
http.HandleFunc("/api/device-power/", app.HandleDevicePower)
|
||||
http.HandleFunc("/api/device-power-status/", app.HandleDevicePowerStatus)
|
||||
http.HandleFunc("/api/device-ws/", app.HandleDeviceWebSocket)
|
||||
// Enhanced device control endpoints
|
||||
r.Post("/api/device-key/{id}/{key}", app.HandleDeviceKey)
|
||||
r.Post("/api/device-volume/{id}/{volume}", app.HandleDirectVolumeControl)
|
||||
r.Post("/api/device-power/{id}", app.HandleDevicePower)
|
||||
r.Get("/api/device-power-status/{id}", app.HandleDevicePowerStatus)
|
||||
r.Get("/api/device-ws/{id}", app.HandleDeviceWebSocket)
|
||||
|
||||
// SPA routes - serve index.html for specific routes only
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Serve the SPA index.html file for root path
|
||||
spaPath := staticDir + "index.html"
|
||||
http.ServeFile(w, r, spaPath)
|
||||
})
|
||||
// SPA routes - serve index.html for client-side routing
|
||||
r.Get("/", serveIndex)
|
||||
r.Get("/devices", serveIndex)
|
||||
r.Get("/device/*", serveIndex)
|
||||
|
||||
// Additional SPA routes for client-side routing
|
||||
http.HandleFunc("/devices", func(w http.ResponseWriter, r *http.Request) {
|
||||
spaPath := staticDir + "index.html"
|
||||
http.ServeFile(w, r, spaPath)
|
||||
})
|
||||
|
||||
http.HandleFunc("/device/", func(w http.ResponseWriter, r *http.Request) {
|
||||
spaPath := staticDir + "index.html"
|
||||
http.ServeFile(w, r, spaPath)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func discoverDevices(ctx context.Context, app *handlers.WebApp, discoveryService *discovery.UnifiedDiscoveryService) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -11,8 +12,17 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/cmd/soundtouch-web/handlers"
|
||||
"github.com/gesellix/bose-soundtouch/cmd/soundtouch-web/webtypes"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func withChiParams(r *http.Request, params map[string]string) *http.Request {
|
||||
rctx := chi.NewRouteContext()
|
||||
for k, v := range params {
|
||||
rctx.URLParams.Add(k, v)
|
||||
}
|
||||
return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||
}
|
||||
|
||||
func TestSPARouting(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -134,6 +144,8 @@ func TestAPIEndpoints(t *testing.T) {
|
||||
app.HandleAPIDiscover(w, req)
|
||||
default:
|
||||
if strings.HasPrefix(tt.path, "/api/device/") {
|
||||
deviceID := strings.TrimPrefix(tt.path, "/api/device/")
|
||||
req = withChiParams(req, map[string]string{"id": deviceID})
|
||||
app.HandleAPIDevice(w, req)
|
||||
}
|
||||
}
|
||||
@@ -200,6 +212,7 @@ func TestControlAPIValidation(t *testing.T) {
|
||||
method string
|
||||
body string
|
||||
expectedStatus int
|
||||
chiParams map[string]string
|
||||
}{
|
||||
{
|
||||
name: "missing device ID",
|
||||
@@ -218,18 +231,21 @@ func TestControlAPIValidation(t *testing.T) {
|
||||
path: "/api/control/nonexistent/invalid",
|
||||
method: "GET",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
chiParams: map[string]string{"id": "nonexistent", "action": "invalid"},
|
||||
},
|
||||
{
|
||||
name: "nonexistent device",
|
||||
path: "/api/control/nonexistent/play",
|
||||
method: "GET",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
chiParams: map[string]string{"id": "nonexistent", "action": "play"},
|
||||
},
|
||||
{
|
||||
name: "unknown action with valid device",
|
||||
path: "/api/control/testdevice/unknownaction",
|
||||
method: "GET",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
chiParams: map[string]string{"id": "testdevice", "action": "unknownaction"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -251,6 +267,9 @@ func TestControlAPIValidation(t *testing.T) {
|
||||
} else {
|
||||
req = httptest.NewRequest(tt.method, tt.path, nil)
|
||||
}
|
||||
if tt.chiParams != nil {
|
||||
req = withChiParams(req, tt.chiParams)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
app.HandleAPIControl(w, req)
|
||||
@@ -319,6 +338,8 @@ func TestJSONAPIConsistency(t *testing.T) {
|
||||
app.HandleAPIDevices(w, req)
|
||||
default:
|
||||
if strings.HasPrefix(endpoint, "/api/device/") {
|
||||
deviceID := strings.TrimPrefix(endpoint, "/api/device/")
|
||||
req = withChiParams(req, map[string]string{"id": deviceID})
|
||||
app.HandleAPIDevice(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user