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