diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index 63574c5..735803f 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -661,46 +661,58 @@ func setupRouter(server *handlers.Server) *chi.Mux { r.Route("/bmx", func(r chi.Router) { r.Get("/registry/v1/services", server.HandleBMXRegistry) - r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback) - r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo) - r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast) + + r.Route("/tunein", func(r chi.Router) { + r.Get("/v1/playback/station/{stationID}", server.HandleTuneInPlayback) + r.Get("/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo) + r.Get("/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast) + }) + r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback) }) - // Legacy or direct domain calls without /bmx prefix - r.Get("/registry/v1/services", server.HandleBMXRegistry) - r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback) - r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo) - r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast) - r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback) r.Get("/custom/v1/playback/{encodedURL}", server.HandleCustomPlayback) r.Route("/streaming", func(r chi.Router) { r.Get("/sourceproviders", server.HandleMargeSourceProviders) r.Post("/account", server.HandleMargeCreateAccount) r.Post("/account/login", server.HandleMargeLogin) - r.Route("/account/{account}/device", func(r chi.Router) { - r.Post("/", server.HandleMargeAddDevice) - r.Post("/{device}", server.HandleMargeAddDevice) + + r.Route("/account/{account}", func(r chi.Router) { + r.Get("/emailaddress", server.HandleMargeGetEmailAddress) + r.Get("/full", server.HandleMargeAccountFull) + r.Get("/provider_settings", server.HandleMargeProviderSettings) + + r.Route("/device", func(r chi.Router) { + r.Post("/", server.HandleMargeAddDevice) + r.Post("/{device}", server.HandleMargeAddDevice) + }) + + r.Route("/device/{device}", func(r chi.Router) { + r.Get("/presets", server.HandleMargePresets) + r.Post("/presets/{presetNumber}", server.HandleMargeUpdatePreset) + r.Get("/recent", server.HandleMargeRecents) + r.Post("/recent", server.HandleMargeAddRecent) + + r.Get("/group", server.HandleMargeDeviceGroup) + r.Get("/group/", server.HandleMargeDeviceGroup) + r.Get("/group/server", server.HandleMargeDeviceGroupServer) + r.Get("/group/member", server.HandleMargeDeviceGroupMember) + }) }) - r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents) - r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent) - r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets) - r.Post("/account/{account}/device/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset) - r.Post("/support/power_on", server.HandleMargePowerOn) - r.Get("/account/{account}/provider_settings", server.HandleMargeProviderSettings) + r.Get("/device/{device}/streaming_token", server.HandleMargeStreamingToken) - r.Post("/support/customersupport", server.HandleMargeCustomerSupport) + r.Get("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings) - r.Get("/account/{account}/device/{device}/group", server.HandleMargeDeviceGroup) - r.Get("/account/{account}/device/{device}/group/", server.HandleMargeDeviceGroup) - r.Get("/account/{account}/device/{device}/group/server", server.HandleMargeDeviceGroupServer) - r.Get("/account/{account}/device/{device}/group/member", server.HandleMargeDeviceGroupMember) r.Post("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings) - r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress) - r.Get("/account/{account}/full", server.HandleMargeAccountFull) + r.Get("/software/update/account/{account}", server.HandleMargeSoftwareUpdate) + r.Route("/support", func(r chi.Router) { + r.Post("/power_on", server.HandleMargePowerOn) + r.Post("/customersupport", server.HandleMargeCustomerSupport) + }) + r.Route("/stats", func(r chi.Router) { r.Post("/usage", server.HandleUsageStats) r.Post("/error", server.HandleErrorStats) @@ -708,17 +720,22 @@ func setupRouter(server *handlers.Server) *chi.Mux { }) r.Route("/accounts", func(r chi.Router) { - r.Get("/{account}/full", server.HandleMargeAccountFull) - r.Get("/{account}/devices/{device}/presets", server.HandleMargePresets) - r.Post("/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset) - r.Get("/{account}/devices/{device}/recents", server.HandleMargeRecents) - r.Post("/{account}/devices/{device}/recents", server.HandleMargeAddRecent) - r.Post("/{account}/devices", server.HandleMargeAddDevice) - r.Delete("/{account}/devices/{device}", server.HandleMargeRemoveDevice) - r.Get("/{account}/devices/{device}/group", server.HandleMargeDeviceGroup) - r.Get("/{account}/devices/{device}/group/", server.HandleMargeDeviceGroup) - r.Get("/{account}/devices/{device}/group/server", server.HandleMargeDeviceGroupServer) - r.Get("/{account}/devices/{device}/group/member", server.HandleMargeDeviceGroupMember) + r.Route("/{account}", func(r chi.Router) { + r.Get("/full", server.HandleMargeAccountFull) + + r.Post("/devices", server.HandleMargeAddDevice) + + r.Delete("/devices/{device}", server.HandleMargeRemoveDevice) + r.Get("/devices/{device}/group", server.HandleMargeDeviceGroup) + r.Get("/devices/{device}/group/", server.HandleMargeDeviceGroup) + r.Get("/devices/{device}/group/server", server.HandleMargeDeviceGroupServer) + r.Get("/devices/{device}/group/member", server.HandleMargeDeviceGroupMember) + r.Get("/devices/{device}/presets", server.HandleMargePresets) + r.Get("/devices/{device}/recents", server.HandleMargeRecents) + + r.Post("/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset) + r.Post("/devices/{device}/recents", server.HandleMargeAddRecent) + }) }) r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate) diff --git a/cmd/soundtouch-service/router_test.go b/cmd/soundtouch-service/router_test.go new file mode 100644 index 0000000..d11db27 --- /dev/null +++ b/cmd/soundtouch-service/router_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "reflect" + "runtime" + "sort" + "strings" + "testing" + + "github.com/gesellix/bose-soundtouch/pkg/service/handlers" + "github.com/go-chi/chi/v5" +) + +func TestPrintRoutes(t *testing.T) { + // Initialize a minimal server to get the router + server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true) + r := setupRouter(server) + + var routes []string + walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { + route = strings.ReplaceAll(route, "/*/", "/") + handlerName := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() + // Clean up the handler name (remove package path) + // For example, "github.com/gesellix/bose-soundtouch/cmd/soundtouch-service.setupRouter.func1" + // or "command-line-arguments.setupRouter.func1" + // or "main.setupRouter.func1" + parts := strings.Split(handlerName, "/") + if len(parts) > 0 { + handlerName = parts[len(parts)-1] + } + // Now we might have "soundtouch-service.setupRouter.func1" + // or "command-line-arguments.setupRouter.func1" + // or "main.setupRouter.func1" + // Let's remove the first part if it's a known varying package name + if idx := strings.Index(handlerName, "setupRouter"); idx != -1 { + handlerName = handlerName[idx:] + } + // In case it's not setupRouter but still has a package prefix + for { + dotIdx := strings.Index(handlerName, ".") + if dotIdx == -1 { + break + } + prefix := handlerName[:dotIdx] + if prefix == "main" || prefix == "command-line-arguments" || strings.Contains(prefix, "soundtouch-service") { + handlerName = handlerName[dotIdx+1:] + } else { + break + } + } + + // Also remove any ".funcN" suffix if it's an anonymous function + if idx := strings.Index(handlerName, ".func"); idx != -1 { + handlerName = handlerName[:idx] + } + + routes = append(routes, fmt.Sprintf("%-8s %-60s %s", method, route, handlerName)) + return nil + } + + if err := chi.Walk(r, walkFunc); err != nil { + t.Fatalf("Failed to walk routes: %v", err) + } + + sort.Strings(routes) + + output := strings.Join(routes, "\n") + "\n" + + // Define snapshot path + snapshotPath := "testdata/router_routes.txt" + actualPath := "testdata/router_routes.actual.txt" + + // Always write the current (actual) routes to a file + if err := os.WriteFile(actualPath, []byte(output), 0644); err != nil { + t.Fatalf("Failed to write actual routes: %v", err) + } + + // Check if snapshot exists + if _, err := os.Stat(snapshotPath); os.IsNotExist(err) { + // Create testdata directory if it doesn't exist + if err := os.MkdirAll("testdata", 0755); err != nil { + t.Fatalf("Failed to create testdata directory: %v", err) + } + // Initial snapshot creation + if err := os.WriteFile(snapshotPath, []byte(output), 0644); err != nil { + t.Fatalf("Failed to write snapshot: %v", err) + } + t.Logf("Initial snapshot created at %s", snapshotPath) + return + } + + // Read existing snapshot + existingOutput, err := os.ReadFile(snapshotPath) + if err != nil { + t.Fatalf("Failed to read snapshot: %v", err) + } + + if string(existingOutput) != output { + t.Errorf("Router routes changed! Diff the snapshot at %s with %s", snapshotPath, actualPath) + } +} diff --git a/cmd/soundtouch-service/testdata/.gitignore b/cmd/soundtouch-service/testdata/.gitignore new file mode 100644 index 0000000..bdc49b1 --- /dev/null +++ b/cmd/soundtouch-service/testdata/.gitignore @@ -0,0 +1 @@ +*.actual.txt diff --git a/cmd/soundtouch-service/testdata/router_routes.txt b/cmd/soundtouch-service/testdata/router_routes.txt new file mode 100644 index 0000000..4f27e27 --- /dev/null +++ b/cmd/soundtouch-service/testdata/router_routes.txt @@ -0,0 +1,114 @@ +CONNECT /oauth/* handlers.(*Server).HandleBoseProxy-fm +DELETE /accounts/{account}/devices/{device} handlers.(*Server).HandleMargeRemoveDevice-fm +DELETE /oauth/* handlers.(*Server).HandleBoseProxy-fm +DELETE /setup/devices/{deviceId} handlers.(*Server).HandleRemoveDevice-fm +DELETE /setup/dns-discoveries handlers.(*Server).HandleClearDNSDiscoveries-fm +DELETE /setup/interactions/sessions handlers.(*Server).HandleCleanupSessions-fm +DELETE /setup/interactions/sessions/{session} handlers.(*Server).HandleDeleteSession-fm +DELETE /setup/parity-mismatches handlers.(*Server).HandleClearParityMismatches-fm +GET / handlers.(*Server).HandleRoot-fm +GET /accounts/{account}/devices/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm +GET /accounts/{account}/devices/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm +GET /accounts/{account}/devices/{device}/group/member handlers.(*Server).HandleMargeDeviceGroupMember-fm +GET /accounts/{account}/devices/{device}/group/server handlers.(*Server).HandleMargeDeviceGroupServer-fm +GET /accounts/{account}/devices/{device}/presets handlers.(*Server).HandleMargePresets-fm +GET /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleMargeRecents-fm +GET /accounts/{account}/full handlers.(*Server).HandleMargeAccountFull-fm +GET /bmx/registry/v1/services handlers.(*Server).HandleBMXRegistry-fm +GET /bmx/tunein/v1/playback/episode/{podcastID} handlers.(*Server).HandleTuneInPlaybackPodcast-fm +GET /bmx/tunein/v1/playback/episodes/{podcastID} handlers.(*Server).HandleTuneInPodcastInfo-fm +GET /bmx/tunein/v1/playback/station/{stationID} handlers.(*Server).HandleTuneInPlayback-fm +GET /custom/v1/playback/{encodedURL} handlers.(*Server).HandleCustomPlayback-fm +GET /customer/account/{account} handlers.(*Server).HandleMargeAccountProfile-fm +GET /docs/* handlers.(*Server).HandleDocs-fm +GET /favicon.ico setupRouter +GET /health handlers.(*Server).HandleHealth-fm +GET /media/* handlers.(*Server).HandleMedia +GET /mgmt/accounts/ handlers.(*Server).HandleMgmtListAccounts-fm +GET /mgmt/accounts/{accountId} handlers.(*Server).HandleMgmtAccountDetails-fm +GET /mgmt/accounts/{accountId}/speakers handlers.(*Server).HandleMgmtListSpeakers-fm +GET /mgmt/devices/{deviceId}/events handlers.(*Server).HandleMgmtDeviceEvents-fm +GET /mgmt/spotify/accounts handlers.(*Server).HandleMgmtSpotifyAccounts-fm +GET /mgmt/spotify/callback handlers.(*Server).HandleMgmtSpotifyCallback-fm +GET /mgmt/spotify/token handlers.(*Server).HandleMgmtSpotifyToken-fm +GET /oauth/* handlers.(*Server).HandleBoseProxy-fm +GET /proxy/* handlers.(*Server).HandleProxyRequest-fm +GET /setup/ca.crt handlers.(*Server).HandleGetCACert-fm +GET /setup/devices handlers.(*Server).HandleListDiscoveredDevices-fm +GET /setup/devices/{deviceId}/events handlers.(*Server).HandleGetDeviceEvents-fm +GET /setup/discovery-status handlers.(*Server).HandleGetDiscoveryStatus-fm +GET /setup/dns-discoveries handlers.(*Server).HandleGetDNSDiscoveries-fm +GET /setup/dns-discoveries/download handlers.(*Server).HandleDownloadDNSDiscoveries-fm +GET /setup/info/{deviceId} handlers.(*Server).HandleGetDeviceInfo-fm +GET /setup/interaction-content handlers.(*Server).HandleGetInteractionContent-fm +GET /setup/interaction-stats handlers.(*Server).HandleGetInteractionStats-fm +GET /setup/interactions handlers.(*Server).HandleListInteractions-fm +GET /setup/interactions/sessions/{session}/download handlers.(*Server).HandleDownloadSession-fm +GET /setup/parity-mismatches handlers.(*Server).HandleListParityMismatches-fm +GET /setup/proxy-settings handlers.(*Server).HandleGetProxySettings-fm +GET /setup/settings handlers.(*Server).HandleGetSettings-fm +GET /setup/summary/{deviceId} handlers.(*Server).HandleGetMigrationSummary-fm +GET /setup/version handlers.(*Server).HandleGetVersionInfo-fm +GET /streaming/account/{account}/device/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm +GET /streaming/account/{account}/device/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm +GET /streaming/account/{account}/device/{device}/group/member handlers.(*Server).HandleMargeDeviceGroupMember-fm +GET /streaming/account/{account}/device/{device}/group/server handlers.(*Server).HandleMargeDeviceGroupServer-fm +GET /streaming/account/{account}/device/{device}/presets handlers.(*Server).HandleMargePresets-fm +GET /streaming/account/{account}/device/{device}/recent handlers.(*Server).HandleMargeRecents-fm +GET /streaming/account/{account}/emailaddress handlers.(*Server).HandleMargeGetEmailAddress-fm +GET /streaming/account/{account}/full handlers.(*Server).HandleMargeAccountFull-fm +GET /streaming/account/{account}/provider_settings handlers.(*Server).HandleMargeProviderSettings-fm +GET /streaming/device/{device}/streaming_token handlers.(*Server).HandleMargeStreamingToken-fm +GET /streaming/device_setting/account/{account}/device/{device}/device_settings handlers.(*Server).HandleMargeGetDeviceSettings-fm +GET /streaming/software/update/account/{account} handlers.(*Server).HandleMargeSoftwareUpdate-fm +GET /streaming/sourceproviders handlers.(*Server).HandleMargeSourceProviders-fm +GET /updates/soundtouch handlers.(*Server).HandleMargeSoftwareUpdate-fm +GET /web/* setupRouter.(*Server).HandleWeb +HEAD /oauth/* handlers.(*Server).HandleBoseProxy-fm +OPTIONS /oauth/* handlers.(*Server).HandleBoseProxy-fm +PATCH /oauth/* handlers.(*Server).HandleBoseProxy-fm +POST /accounts/{account}/devices handlers.(*Server).HandleMargeAddDevice-fm +POST /accounts/{account}/devices/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm +POST /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleMargeAddRecent-fm +POST /bmx/orion/v1/playback/station/{data} handlers.(*Server).HandleOrionPlayback-fm +POST /customer/account/{account} handlers.(*Server).HandleMargeUpdateAccountProfile-fm +POST /customer/account/{account}/password handlers.(*Server).HandleMargeChangePassword-fm +POST /mgmt/accounts/{accountId}/language handlers.(*Server).HandleMgmtUpdateAccountLanguage-fm +POST /mgmt/accounts/{accountId}/provider-settings handlers.(*Server).HandleMgmtUpdateAccountProviderSetting-fm +POST /mgmt/spotify/confirm handlers.(*Server).HandleMgmtSpotifyConfirm-fm +POST /mgmt/spotify/entity handlers.(*Server).HandleMgmtSpotifyEntity-fm +POST /mgmt/spotify/init handlers.(*Server).HandleMgmtSpotifyInit-fm +POST /mgmt/spotify/prime handlers.(*Server).HandleMgmtPrimeDevice-fm +POST /oauth/* handlers.(*Server).HandleBoseProxy-fm +POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token handlers.(*Server).HandleBoseLegacyToken-fm +POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs3 handlers.(*Server).HandleBoseToken-fm +POST /setup/backup/{deviceId} handlers.(*Server).HandleBackupConfig-fm +POST /setup/devices handlers.(*Server).HandleAddManualDevice-fm +POST /setup/discover handlers.(*Server).HandleTriggerDiscovery-fm +POST /setup/ensure-remote-services/{deviceId} handlers.(*Server).HandleEnsureRemoteServices-fm +POST /setup/migrate/{deviceId} handlers.(*Server).HandleMigrateDevice-fm +POST /setup/proxy-settings handlers.(*Server).HandleUpdateProxySettings-fm +POST /setup/reboot/{deviceId} handlers.(*Server).HandleRebootDevice-fm +POST /setup/remove-remote-services/{deviceId} handlers.(*Server).HandleRemoveRemoteServices-fm +POST /setup/revert/{deviceId} handlers.(*Server).HandleRevertMigration-fm +POST /setup/settings handlers.(*Server).HandleUpdateSettings-fm +POST /setup/sync/{deviceId} handlers.(*Server).HandleInitialSync-fm +POST /setup/test-connection/{deviceId} handlers.(*Server).HandleTestConnection-fm +POST /setup/test-dns/{deviceId} handlers.(*Server).HandleTestDNSRedirection-fm +POST /setup/test-hosts/{deviceId} handlers.(*Server).HandleTestHostsRedirection-fm +POST /setup/trust-ca/{deviceId} handlers.(*Server).HandleTrustCACert-fm +POST /streaming/account handlers.(*Server).HandleMargeCreateAccount-fm +POST /streaming/account/login handlers.(*Server).HandleMargeLogin-fm +POST /streaming/account/{account}/device/ handlers.(*Server).HandleMargeAddDevice-fm +POST /streaming/account/{account}/device/{device} handlers.(*Server).HandleMargeAddDevice-fm +POST /streaming/account/{account}/device/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm +POST /streaming/account/{account}/device/{device}/recent handlers.(*Server).HandleMargeAddRecent-fm +POST /streaming/device_setting/account/{account}/device/{device}/device_settings handlers.(*Server).HandleMargeUpdateDeviceSettings-fm +POST /streaming/stats/error handlers.(*Server).HandleErrorStats-fm +POST /streaming/stats/usage handlers.(*Server).HandleUsageStats-fm +POST /streaming/support/customersupport handlers.(*Server).HandleMargeCustomerSupport-fm +POST /streaming/support/power_on handlers.(*Server).HandleMargePowerOn-fm +POST /v1/scmudc/{deviceId} handlers.(*Server).HandleAppEvents-fm +POST /v1/stapp/{deviceId} handlers.(*Server).HandleAppEvents-fm +PUT /oauth/* handlers.(*Server).HandleBoseProxy-fm +TRACE /oauth/* handlers.(*Server).HandleBoseProxy-fm