mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +00:00
177 lines
5.5 KiB
Go
177 lines
5.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/spotify"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
func TestSpotifyBridge(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
ds := datastore.NewDataStore(tmpDir)
|
|
server := NewServer(ds, nil, "http://localhost", false, false, false)
|
|
|
|
// Mock Speaker (LISA API)
|
|
var speakerReceived atomic.Bool
|
|
speakerTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/setMusicServiceOAuthAccount" {
|
|
speakerReceived.Store(true)
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8" ?><status>/setMusicServiceOAuthAccount</status>`))
|
|
}
|
|
}))
|
|
defer speakerTS.Close()
|
|
|
|
// Register the speaker in the datastore so the bridge finds it
|
|
devInfo := &models.ServiceDeviceInfo{
|
|
DeviceID: "DEV123",
|
|
AccountID: "acc123",
|
|
Name: "Test Speaker",
|
|
IPAddress: strings.TrimPrefix(speakerTS.URL, "http://"),
|
|
}
|
|
_ = ds.SaveDeviceInfo("acc123", "DEV123", devInfo)
|
|
|
|
// Ensure the directory structure exists for marge.AddSource
|
|
_ = os.MkdirAll(ds.AccountDevicesDir("acc123"), 0755)
|
|
_ = os.MkdirAll(filepath.Join(ds.AccountDevicesDir("acc123"), "DEV123"), 0755)
|
|
|
|
// Mock Spotify response
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/token":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"access_token": "access-123",
|
|
"refresh_token": "refresh-123",
|
|
"expires_in": 3600,
|
|
})
|
|
case "/me":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": "spotify-user",
|
|
"display_name": "Spotify User",
|
|
"email": "user@example.com",
|
|
})
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
// Initialize Spotify service
|
|
ss := spotify.NewSpotifyService("client-id", "client-secret", "http://localhost/callback", tmpDir)
|
|
ss.SetEndpoints(ts.URL+"/token", ts.URL)
|
|
server.SetSpotifyService(ss)
|
|
|
|
r := chi.NewRouter()
|
|
r.Get("/mgmt/spotify/callback", server.HandleMgmtSpotifyCallback)
|
|
|
|
// Trigger the callback
|
|
req := httptest.NewRequest("GET", "/mgmt/spotify/callback?code=fake-code&account=acc123", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Expected 200 OK, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// 1. Verify Marge registration
|
|
// We need to check if the source was added to the datastore
|
|
foundInMarge := false
|
|
sources, err := ds.GetConfiguredSources("acc123", "DEV123")
|
|
if err == nil {
|
|
for _, src := range sources {
|
|
t.Logf(" Found source: %s (User: %s)", src.SourceKey.Type, src.Username)
|
|
if (src.Username == "spotify-user" || src.SourceKey.Account == "spotify-user") &&
|
|
(src.SourceProviderID == "15" || src.SourceKey.Type == "SPOTIFY") {
|
|
foundInMarge = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !foundInMarge {
|
|
// Log what we found to debug
|
|
allDevices, _ := ds.ListAllDevices()
|
|
t.Logf("Total devices in datastore: %d", len(allDevices))
|
|
for _, d := range allDevices {
|
|
t.Logf("Device: %s (Account: %s)", d.DeviceID, d.AccountID)
|
|
s, _ := ds.GetConfiguredSources(d.AccountID, d.DeviceID)
|
|
t.Logf(" Sources: %d", len(s))
|
|
}
|
|
t.Errorf("Spotify user not found in Marge configured sources")
|
|
}
|
|
|
|
// 2. Verify Speaker notification (LISA API)
|
|
// Using time.Sleep for simplicity in this test
|
|
// Wait up to 1 second
|
|
deadline := time.Now().Add(1 * time.Second)
|
|
for time.Now().Before(deadline) && !speakerReceived.Load() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
if !speakerReceived.Load() {
|
|
t.Errorf("Speaker did not receive /setMusicServiceOAuthAccount notification")
|
|
}
|
|
|
|
// 3. Verify Token Refresh via Surrogate
|
|
// Now simulate the speaker asking for a fresh token using the surrogate secret it received.
|
|
// We need to find the surrogate first.
|
|
sources, _ = ds.GetConfiguredSources("acc123", "DEV123")
|
|
var surrogate string
|
|
for _, src := range sources {
|
|
if src.SourceKey.Type == "SPOTIFY" {
|
|
surrogate = src.Secret
|
|
break
|
|
}
|
|
}
|
|
|
|
if surrogate == "" {
|
|
t.Fatal("Could not find surrogate token in Marge sources")
|
|
}
|
|
|
|
if !strings.HasPrefix(surrogate, "bs-") || len(surrogate) != 35 {
|
|
t.Errorf("Expected surrogate to have 'bs-' prefix and be 35 chars, got %s", surrogate)
|
|
}
|
|
|
|
// Request refresh
|
|
refreshReqBody := map[string]string{
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": surrogate,
|
|
}
|
|
body, err := json.Marshal(refreshReqBody)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal refresh request: %v", err)
|
|
}
|
|
|
|
refreshReq := httptest.NewRequest("POST", "/oauth/device/DEV123/music/musicprovider/15/token/cs3", strings.NewReader(string(body)))
|
|
refreshW := httptest.NewRecorder()
|
|
|
|
// Need to register the route for testing
|
|
r.Post("/oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs3", server.HandleBoseToken)
|
|
r.ServeHTTP(refreshW, refreshReq)
|
|
|
|
if refreshW.Code != http.StatusOK {
|
|
t.Fatalf("Token refresh failed: %d: %s", refreshW.Code, refreshW.Body.String())
|
|
}
|
|
|
|
var refreshResp map[string]interface{}
|
|
if err := json.Unmarshal(refreshW.Body.Bytes(), &refreshResp); err != nil {
|
|
t.Fatalf("Failed to parse refresh response: %v", err)
|
|
}
|
|
|
|
if refreshResp["access_token"] != "access-123" {
|
|
t.Errorf("Expected access_token 'access-123', got '%v'", refreshResp["access_token"])
|
|
}
|
|
}
|