mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
This commit completes the music service account management implementation and resolves all golangci-lint issues across the codebase. Music Service Account Management: • Add/remove accounts for all major streaming services (Spotify, Pandora, Amazon Music, Deezer, iHeartRadio) • Support for network music libraries (NAS/UPnP/DLNA servers) • Generic account management with service-specific convenience methods • Full CLI integration with 14 account management commands • Comprehensive test coverage with mock HTTP servers • Complete API documentation and usage examples New CLI Commands: • account list - List configured accounts • account add/remove - Generic account management • account add-spotify/remove-spotify - Spotify Premium • account add-pandora/remove-pandora - Pandora Music Service • account add-amazon/remove-amazon - Amazon Music • account add-deezer/remove-deezer - Deezer Premium • account add-iheart/remove-iheart - iHeartRadio • account add-nas/remove-nas - Network music libraries New API Methods: • SetMusicServiceAccount() / RemoveMusicServiceAccount() - Generic methods • AddSpotifyAccount() / RemoveSpotifyAccount() - Convenience methods • AddPandoraAccount() / RemovePandoraAccount() - Convenience methods • AddAmazonMusicAccount() / RemoveAmazonMusicAccount() - Convenience methods • AddDeezerAccount() / RemoveDeezerAccount() - Convenience methods • AddIHeartRadioAccount() / RemoveIHeartRadioAccount() - Convenience methods • AddStoredMusicAccount() / RemoveStoredMusicAccount() - Network libraries golangci-lint Fixes (36 issues resolved): • errcheck (3): Fixed unchecked w.Write() returns in tests • gocritic (3): Rewrote if-else chains to switch statements • gocyclo (6): Reduced cyclomatic complexity via helper function extraction • govet (12): Removed unused test data and field assignments • revive (6): Added package comments and fixed unused parameters • staticcheck (2): Replaced deprecated strings.Title usage • thelper (6): Added t.Helper() calls to test helper functions • unused (1): Removed unused createTestApp() function • whitespace/wsl_v5 (7): Fixed whitespace and formatting issues Code Quality Improvements: • All functions now have complexity < 15 (down from max 28) • Consistent error handling and validation patterns • Better separation of concerns with extracted helper functions • Zero external dependencies added for simple fixes • Comprehensive documentation with usage examples • Full backward compatibility maintained Files Added: • pkg/models/account.go - Account management models • pkg/models/account_test.go - Account model tests • pkg/client/account_test.go - Account client tests • cmd/soundtouch-cli/cmd_account.go - Account CLI commands • examples/account-management/ - Complete usage example • Updated docs/CLI-REFERENCE.md with account management section The implementation provides a complete, production-ready music service account management system with full CLI and programmatic API support.
515 lines
12 KiB
Go
515 lines
12 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewMusicServiceCredentials(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
source string
|
|
displayName string
|
|
user string
|
|
pass string
|
|
}{
|
|
{
|
|
name: "Valid credentials",
|
|
source: "SPOTIFY",
|
|
displayName: "Spotify Premium",
|
|
user: "user@spotify.com",
|
|
pass: "password123",
|
|
},
|
|
{
|
|
name: "Empty display name",
|
|
source: "PANDORA",
|
|
displayName: "",
|
|
user: "pandora_user",
|
|
pass: "pandora_pass",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cred := NewMusicServiceCredentials(tt.source, tt.displayName, tt.user, tt.pass)
|
|
|
|
if cred.Source != tt.source {
|
|
t.Errorf("Expected source %s, got %s", tt.source, cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != tt.displayName {
|
|
t.Errorf("Expected displayName %s, got %s", tt.displayName, cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != tt.user {
|
|
t.Errorf("Expected user %s, got %s", tt.user, cred.User)
|
|
}
|
|
|
|
if cred.Pass != tt.pass {
|
|
t.Errorf("Expected pass %s, got %s", tt.pass, cred.Pass)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSpotifyCredentials(t *testing.T) {
|
|
cred := NewSpotifyCredentials("user@spotify.com", "mypassword")
|
|
|
|
if cred.Source != "SPOTIFY" {
|
|
t.Errorf("Expected source SPOTIFY, got %s", cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != "Spotify Premium" {
|
|
t.Errorf("Expected displayName 'Spotify Premium', got %s", cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != "user@spotify.com" {
|
|
t.Errorf("Expected user 'user@spotify.com', got %s", cred.User)
|
|
}
|
|
|
|
if cred.Pass != "mypassword" {
|
|
t.Errorf("Expected pass 'mypassword', got %s", cred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestNewPandoraCredentials(t *testing.T) {
|
|
cred := NewPandoraCredentials("pandora_user", "pandora_pass")
|
|
|
|
if cred.Source != "PANDORA" {
|
|
t.Errorf("Expected source PANDORA, got %s", cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != "Pandora Music Service" {
|
|
t.Errorf("Expected displayName 'Pandora Music Service', got %s", cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != "pandora_user" {
|
|
t.Errorf("Expected user 'pandora_user', got %s", cred.User)
|
|
}
|
|
|
|
if cred.Pass != "pandora_pass" {
|
|
t.Errorf("Expected pass 'pandora_pass', got %s", cred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestNewStoredMusicCredentials(t *testing.T) {
|
|
cred := NewStoredMusicCredentials("d09708a1-5953-44bc-a413-123456789012/0", "My NAS Library")
|
|
|
|
if cred.Source != "STORED_MUSIC" {
|
|
t.Errorf("Expected source STORED_MUSIC, got %s", cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != "My NAS Library" {
|
|
t.Errorf("Expected displayName 'My NAS Library', got %s", cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != "d09708a1-5953-44bc-a413-123456789012/0" {
|
|
t.Errorf("Expected user 'd09708a1-5953-44bc-a413-123456789012/0', got %s", cred.User)
|
|
}
|
|
|
|
if cred.Pass != "" {
|
|
t.Errorf("Expected empty pass for STORED_MUSIC, got %s", cred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestNewAmazonMusicCredentials(t *testing.T) {
|
|
cred := NewAmazonMusicCredentials("amazon_user", "amazon_pass")
|
|
|
|
if cred.Source != "AMAZON" {
|
|
t.Errorf("Expected source AMAZON, got %s", cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != "Amazon Music" {
|
|
t.Errorf("Expected displayName 'Amazon Music', got %s", cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != "amazon_user" {
|
|
t.Errorf("Expected user 'amazon_user', got %s", cred.User)
|
|
}
|
|
|
|
if cred.Pass != "amazon_pass" {
|
|
t.Errorf("Expected pass 'amazon_pass', got %s", cred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestNewDeezerCredentials(t *testing.T) {
|
|
cred := NewDeezerCredentials("deezer_user", "deezer_pass")
|
|
|
|
if cred.Source != "DEEZER" {
|
|
t.Errorf("Expected source DEEZER, got %s", cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != "Deezer Premium" {
|
|
t.Errorf("Expected displayName 'Deezer Premium', got %s", cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != "deezer_user" {
|
|
t.Errorf("Expected user 'deezer_user', got %s", cred.User)
|
|
}
|
|
|
|
if cred.Pass != "deezer_pass" {
|
|
t.Errorf("Expected pass 'deezer_pass', got %s", cred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestNewIHeartRadioCredentials(t *testing.T) {
|
|
cred := NewIHeartRadioCredentials("iheart_user", "iheart_pass")
|
|
|
|
if cred.Source != "IHEART" {
|
|
t.Errorf("Expected source IHEART, got %s", cred.Source)
|
|
}
|
|
|
|
if cred.DisplayName != "iHeartRadio" {
|
|
t.Errorf("Expected displayName 'iHeartRadio', got %s", cred.DisplayName)
|
|
}
|
|
|
|
if cred.User != "iheart_user" {
|
|
t.Errorf("Expected user 'iheart_user', got %s", cred.User)
|
|
}
|
|
|
|
if cred.Pass != "iheart_pass" {
|
|
t.Errorf("Expected pass 'iheart_pass', got %s", cred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceCredentials_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
credentials *MusicServiceCredentials
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "Valid Spotify credentials",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "SPOTIFY",
|
|
User: "user@spotify.com",
|
|
Pass: "password",
|
|
},
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Valid STORED_MUSIC credentials (no password required)",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "STORED_MUSIC",
|
|
User: "guid/0",
|
|
Pass: "",
|
|
},
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Empty source",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "",
|
|
User: "user",
|
|
Pass: "pass",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "source cannot be empty",
|
|
},
|
|
{
|
|
name: "Empty user",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "SPOTIFY",
|
|
User: "",
|
|
Pass: "pass",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "user cannot be empty",
|
|
},
|
|
{
|
|
name: "Empty password for non-STORED_MUSIC",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "SPOTIFY",
|
|
User: "user",
|
|
Pass: "",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "password cannot be empty for SPOTIFY",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.credentials.Validate()
|
|
|
|
if tt.wantError {
|
|
if err == nil {
|
|
t.Error("Expected error but got none")
|
|
} else if tt.errorMsg != "" && err.Error() != tt.errorMsg {
|
|
t.Errorf("Expected error message %q, got %q", tt.errorMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceCredentials_IsForRemoval(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
credentials *MusicServiceCredentials
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Has password - not for removal",
|
|
credentials: &MusicServiceCredentials{
|
|
Pass: "password",
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Empty password - for removal",
|
|
credentials: &MusicServiceCredentials{
|
|
Pass: "",
|
|
},
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.credentials.IsForRemoval()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %t, got %t", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceCredentials_HasPassword(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
credentials *MusicServiceCredentials
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Has password",
|
|
credentials: &MusicServiceCredentials{
|
|
Pass: "password",
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "No password",
|
|
credentials: &MusicServiceCredentials{
|
|
Pass: "",
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.credentials.HasPassword()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %t, got %t", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceCredentials_GetDescription(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
credentials *MusicServiceCredentials
|
|
expected string
|
|
}{
|
|
{
|
|
name: "Has display name",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "SPOTIFY",
|
|
DisplayName: "Custom Spotify Name",
|
|
},
|
|
expected: "Custom Spotify Name",
|
|
},
|
|
{
|
|
name: "Spotify default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "SPOTIFY",
|
|
},
|
|
expected: "Spotify Premium",
|
|
},
|
|
{
|
|
name: "Pandora default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "PANDORA",
|
|
},
|
|
expected: "Pandora Music Service",
|
|
},
|
|
{
|
|
name: "Amazon default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "AMAZON",
|
|
},
|
|
expected: "Amazon Music",
|
|
},
|
|
{
|
|
name: "Deezer default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "DEEZER",
|
|
},
|
|
expected: "Deezer Premium",
|
|
},
|
|
{
|
|
name: "iHeartRadio default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "IHEART",
|
|
},
|
|
expected: "iHeartRadio",
|
|
},
|
|
{
|
|
name: "STORED_MUSIC default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "STORED_MUSIC",
|
|
},
|
|
expected: "Network Music Library",
|
|
},
|
|
{
|
|
name: "LOCAL_MUSIC default",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "LOCAL_MUSIC",
|
|
},
|
|
expected: "Local Music Server",
|
|
},
|
|
{
|
|
name: "Unknown source",
|
|
credentials: &MusicServiceCredentials{
|
|
Source: "UNKNOWN",
|
|
},
|
|
expected: "UNKNOWN",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.credentials.GetDescription()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %s, got %s", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceAccountResponse_IsSuccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
response *MusicServiceAccountResponse
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Set account success",
|
|
response: &MusicServiceAccountResponse{
|
|
Status: "/setMusicServiceAccount",
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Remove account success",
|
|
response: &MusicServiceAccountResponse{
|
|
Status: "/removeMusicServiceAccount",
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Other status",
|
|
response: &MusicServiceAccountResponse{
|
|
Status: "/someOtherEndpoint",
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Empty status",
|
|
response: &MusicServiceAccountResponse{
|
|
Status: "",
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.response.IsSuccess()
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %t, got %t", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceCredentials_XMLMarshaling(t *testing.T) {
|
|
cred := &MusicServiceCredentials{
|
|
Source: "SPOTIFY",
|
|
DisplayName: "Spotify Premium",
|
|
User: "user@spotify.com",
|
|
Pass: "mypassword",
|
|
}
|
|
|
|
// Test marshaling
|
|
data, err := xml.Marshal(cred)
|
|
if err != nil {
|
|
t.Errorf("Failed to marshal credentials: %v", err)
|
|
}
|
|
|
|
expectedXML := `<credentials source="SPOTIFY" displayName="Spotify Premium"><user>user@spotify.com</user><pass>mypassword</pass></credentials>`
|
|
if string(data) != expectedXML {
|
|
t.Errorf("Expected XML %s, got %s", expectedXML, string(data))
|
|
}
|
|
|
|
// Test unmarshaling
|
|
var unmarshaledCred MusicServiceCredentials
|
|
|
|
err = xml.Unmarshal(data, &unmarshaledCred)
|
|
if err != nil {
|
|
t.Errorf("Failed to unmarshal credentials: %v", err)
|
|
}
|
|
|
|
if unmarshaledCred.Source != cred.Source {
|
|
t.Errorf("Expected source %s, got %s", cred.Source, unmarshaledCred.Source)
|
|
}
|
|
|
|
if unmarshaledCred.DisplayName != cred.DisplayName {
|
|
t.Errorf("Expected displayName %s, got %s", cred.DisplayName, unmarshaledCred.DisplayName)
|
|
}
|
|
|
|
if unmarshaledCred.User != cred.User {
|
|
t.Errorf("Expected user %s, got %s", cred.User, unmarshaledCred.User)
|
|
}
|
|
|
|
if unmarshaledCred.Pass != cred.Pass {
|
|
t.Errorf("Expected pass %s, got %s", cred.Pass, unmarshaledCred.Pass)
|
|
}
|
|
}
|
|
|
|
func TestMusicServiceAccountResponse_XMLMarshaling(t *testing.T) {
|
|
response := &MusicServiceAccountResponse{
|
|
Status: "/setMusicServiceAccount",
|
|
}
|
|
|
|
// Test marshaling
|
|
data, err := xml.Marshal(response)
|
|
if err != nil {
|
|
t.Errorf("Failed to marshal response: %v", err)
|
|
}
|
|
|
|
expectedXML := `<status>/setMusicServiceAccount</status>`
|
|
if string(data) != expectedXML {
|
|
t.Errorf("Expected XML %s, got %s", expectedXML, string(data))
|
|
}
|
|
|
|
// Test unmarshaling
|
|
var unmarshaledResponse MusicServiceAccountResponse
|
|
|
|
err = xml.Unmarshal(data, &unmarshaledResponse)
|
|
if err != nil {
|
|
t.Errorf("Failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if unmarshaledResponse.Status != response.Status {
|
|
t.Errorf("Expected status %s, got %s", response.Status, unmarshaledResponse.Status)
|
|
}
|
|
}
|