package main
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseFileInputJSONItems(t *testing.T) {
content := []byte(`[
{"source":"qobuz","media_type":"album","id":"0066991040005"},
{"source":"tidal","media_type":"track","id":3083287}
]`)
items, urls, repeated, jsonInput, err := parseFileInput(content)
if err != nil {
t.Fatalf("parseFileInput() error = %v", err)
}
if !jsonInput {
t.Fatalf("jsonInput = false, want true")
}
if len(urls) != 0 {
t.Fatalf("urls len = %d, want 0", len(urls))
}
if repeated != 0 {
t.Fatalf("repeated = %d, want 0", repeated)
}
if len(items) != 2 {
t.Fatalf("items len = %d, want 2", len(items))
}
if items[0].Source != "qobuz" || items[0].MediaType != "album" || items[0].ID != "0066991040005" {
t.Fatalf("unexpected first item: %+v", items[0])
}
if items[1].Source != "tidal" || items[1].MediaType != "track" || items[1].ID != "3083287" {
t.Fatalf("unexpected second item: %+v", items[1])
}
}
func TestParseFileInputTextURLsDedupes(t *testing.T) {
content := []byte("https://tidal.com/browse/track/3083287\nhttps://tidal.com/browse/track/3083287\nhttps://www.qobuz.com/fr-fr/album/example/0066991040005\n")
items, urls, repeated, jsonInput, err := parseFileInput(content)
if err != nil {
t.Fatalf("parseFileInput() error = %v", err)
}
if jsonInput {
t.Fatalf("jsonInput = true, want false")
}
if len(items) != 0 {
t.Fatalf("items len = %d, want 0", len(items))
}
if repeated != 1 {
t.Fatalf("repeated = %d, want 1", repeated)
}
if len(urls) != 2 {
t.Fatalf("urls len = %d, want 2", len(urls))
}
}
func TestParseFileInputRejectsInvalidJSONShape(t *testing.T) {
content := []byte(`{"source":"qobuz","media_type":"track","id":"1"}`)
_, _, _, jsonInput, err := parseFileInput(content)
if err == nil {
t.Fatalf("expected error for non-array json")
}
if !jsonInput {
t.Fatalf("jsonInput = false, want true")
}
}
func TestParseLastFMArgsDefaults(t *testing.T) {
opts, err := parseLastFMArgs([]string{"https://www.last.fm/user/x/playlists/123"}, "qobuz", "")
if err != nil {
t.Fatalf("parseLastFMArgs() error = %v", err)
}
if opts.Source != "qobuz" {
t.Fatalf("source = %q, want qobuz", opts.Source)
}
if opts.PlaylistURL == "" {
t.Fatalf("playlist url should not be empty")
}
}
func TestParseLastFMArgsOptions(t *testing.T) {
opts, err := parseLastFMArgs([]string{"--source", "tidal", "--fallback-source", "qobuz", "https://www.last.fm/user/x/playlists/123"}, "qobuz", "")
if err != nil {
t.Fatalf("parseLastFMArgs() error = %v", err)
}
if opts.Source != "tidal" || opts.FallbackSource != "qobuz" {
t.Fatalf("unexpected options: %+v", opts)
}
}
func TestExtractLastFMPlaylistInfoAndPairs(t *testing.T) {
html := `
`
title, total, err := extractLastFMPlaylistInfo(html)
if err != nil {
t.Fatalf("extractLastFMPlaylistInfo() error = %v", err)
}
if title != "Road & Rain" {
t.Fatalf("title = %q, want %q", title, "Road & Rain")
}
if total != 2 {
t.Fatalf("total = %d, want 2", total)
}
pairs := extractLastFMTitleArtistPairs(html)
if len(pairs) != 2 {
t.Fatalf("pairs len = %d, want 2", len(pairs))
}
if pairs[0].Title != "Dreams" || pairs[0].Artist != "Fleetwood Mac" {
t.Fatalf("unexpected first pair: %+v", pairs[0])
}
}
func TestParseGlobalArgsNoDBBeforeCommand(t *testing.T) {
opts, err := parseGlobalArgs([]string{"-ndb", "url", "https://play.qobuz.com/album/0004228000522"})
if err != nil {
t.Fatalf("parseGlobalArgs() error = %v", err)
}
if !opts.noDB {
t.Fatalf("expected noDB true")
}
if opts.command != "url" {
t.Fatalf("command = %q, want %q", opts.command, "url")
}
if len(opts.commandArgs) != 1 {
t.Fatalf("command args len = %d, want 1", len(opts.commandArgs))
}
}
func TestParseGlobalArgsAllOfficialFlags(t *testing.T) {
opts, err := parseGlobalArgs([]string{
"--config-path", "/tmp/custom.toml",
"-f", "/tmp/music",
"--no-db",
"-q", "3",
"-c", "ogg",
"--no-progress",
"--no-ssl-verify",
"-v",
"search", "tidal", "track", "dreams",
})
if err != nil {
t.Fatalf("parseGlobalArgs() error = %v", err)
}
if opts.configPath != "/tmp/custom.toml" || opts.folder != "/tmp/music" {
t.Fatalf("unexpected path/folder opts: %+v", opts)
}
if !opts.noDB || !opts.qualitySet || opts.quality != 3 || !opts.codecSet || opts.codec != "VORBIS" {
t.Fatalf("unexpected quality/codec/db opts: %+v", opts)
}
if !opts.noProgress || !opts.noSSLVerify || !opts.verbose {
t.Fatalf("unexpected boolean opts: %+v", opts)
}
if opts.command != "search" {
t.Fatalf("command = %q, want search", opts.command)
}
}
func TestNormalizeCodecRejectsUnknown(t *testing.T) {
if _, err := normalizeCodec("wav"); err == nil {
t.Fatalf("expected error for unsupported codec")
}
}
func TestExtractLastFMTracksFromMirrorMarkdown(t *testing.T) {
md := `Title: My Playlist | user playlists | Last.fm
| Play | Image | Loved | Name | Artist name | Buy | Options | Duration |
| --- | --- | --- | --- | --- | --- | --- | --- |
| [Play track](https://x) | [img](https://i) | x | [Song A](https://a) | [Artist A](https://aa) | | | 3:00 |
| [Play track](https://x) | [img](https://i) | x | [Song B](https://b) | [Artist B](https://bb) | | | 4:00 |`
title, tracks := extractLastFMTracksFromMirrorMarkdown(md)
if title != "My Playlist" {
t.Fatalf("title = %q, want %q", title, "My Playlist")
}
if len(tracks) != 2 {
t.Fatalf("tracks len = %d, want 2", len(tracks))
}
if tracks[0].Title != "Song A" || tracks[0].Artist != "Artist A" {
t.Fatalf("unexpected first track: %+v", tracks[0])
}
}
func TestParseSearchArgsAllowsFirstAndOutputFileButCallerCanReject(t *testing.T) {
opts, err := parseSearchArgs([]string{"q", "--first", "--output-file", "/tmp/out.json"}, 20)
if err != nil {
t.Fatalf("parseSearchArgs() error = %v", err)
}
if !opts.first || opts.outputFile == "" {
t.Fatalf("expected first=true and output file set, got %+v", opts)
}
}
func TestParseSearchArgsRejectsUnknownOption(t *testing.T) {
_, err := parseSearchArgs([]string{"query", "--bogus"}, 20)
if err == nil || !strings.Contains(err.Error(), "unknown option") {
t.Fatalf("expected unknown option error, got %v", err)
}
}
func TestParseSearchArgsSupportsDoubleDashTerminator(t *testing.T) {
opts, err := parseSearchArgs([]string{"--limit", "10", "--", "--weird", "track"}, 20)
if err != nil {
t.Fatalf("parseSearchArgs() error = %v", err)
}
if opts.limit != 10 {
t.Fatalf("limit = %d, want 10", opts.limit)
}
if opts.query != "--weird track" {
t.Fatalf("query = %q, want %q", opts.query, "--weird track")
}
}
func TestWriteSearchResultsToFileCreatesParentDirectory(t *testing.T) {
tmp := t.TempDir()
out := filepath.Join(tmp, "nested", "search", "results.json")
results := []searchResult{{ID: "1", Title: "Dreams"}}
if err := writeSearchResultsToFile("qobuz", "track", results, out); err != nil {
t.Fatalf("writeSearchResultsToFile() error = %v", err)
}
if _, err := os.Stat(out); err != nil {
t.Fatalf("expected output file, stat error = %v", err)
}
}
func TestErrorWithActionableHintForSSL(t *testing.T) {
err := errors.New("x509: certificate signed by unknown authority")
msg := errorWithActionableHint(err, globalOptions{})
if msg == err.Error() {
t.Fatalf("expected ssl hint in message")
}
}
func TestErrorWithActionableHintNoHintWhenDisabled(t *testing.T) {
err := errors.New("tls handshake failure")
msg := errorWithActionableHint(err, globalOptions{noSSLVerify: true})
if msg != err.Error() {
t.Fatalf("unexpected hint when noSSLVerify set")
}
}