all repos

rss-tools @ 77475b60d1ffffea61016d65bfa79f6aaa52b20b

get rss feed from sources that(i need and) dont provide one

rss-tools/sources/musicfeed/musicfeed_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
add musicfeed, 14 days ago
1
package musicfeed
2
3
import (
4
	"context"
5
	"net/http"
6
	"net/http/httptest"
7
	"strings"
8
	"testing"
9
	"time"
10
11
	"go.etcd.io/bbolt"
12
	"olexsmir.xyz/rss-tools/app"
13
	"olexsmir.xyz/x/is"
14
)
15
16
type fakeMusicAPI struct {
17
	artists  map[string]string
18
	releases map[string][]mbRelease
19
	errs     map[string]error
20
}
21
22
func (f fakeMusicAPI) searchArtist(_ context.Context, name string) (string, string, error) {
23
	if err, ok := f.errs[name]; ok {
24
		return "", "", err
25
	}
26
	mbid, ok := f.artists[name]
27
	if !ok {
28
		return "", "", nil
29
	}
30
	return mbid, name, nil
31
}
32
33
func (f fakeMusicAPI) fetchArtist(_ context.Context, mbid string) (string, error) {
34
	for name, id := range f.artists {
35
		if id == mbid {
36
			return name, nil
37
		}
38
	}
39
	return "", nil
40
}
41
42
func (f fakeMusicAPI) fetchReleases(_ context.Context, mbid string) ([]mbRelease, error) {
43
	if err, ok := f.errs[mbid]; ok {
44
		return nil, err
45
	}
46
	return f.releases[mbid], nil
47
}
48
49
func TestParseArtistEntryLabelMbid(t *testing.T) {
50
	entry := parseArtistEntry("orphan::2a9e4c32-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
51
	is.Equal(t, "orphan", entry.label)
52
	is.Equal(t, "2a9e4c32-xxxx-xxxx-xxxx-xxxxxxxxxxxx", entry.mbid)
53
}
54
55
func TestParseArtistEntryPlainSlug(t *testing.T) {
56
	entry := parseArtistEntry("metallica")
57
	is.Equal(t, "metallica", entry.label)
58
	is.Equal(t, "", entry.mbid)
59
}
60
61
func TestParseArtistEntryRawMBID(t *testing.T) {
62
	entry := parseArtistEntry("2a9e4c32-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
63
	is.Equal(t, "2a9e4c32-xxxx-xxxx-xxxx-xxxxxxxxxxxx", entry.label)
64
	is.Equal(t, "", entry.mbid)
65
}
66
67
func TestParseArtistEntryWhitespace(t *testing.T) {
68
	entry := parseArtistEntry("  orphan::  2a9e4c32-xxxx  ")
69
	is.Equal(t, "orphan", entry.label)
70
	is.Equal(t, "2a9e4c32-xxxx", entry.mbid)
71
}
72
73
func TestIsMBIDValid(t *testing.T) {
74
	is.Equal(t, true, isMBID("2a9e4c32-abcd-4ef8-9abc-123456789abc"))
75
	is.Equal(t, true, isMBID("550e8400-e29b-41d4-a716-446655440000"))
76
}
77
78
func TestIsMBIDInvalid(t *testing.T) {
79
	is.Equal(t, false, isMBID(""))
80
	is.Equal(t, false, isMBID("metallica"))
81
	is.Equal(t, false, isMBID("550e8400-e29b-41d4-a716-44665544000"))   // 35 chars
82
	is.Equal(t, false, isMBID("550e8400-e29b-41d4-a716-4466554400000")) // 37 chars
83
	is.Equal(t, false, isMBID("550e8400:e29b-41d4-a716-446655440000"))  // wrong separator
84
}
85
86
func TestParseMBDateFull(t *testing.T) {
87
	d := parseMBDate("2026-04-22")
88
	is.Equal(t, 2026, d.Year())
89
	is.Equal(t, time.Month(4), d.Month())
90
	is.Equal(t, 22, d.Day())
91
}
92
93
func TestParseMBDateYearMonth(t *testing.T) {
94
	d := parseMBDate("2026-04")
95
	is.Equal(t, 2026, d.Year())
96
	is.Equal(t, time.Month(4), d.Month())
97
	is.Equal(t, 1, d.Day())
98
}
99
100
func TestParseMBDateYearOnly(t *testing.T) {
101
	d := parseMBDate("2026")
102
	is.Equal(t, 2026, d.Year())
103
	is.Equal(t, time.Month(1), d.Month())
104
	is.Equal(t, 1, d.Day())
105
}
106
107
func TestParseMBDateInvalid(t *testing.T) {
108
	is.Equal(t, true, parseMBDate("").IsZero())
109
	is.Equal(t, true, parseMBDate("not-a-date").IsZero())
110
}
111
112
func TestDedupeByReleaseGroup(t *testing.T) {
113
	releases := []release{
114
		{id: "r1", releaseGroupID: "g1", title: "Album CD", hasArtwork: false},
115
		{id: "r2", releaseGroupID: "g1", title: "Album Vinyl", hasArtwork: true},
116
		{id: "r3", releaseGroupID: "g2", title: "Single A", hasArtwork: false},
117
	}
118
119
	deduped := dedupeByReleaseGroup(releases)
120
	is.Equal(t, 2, len(deduped))
121
	is.Equal(t, "Album Vinyl", deduped[0].title)
122
	is.Equal(t, "Single A", deduped[1].title)
123
	is.Equal(t, true, deduped[0].hasArtwork)
124
}
125
126
func TestDedupeByReleaseGroupNoID(t *testing.T) {
127
	releases := []release{
128
		{id: "r1", title: "No group", releaseGroupID: ""},
129
	}
130
	deduped := dedupeByReleaseGroup(releases)
131
	is.Equal(t, 1, len(deduped))
132
}
133
134
func TestReleaseContentWithoutArtwork(t *testing.T) {
135
	r := release{
136
		title:       "Porcelain",
137
		releaseType: "Album",
138
		hasArtwork:  false,
139
	}
140
	content, ctype := releaseContent(r, "Orphan")
141
	is.Equal(t, "", ctype)
142
	is.Equal(t, "Porcelain by Orphan (Album)", content)
143
}
144
145
func TestReleaseContentWithoutArtworkAndType(t *testing.T) {
146
	r := release{
147
		title:       "Porcelain",
148
		releaseType: "",
149
		hasArtwork:  false,
150
	}
151
	content, ctype := releaseContent(r, "Orphan")
152
	is.Equal(t, "", ctype)
153
	is.Equal(t, "Porcelain by Orphan", content)
154
}
155
156
func TestReleaseContentWithArtwork(t *testing.T) {
157
	r := release{
158
		id:          "mbid-123",
159
		title:       "Porcelain",
160
		releaseType: "Album",
161
		hasArtwork:  true,
162
	}
163
	content, ctype := releaseContent(r, "Orphan")
164
	is.Equal(t, "xhtml", ctype)
165
	is.Equal(t, strings.Contains(content, "<body>"), true)
166
	is.Equal(t, strings.Contains(content, "Porcelain by Orphan (Album)"), true)
167
	is.Equal(t, strings.Contains(content, `<img src="https://coverartarchive.org/release/mbid-123/front-250.jpg"`), true)
168
	is.Equal(t, strings.Contains(content, "</body>"), true)
169
}
170
171
func TestGenerateFeed(t *testing.T) {
172
	now := time.Now()
173
	releases := []release{
174
		{
175
			id:          "mbid-1",
176
			title:       "New Album",
177
			date:        now.Add(-24 * time.Hour),
178
			releaseType: "Album",
179
			artistName:  "Test Band",
180
			label:       "test-band",
181
			hasArtwork:  false,
182
		},
183
		{
184
			id:          "mbid-2",
185
			title:       "New Single",
186
			date:        now.Add(-48 * time.Hour),
187
			releaseType: "Single",
188
			artistName:  "Test Band",
189
			label:       "",
190
			hasArtwork:  false,
191
		},
192
	}
193
194
	feed := generateFeed(releases)
195
	is.Equal(t, "New Music Releases", feed.Title)
196
	is.Equal(t, "musicfeed", feed.ID)
197
	is.Equal(t, 2, len(feed.Entry))
198
199
	is.Equal(t, "test-band — New Album (Album)", feed.Entry[0].Title)
200
	is.Equal(t, "mbid-1", feed.Entry[0].ID)
201
	is.Equal(t, "Test Band — New Single (Single)", feed.Entry[1].Title)
202
}
203
204
func TestGenerateFeedWithLabelFallback(t *testing.T) {
205
	r := []release{{
206
		id: "mbid-1", title: "Album", date: time.Now(),
207
		releaseType: "Album", artistName: "Real Band Name", label: "",
208
	}}
209
	feed := generateFeed(r)
210
	is.Equal(t, "Real Band Name — Album (Album)", feed.Entry[0].Title)
211
}
212
213
func TestGenerateFeedWithArtworkLink(t *testing.T) {
214
	r := []release{{
215
		id: "mbid-1", title: "Album", date: time.Now(),
216
		releaseType: "Album", artistName: "Band", label: "band",
217
		hasArtwork: true,
218
	}}
219
	feed := generateFeed(r)
220
	is.Equal(t, 2, len(feed.Entry[0].Link))
221
	is.Equal(t, "alternate", feed.Entry[0].Link[0].Rel)
222
	is.Equal(t, "enclosure", feed.Entry[0].Link[1].Rel)
223
	is.Equal(t, "image/jpeg", feed.Entry[0].Link[1].Type)
224
}
225
226
func TestHandleMusicServesCachedFeed(t *testing.T) {
227
	bucket := newBucket(t)
228
229
	err := bucket.Set([]byte("feed"), []byte("<feed>test</feed>"))
230
	is.Err(t, err, nil)
231
232
	mf := &musicfeed{bucket: bucket}
233
	mf.refreshed.Store(true)
234
	mux := http.NewServeMux()
235
	mux.HandleFunc("GET /music", mf.handleMusic)
236
237
	req := httptest.NewRequest(http.MethodGet, "/music", nil)
238
	rr := httptest.NewRecorder()
239
	mux.ServeHTTP(rr, req)
240
241
	is.Equal(t, rr.Code, http.StatusOK)
242
	is.Equal(t, strings.Contains(rr.Header().Get("Content-Type"), "application/atom+xml"), true)
243
	is.Equal(t, "<feed>test</feed>", rr.Body.String())
244
}
245
246
func TestHandleMusicNoCache(t *testing.T) {
247
	bucket := newBucket(t)
248
	mf := &musicfeed{bucket: bucket}
249
	mf.refreshed.Store(true)
250
	mux := http.NewServeMux()
251
	mux.HandleFunc("GET /music", mf.handleMusic)
252
253
	req := httptest.NewRequest(http.MethodGet, "/music", nil)
254
	rr := httptest.NewRecorder()
255
	mux.ServeHTTP(rr, req)
256
257
	is.Equal(t, rr.Code, http.StatusServiceUnavailable)
258
}
259
260
func TestResolveArtistCachedMapping(t *testing.T) {
261
	bucket := newBucket(t)
262
	bucket.Set([]byte("mapping:metallica"), []byte("mbid-123"))
263
264
	mf := &musicfeed{
265
		bucket: bucket,
266
		api: fakeMusicAPI{
267
			artists: map[string]string{"metallica": "mbid-999"},
268
		},
269
	}
270
271
	mbid, label := mf.resolveArtist(context.Background(), artistEntry{label: "metallica"})
272
	is.Equal(t, "mbid-123", mbid)
273
	is.Equal(t, "metallica", label)
274
}
275
276
func TestResolveArtistWithExplicitMBID(t *testing.T) {
277
	mf := &musicfeed{
278
		api: fakeMusicAPI{
279
			artists: map[string]string{"Orphan": "mbid-orphan"},
280
		},
281
	}
282
283
	mbid, label := mf.resolveArtist(context.Background(), artistEntry{label: "orphan", mbid: "mbid-orphan"})
284
	is.Equal(t, "mbid-orphan", mbid)
285
	is.Equal(t, "orphan", label)
286
}
287
288
func TestIsSameDay(t *testing.T) {
289
	now := time.Now()
290
	is.Equal(t, true, isSameDay(now, now))
291
	is.Equal(t, true, isSameDay(now, now.Add(time.Hour)))
292
	is.Equal(t, false, isSameDay(now, now.Add(24*time.Hour)))
293
}
294
295
func newBucket(t *testing.T) *app.Bucket {
296
	t.Helper()
297
	db, err := bbolt.Open(t.TempDir()+"/test.db", 0o600, nil)
298
	is.Err(t, err, nil)
299
	t.Cleanup(func() { db.Close() })
300
301
	a := app.New(&app.Config{}, db)
302
	bucket, err := a.Bucket("musicfeed")
303
	is.Err(t, err, nil)
304
	return bucket
305
}