credits_test.mx raw
1 package protocol
2
3 import "testing"
4
5 const (
6 prodPub = "4444444444444444444444444444444444444444444444444444444444444444"
7 mixPub = "5555555555555555555555555555555555555555555555555555555555555555"
8 mastPub = "6666666666666666666666666666666666666666666666666666666666666666"
9 labelPub = "7777777777777777777777777777777777777777777777777777777777777777"
10 )
11
12 // The release credits example from FEATURES_CREDITS.md.
13 func TestReleaseCreditsRoundTrip(t *testing.T) {
14 var want [][]string
15 want = appendKV(want, TagD, "credits-d")
16 want = appendTag(want, (&Address{Kind: KindAlbum, Pubkey: ownerPub, D: "album-d", Relay: "wss://relay.musiquay.com"}).Tag())
17 want = appendKV(want, TagTitle, "Credits: Album Name — Artist Name")
18 want = appendTag(want, (&Participant{Pubkey: prodPub, Relay: "wss://relay.musiquay.com", Role: "producer"}).Tag())
19 want = appendTag(want, (&Participant{Pubkey: mixPub, Relay: "wss://relay.musiquay.com", Role: "mix-engineer"}).Tag())
20 want = appendTag(want, (&Participant{Pubkey: mastPub, Relay: "wss://relay.musiquay.com", Role: "mastering-engineer"}).Tag())
21 want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer-vocals"}).Tag())
22 want = appendTag(want, (&Participant{Pubkey: labelPub, Relay: "wss://relay.musiquay.com", Role: "label"}).Tag())
23 want = appendTag(want, (&Studio{Activity: StudioRecording, Name: "Studio Name, City"}).Tag())
24 want = appendTag(want, (&Studio{Activity: StudioMix, Name: "Mix Studio Name, City"}).Tag())
25 want = appendTag(want, (&Studio{Activity: StudioMastering, Name: "Mastering Studio Name, City"}).Tag())
26
27 c, err := ParseReleaseCredits(want, "Liner notes.")
28 if err != nil {
29 t.Fatalf("parse: %s", err.Error())
30 }
31 if verr := c.Validate(); verr != nil {
32 t.Fatalf("validate: %s", verr.Error())
33 }
34 if c.Album == nil || c.Album.Kind != KindAlbum {
35 t.Fatalf("album reference = %v", c.Album)
36 }
37 if len(c.Participants) != 5 || len(c.Studios) != 3 {
38 t.Fatalf("participants=%d studios=%d", len(c.Participants), len(c.Studios))
39 }
40 if got := c.Tags(); !sameTags(got, want) {
41 t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
42 }
43 for i := range c.Participants {
44 if !KnownRole(c.Participants[i].Role) {
45 t.Fatalf("role %q from the spec is not in the vocabulary", c.Participants[i].Role)
46 }
47 }
48 }
49
50 func TestReleaseCreditsValidate(t *testing.T) {
51 c := &ReleaseCredits{D: "x"}
52 c.Participants = push(c.Participants, Participant{Pubkey: prodPub})
53 if err := c.Validate(); err == nil {
54 t.Fatal("a participant with no role was accepted")
55 }
56 c2 := &ReleaseCredits{D: "x"}
57 c2.Participants = push(c2.Participants, Participant{Pubkey: "short", Role: "producer"})
58 if err := c2.Validate(); err == nil {
59 t.Fatal("a short pubkey was accepted")
60 }
61 c3 := &ReleaseCredits{D: "x"}
62 c3.Studios = push(c3.Studios, Studio{Activity: StudioMix})
63 if err := c3.Validate(); err == nil {
64 t.Fatal("a studio with no name was accepted")
65 }
66 c4 := &ReleaseCredits{}
67 if err := c4.Validate(); err == nil {
68 t.Fatal("credits with no d tag were accepted")
69 }
70 c5 := &ReleaseCredits{D: "x"}
71 c5.Album = &Address{Kind: KindTrack, Pubkey: ownerPub, D: "d"}
72 if err := c5.Validate(); err == nil {
73 t.Fatal("release credits pointing at a track were accepted")
74 }
75 if w := (&ReleaseCredits{D: "x"}).Warnings(); len(w) != 2 {
76 t.Fatalf("warnings = %s", joinWarnings(w))
77 }
78 }
79
80 // The track credits example: the track address and the release credits address
81 // are told apart by kind, whichever order they arrive in.
82 func TestTrackCreditsRoundTrip(t *testing.T) {
83 want := [][]string{
84 {TagD, "tc-d"},
85 {TagA, "32210:" | ownerPub | ":track-d", "wss://relay.musiquay.com"},
86 {TagA, "32214:" | ownerPub | ":credits-d", "wss://relay.musiquay.com"},
87 {TagP, prodPub, "wss://relay.musiquay.com", "songwriter"},
88 {TagP, mixPub, "wss://relay.musiquay.com", "performer-cello"},
89 {TagP, artistPub, "wss://relay.musiquay.com", "featured-artist"},
90 }
91 c, err := ParseTrackCredits(want, "")
92 if err != nil {
93 t.Fatalf("parse: %s", err.Error())
94 }
95 if verr := c.Validate(); verr != nil {
96 t.Fatalf("validate: %s", verr.Error())
97 }
98 if c.Track == nil || c.Track.Kind != KindTrack || c.Track.D != "track-d" {
99 t.Fatalf("track = %v", c.Track)
100 }
101 if c.Release == nil || c.Release.Kind != KindReleaseCredits || c.Release.D != "credits-d" {
102 t.Fatalf("release = %v", c.Release)
103 }
104 if got := c.Tags(); !sameTags(got, want) {
105 t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
106 }
107 // Reversed order still resolves both addresses.
108 rev := [][]string{
109 {TagD, "tc-d"},
110 {TagA, "32214:" | ownerPub | ":credits-d"},
111 {TagA, "32210:" | ownerPub | ":track-d"},
112 }
113 c2, err := ParseTrackCredits(rev, "")
114 if err != nil {
115 t.Fatal(err.Error())
116 }
117 if c2.Track == nil || c2.Release == nil {
118 t.Fatalf("reversed order parsed to track=%v release=%v", c2.Track, c2.Release)
119 }
120 }
121
122 // The edition example: format-detail packs several values into one tag.
123 func TestEditionRoundTrip(t *testing.T) {
124 var want [][]string
125 want = appendKV(want, TagD, "edition-d")
126 want = appendTag(want, (&Address{Kind: KindAlbum, Pubkey: ownerPub, D: "album-d", Relay: "wss://relay.musiquay.com"}).Tag())
127 want = appendKV(want, TagTitle, "Album Name — Vinyl (First Pressing)")
128 want = appendKV(want, TagFormat, FormatVinyl)
129 want = appendTag(want, []string{TagFormatDetail, "180g", "black", "gatefold"})
130 want = appendKV(want, TagCatalog, "ABC-001")
131 want = appendKV(want, TagBarcode, "1234567890123")
132 want = appendKV(want, TagI, "barcode:1234567890123")
133 want = appendKV(want, TagK, "barcode")
134 want = appendKV(want, TagQuantity, "1000")
135 want = appendKV(want, TagReleaseDate, "2026-06-15")
136 want = appendKV(want, TagTerritory, "EU")
137 want = appendTag(want, (&Participant{Pubkey: mastPub, Relay: "wss://relay.musiquay.com", Role: "mastering-engineer"}).Tag())
138 want = appendTag(want, (&Participant{Pubkey: labelPub, Relay: "wss://relay.musiquay.com", Role: "label"}).Tag())
139 want = appendTag(want, (&Participant{Pubkey: prodPub, Relay: "wss://relay.musiquay.com", Role: "pressing-plant"}).Tag())
140 want = appendKV(want, TagT, "vinyl")
141 want = appendKV(want, TagT, "first-pressing")
142 want = appendKV(want, TagT, "limited")
143
144 e, err := ParseEdition(want, "First pressing. Black 180g vinyl in gatefold sleeve.")
145 if err != nil {
146 t.Fatalf("parse: %s", err.Error())
147 }
148 if verr := e.Validate(); verr != nil {
149 t.Fatalf("validate: %s", verr.Error())
150 }
151 if e.Format != FormatVinyl || !KnownFormat(e.Format) {
152 t.Fatalf("format = %q", e.Format)
153 }
154 if len(e.FormatDetail) != 3 || e.FormatDetail[0] != "180g" || e.FormatDetail[2] != "gatefold" {
155 t.Fatalf("format detail = %s", joinWarnings(e.FormatDetail))
156 }
157 if len(e.External) != 1 || e.External[0].I != "barcode:1234567890123" || e.External[0].K != "barcode" {
158 t.Fatalf("external = %v", e.External)
159 }
160 if len(e.Hashtags) != 3 {
161 t.Fatalf("hashtags = %s", joinWarnings(e.Hashtags))
162 }
163 if got := e.Tags(); !sameTags(got, want) {
164 t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
165 }
166 }
167
168 // The setlist example: ordered song tags, encore markers, and one single-letter
169 // a tag per published song so the query works.
170 func TestSetlistRoundTrip(t *testing.T) {
171 var want [][]string
172 want = appendTag(want, (&Address{Kind: KindCalendar, Pubkey: ownerPub, D: "event-d", Relay: "wss://relay.musiquay.com"}).Tag())
173 want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag())
174 want = appendKV(want, TagTitle, "Setlist: Band Name @ Lux Frágil, 2026-03-15")
175 want = appendTag(want, (&Song{Index: "1", Title: "Song Title One", Track: "32210:" | ownerPub | ":t1"}).Tag())
176 want = appendTag(want, (&Song{Index: "2", Title: "Song Title Two"}).Tag())
177 want = appendTag(want, (&Song{Index: "5", Title: "Song Title Five", Track: "32210:" | ownerPub | ":t5", Marker: SongFeat, Marker2: guestPub}).Tag())
178 want = appendTag(want, (&Song{Index: "6", Title: "Cover Song Title", Marker: SongCover, Marker2: "Original Artist"}).Tag())
179 want = appendKV(want, TagEncore, "1")
180 want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "t1"}).Tag())
181 want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "t5"}).Tag())
182 want = appendKV(want, TagT, "setlist")
183
184 sl, err := ParseSetlist(want, "Incredible set.")
185 if err != nil {
186 t.Fatalf("parse: %s", err.Error())
187 }
188 if verr := sl.Validate(); verr != nil {
189 t.Fatalf("validate: %s", verr.Error())
190 }
191 if sl.Event == nil || sl.Event.Kind != KindCalendar {
192 t.Fatalf("event = %v", sl.Event)
193 }
194 if len(sl.Songs) != 4 || sl.Songs[3].Marker != SongCover {
195 t.Fatalf("songs = %d", len(sl.Songs))
196 }
197 if sl.Songs[1].Track != "" {
198 t.Fatal("an unreleased song carries a track link")
199 }
200 if len(sl.TrackLinks) != 2 {
201 t.Fatalf("track links = %d, want 2", len(sl.TrackLinks))
202 }
203 if w := sl.Warnings(); len(w) != 1 {
204 // No d tag in the spec's own example: that gap is reported, not hidden.
205 t.Fatalf("warnings = %s", joinWarnings(w))
206 }
207 if got := sl.Tags(); !sameTags(got, want) {
208 t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
209 }
210 }
211
212 const guestPub = "8888888888888888888888888888888888888888888888888888888888888888"
213
214 func TestSongTagShapes(t *testing.T) {
215 // A song with neither a track nor a marker is three elements.
216 t3 := (&Song{Index: "1", Title: "One"}).Tag()
217 if len(t3) != 3 || t3[0] != TagSong || t3[1] != "1" || t3[2] != "One" {
218 t.Fatalf("plain song tag = %s", joinTags([][]string{t3}))
219 }
220 // A track link without a marker keeps the marker slot open.
221 t4 := (&Song{Index: "2", Title: "Two", Track: "32210:" | ownerPub | ":t2"}).Tag()
222 if len(t4) != 4 || t4[3] != "32210:"|ownerPub|":t2" {
223 t.Fatalf("track song tag = %s", joinTags([][]string{t4}))
224 }
225 t6 := (&Song{Index: "3", Title: "Three", Marker: SongFeat, Marker2: guestPub}).Tag()
226 if len(t6) != 6 || t6[4] != SongFeat || t6[5] != guestPub {
227 t.Fatalf("feat song tag = %s", joinTags([][]string{t6}))
228 }
229 }
230
231 func TestSetlistValidateRejects(t *testing.T) {
232 sl := &Setlist{}
233 sl.Event = &Address{Kind: KindTrack, Pubkey: ownerPub, D: "d"}
234 if err := sl.Validate(); err == nil {
235 t.Fatal("a setlist on a track address was accepted")
236 }
237 sl2 := &Setlist{}
238 sl2.Event = &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "d"}
239 sl2.Songs = push(sl2.Songs, Song{Title: "no index"})
240 if err := sl2.Validate(); err == nil {
241 t.Fatal("a song with no index was accepted")
242 }
243 sl3 := &Setlist{}
244 sl3.Event = &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "d"}
245 sl3.TrackLinks = push(sl3.TrackLinks, &Address{Kind: KindAlbum, Pubkey: ownerPub, D: "x"})
246 if err := sl3.Validate(); err == nil {
247 t.Fatal("a non-track link in a setlist was accepted")
248 }
249 }
250