Files
Tobias Gesellchen 285f85efa2 feat: implement comprehensive music service account management with full golangci-lint compliance
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.
2026-02-02 17:44:26 +01:00

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)
}
}