diff --git a/internal/download/bpm_test.go b/internal/download/bpm_test.go new file mode 100644 index 0000000..55b90f5 --- /dev/null +++ b/internal/download/bpm_test.go @@ -0,0 +1,42 @@ +package download + +import "testing" + +func bpmHTML(bpm, key, mode string) string { + return `tempo of ` + bpm + ` BPM` + + ` with a ` + key + ` key` + + ` and a ` + mode + ` mode` +} + +func TestParseBPM(t *testing.T) { + tests := []struct { + name string + html string + wantBPM string + wantKey string + }{ + {"major key", bpmHTML("128", "A", "major"), "128", "A"}, + {"minor key gets m suffix", bpmHTML("90", "F", "minor"), "90", "Fm"}, + {"unicode sharp normalized", bpmHTML("124", "C♯", "major"), "124", "C#"}, + {"unicode flat normalized", bpmHTML("100", "B♭", "minor"), "100", "Bbm"}, + {"enharmonic pair keeps first", bpmHTML("110", "A♯/B♭", "major"), "110", "A#"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseBPM(tt.html) + if err != nil { + t.Fatalf("parseBPM: %v", err) + } + if got.BPM != tt.wantBPM || got.Key != tt.wantKey { + t.Errorf("parseBPM() = %+v, want BPM %q Key %q", got, tt.wantBPM, tt.wantKey) + } + }) + } +} + +func TestParseBPMNoData(t *testing.T) { + if _, err := parseBPM("nothing here"); err == nil { + t.Error("expected error for page without BPM data") + } +} diff --git a/internal/download/genre_test.go b/internal/download/genre_test.go new file mode 100644 index 0000000..4e52ba8 --- /dev/null +++ b/internal/download/genre_test.go @@ -0,0 +1,51 @@ +package download + +import ( + "slices" + "testing" +) + +func TestMatchesKeyword(t *testing.T) { + if !matchesKeyword("Deep House", electronicKeywords) { + t.Error("expected Deep House to match electronic keywords") + } + if !matchesKeyword("classic rock", nonElectronicKeywords) { + t.Error("expected classic rock to match non-electronic keywords") + } + if matchesKeyword("Spoken Word", electronicKeywords) { + t.Error("did not expect Spoken Word to match electronic keywords") + } +} + +func TestFilterTags(t *testing.T) { + got := filterTags([]string{"Deep House", "Rock", "Spoken Word"}) + want := []string{"Deep House", "Rock"} + if !slices.Equal(got, want) { + t.Errorf("filterTags() = %v, want %v", got, want) + } + + if got := filterTags([]string{"Rock", "Jazz"}); len(got) != 0 { + t.Errorf("filterTags() = %v, want empty", got) + } + + if got := filterTags(nil); len(got) != 0 { + t.Errorf("filterTags(nil) = %v, want empty", got) + } +} + +func TestFormatTags(t *testing.T) { + tests := []struct { + tags []string + want string + }{ + {[]string{"deep house"}, "Deep House"}, + {[]string{"TECHNO", "trance"}, "Techno / Trance"}, + {[]string{" house ", ""}, "House"}, + } + + for _, tt := range tests { + if got := formatTags(tt.tags); got != tt.want { + t.Errorf("formatTags(%v) = %q, want %q", tt.tags, got, tt.want) + } + } +} diff --git a/internal/download/hash_test.go b/internal/download/hash_test.go new file mode 100644 index 0000000..ed6b6ef --- /dev/null +++ b/internal/download/hash_test.go @@ -0,0 +1,71 @@ +package download + +import ( + "context" + "os" + "path/filepath" + "testing" +) + +func TestHashFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "file.txt") + if err := os.WriteFile(path, []byte("hello world"), 0644); err != nil { + t.Fatal(err) + } + + got, err := hashFile(path) + if err != nil { + t.Fatalf("hashFile: %v", err) + } + + want := "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + if got != want { + t.Errorf("hashFile() = %s, want %s", got, want) + } +} + +func TestHashFileMissing(t *testing.T) { + if _, err := hashFile(filepath.Join(t.TempDir(), "missing")); err == nil { + t.Error("expected error for missing file") + } +} + +func TestHashIndexFind(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "sub", "track.mp3") + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("hello world"), 0644); err != nil { + t.Fatal(err) + } + + index, err := newHashIndex(context.Background(), dir) + if err != nil { + t.Fatalf("newHashIndex: %v", err) + } + + hash := "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + found, ok := index.find(hash) + if !ok || found != path { + t.Errorf("find(%s) = %q, %v; want %q, true", hash, found, ok, path) + } + + if _, ok := index.find("deadbeef"); ok { + t.Error("find() reported a match for an unknown hash") + } +} + +func TestHashIndexCanceledContext(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "a"), []byte("x"), 0644); err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + if _, err := newHashIndex(ctx, dir); err == nil { + t.Error("expected error for canceled context") + } +} diff --git a/internal/download/options_test.go b/internal/download/options_test.go new file mode 100644 index 0000000..a2aa1b2 --- /dev/null +++ b/internal/download/options_test.go @@ -0,0 +1,39 @@ +package download + +import ( + "testing" + "time" +) + +func TestOptionsValidate(t *testing.T) { + valid := Options{Quality: "mp3_320", Timeout: time.Minute, Limit: 10} + + tests := []struct { + name string + mutate func(o *Options) + wantErr bool + }{ + {"valid", func(o *Options) {}, false}, + {"mp3_128", func(o *Options) { o.Quality = "mp3_128" }, false}, + {"flac", func(o *Options) { o.Quality = "flac" }, false}, + {"invalid quality", func(o *Options) { o.Quality = "ogg" }, true}, + {"uppercase quality", func(o *Options) { o.Quality = "MP3_320" }, true}, + {"zero timeout", func(o *Options) { o.Timeout = 0 }, true}, + {"negative timeout", func(o *Options) { o.Timeout = -time.Second }, true}, + {"zero limit", func(o *Options) { o.Limit = 0 }, true}, + {"limit too high", func(o *Options) { o.Limit = 101 }, true}, + {"limit at max", func(o *Options) { o.Limit = 100 }, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := valid + tt.mutate(&opts) + + err := opts.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 0000000..2817d1b --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,63 @@ +package store + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestOpenPutGet(t *testing.T) { + dir := t.TempDir() + + s, err := Open(dir) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer s.Close() + + if _, err := os.Stat(filepath.Join(dir, ".tracks.db")); err != nil { + t.Errorf("expected .tracks.db to exist: %v", err) + } + + info := &DownloadInfo{ + TrackID: "123", + Quality: "MP3_320", + Path: "/music/track.mp3", + Hash: "abc", + Downloaded: time.Now().Truncate(time.Second), + } + if err := s.PutDownloadInfo(info); err != nil { + t.Fatalf("PutDownloadInfo: %v", err) + } + + got, err := s.DownloadInfo("123") + if err != nil { + t.Fatalf("DownloadInfo: %v", err) + } + if got.TrackID != info.TrackID || got.Quality != info.Quality || got.Path != info.Path || got.Hash != info.Hash { + t.Errorf("DownloadInfo() = %+v, want %+v", got, info) + } +} + +func TestDownloadInfoNotFound(t *testing.T) { + s, err := Open(t.TempDir()) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer s.Close() + + if _, err := s.DownloadInfo("missing"); err == nil { + t.Error("expected error for unknown track ID") + } +} + +func TestClose(t *testing.T) { + s, err := Open(t.TempDir()) + if err != nil { + t.Fatalf("Open: %v", err) + } + if err := s.Close(); err != nil { + t.Errorf("Close: %v", err) + } +}