package protocol import "testing" const ( prodPub = "4444444444444444444444444444444444444444444444444444444444444444" mixPub = "5555555555555555555555555555555555555555555555555555555555555555" mastPub = "6666666666666666666666666666666666666666666666666666666666666666" labelPub = "7777777777777777777777777777777777777777777777777777777777777777" ) // The release credits example from FEATURES_CREDITS.md. func TestReleaseCreditsRoundTrip(t *testing.T) { var want [][]string want = appendKV(want, TagD, "credits-d") want = appendTag(want, (&Address{Kind: KindAlbum, Pubkey: ownerPub, D: "album-d", Relay: "wss://relay.musiquay.com"}).Tag()) want = appendKV(want, TagTitle, "Credits: Album Name — Artist Name") want = appendTag(want, (&Participant{Pubkey: prodPub, Relay: "wss://relay.musiquay.com", Role: "producer"}).Tag()) want = appendTag(want, (&Participant{Pubkey: mixPub, Relay: "wss://relay.musiquay.com", Role: "mix-engineer"}).Tag()) want = appendTag(want, (&Participant{Pubkey: mastPub, Relay: "wss://relay.musiquay.com", Role: "mastering-engineer"}).Tag()) want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer-vocals"}).Tag()) want = appendTag(want, (&Participant{Pubkey: labelPub, Relay: "wss://relay.musiquay.com", Role: "label"}).Tag()) want = appendTag(want, (&Studio{Activity: StudioRecording, Name: "Studio Name, City"}).Tag()) want = appendTag(want, (&Studio{Activity: StudioMix, Name: "Mix Studio Name, City"}).Tag()) want = appendTag(want, (&Studio{Activity: StudioMastering, Name: "Mastering Studio Name, City"}).Tag()) c, err := ParseReleaseCredits(want, "Liner notes.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := c.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if c.Album == nil || c.Album.Kind != KindAlbum { t.Fatalf("album reference = %v", c.Album) } if len(c.Participants) != 5 || len(c.Studios) != 3 { t.Fatalf("participants=%d studios=%d", len(c.Participants), len(c.Studios)) } if got := c.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } for i := range c.Participants { if !KnownRole(c.Participants[i].Role) { t.Fatalf("role %q from the spec is not in the vocabulary", c.Participants[i].Role) } } } func TestReleaseCreditsValidate(t *testing.T) { c := &ReleaseCredits{D: "x"} c.Participants = push(c.Participants, Participant{Pubkey: prodPub}) if err := c.Validate(); err == nil { t.Fatal("a participant with no role was accepted") } c2 := &ReleaseCredits{D: "x"} c2.Participants = push(c2.Participants, Participant{Pubkey: "short", Role: "producer"}) if err := c2.Validate(); err == nil { t.Fatal("a short pubkey was accepted") } c3 := &ReleaseCredits{D: "x"} c3.Studios = push(c3.Studios, Studio{Activity: StudioMix}) if err := c3.Validate(); err == nil { t.Fatal("a studio with no name was accepted") } c4 := &ReleaseCredits{} if err := c4.Validate(); err == nil { t.Fatal("credits with no d tag were accepted") } c5 := &ReleaseCredits{D: "x"} c5.Album = &Address{Kind: KindTrack, Pubkey: ownerPub, D: "d"} if err := c5.Validate(); err == nil { t.Fatal("release credits pointing at a track were accepted") } if w := (&ReleaseCredits{D: "x"}).Warnings(); len(w) != 2 { t.Fatalf("warnings = %s", joinWarnings(w)) } } // The track credits example: the track address and the release credits address // are told apart by kind, whichever order they arrive in. func TestTrackCreditsRoundTrip(t *testing.T) { want := [][]string{ {TagD, "tc-d"}, {TagA, "32210:" | ownerPub | ":track-d", "wss://relay.musiquay.com"}, {TagA, "32214:" | ownerPub | ":credits-d", "wss://relay.musiquay.com"}, {TagP, prodPub, "wss://relay.musiquay.com", "songwriter"}, {TagP, mixPub, "wss://relay.musiquay.com", "performer-cello"}, {TagP, artistPub, "wss://relay.musiquay.com", "featured-artist"}, } c, err := ParseTrackCredits(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := c.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if c.Track == nil || c.Track.Kind != KindTrack || c.Track.D != "track-d" { t.Fatalf("track = %v", c.Track) } if c.Release == nil || c.Release.Kind != KindReleaseCredits || c.Release.D != "credits-d" { t.Fatalf("release = %v", c.Release) } if got := c.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } // Reversed order still resolves both addresses. rev := [][]string{ {TagD, "tc-d"}, {TagA, "32214:" | ownerPub | ":credits-d"}, {TagA, "32210:" | ownerPub | ":track-d"}, } c2, err := ParseTrackCredits(rev, "") if err != nil { t.Fatal(err.Error()) } if c2.Track == nil || c2.Release == nil { t.Fatalf("reversed order parsed to track=%v release=%v", c2.Track, c2.Release) } } // The edition example: format-detail packs several values into one tag. func TestEditionRoundTrip(t *testing.T) { var want [][]string want = appendKV(want, TagD, "edition-d") want = appendTag(want, (&Address{Kind: KindAlbum, Pubkey: ownerPub, D: "album-d", Relay: "wss://relay.musiquay.com"}).Tag()) want = appendKV(want, TagTitle, "Album Name — Vinyl (First Pressing)") want = appendKV(want, TagFormat, FormatVinyl) want = appendTag(want, []string{TagFormatDetail, "180g", "black", "gatefold"}) want = appendKV(want, TagCatalog, "ABC-001") want = appendKV(want, TagBarcode, "1234567890123") want = appendKV(want, TagI, "barcode:1234567890123") want = appendKV(want, TagK, "barcode") want = appendKV(want, TagQuantity, "1000") want = appendKV(want, TagReleaseDate, "2026-06-15") want = appendKV(want, TagTerritory, "EU") want = appendTag(want, (&Participant{Pubkey: mastPub, Relay: "wss://relay.musiquay.com", Role: "mastering-engineer"}).Tag()) want = appendTag(want, (&Participant{Pubkey: labelPub, Relay: "wss://relay.musiquay.com", Role: "label"}).Tag()) want = appendTag(want, (&Participant{Pubkey: prodPub, Relay: "wss://relay.musiquay.com", Role: "pressing-plant"}).Tag()) want = appendKV(want, TagT, "vinyl") want = appendKV(want, TagT, "first-pressing") want = appendKV(want, TagT, "limited") e, err := ParseEdition(want, "First pressing. Black 180g vinyl in gatefold sleeve.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := e.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if e.Format != FormatVinyl || !KnownFormat(e.Format) { t.Fatalf("format = %q", e.Format) } if len(e.FormatDetail) != 3 || e.FormatDetail[0] != "180g" || e.FormatDetail[2] != "gatefold" { t.Fatalf("format detail = %s", joinWarnings(e.FormatDetail)) } if len(e.External) != 1 || e.External[0].I != "barcode:1234567890123" || e.External[0].K != "barcode" { t.Fatalf("external = %v", e.External) } if len(e.Hashtags) != 3 { t.Fatalf("hashtags = %s", joinWarnings(e.Hashtags)) } if got := e.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } } // The setlist example: ordered song tags, encore markers, and one single-letter // a tag per published song so the query works. func TestSetlistRoundTrip(t *testing.T) { var want [][]string want = appendTag(want, (&Address{Kind: KindCalendar, Pubkey: ownerPub, D: "event-d", Relay: "wss://relay.musiquay.com"}).Tag()) want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag()) want = appendKV(want, TagTitle, "Setlist: Band Name @ Lux Frágil, 2026-03-15") want = appendTag(want, (&Song{Index: "1", Title: "Song Title One", Track: "32210:" | ownerPub | ":t1"}).Tag()) want = appendTag(want, (&Song{Index: "2", Title: "Song Title Two"}).Tag()) want = appendTag(want, (&Song{Index: "5", Title: "Song Title Five", Track: "32210:" | ownerPub | ":t5", Marker: SongFeat, Marker2: guestPub}).Tag()) want = appendTag(want, (&Song{Index: "6", Title: "Cover Song Title", Marker: SongCover, Marker2: "Original Artist"}).Tag()) want = appendKV(want, TagEncore, "1") want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "t1"}).Tag()) want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "t5"}).Tag()) want = appendKV(want, TagT, "setlist") sl, err := ParseSetlist(want, "Incredible set.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := sl.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if sl.Event == nil || sl.Event.Kind != KindCalendar { t.Fatalf("event = %v", sl.Event) } if len(sl.Songs) != 4 || sl.Songs[3].Marker != SongCover { t.Fatalf("songs = %d", len(sl.Songs)) } if sl.Songs[1].Track != "" { t.Fatal("an unreleased song carries a track link") } if len(sl.TrackLinks) != 2 { t.Fatalf("track links = %d, want 2", len(sl.TrackLinks)) } if w := sl.Warnings(); len(w) != 1 { // No d tag in the spec's own example: that gap is reported, not hidden. t.Fatalf("warnings = %s", joinWarnings(w)) } if got := sl.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } } const guestPub = "8888888888888888888888888888888888888888888888888888888888888888" func TestSongTagShapes(t *testing.T) { // A song with neither a track nor a marker is three elements. t3 := (&Song{Index: "1", Title: "One"}).Tag() if len(t3) != 3 || t3[0] != TagSong || t3[1] != "1" || t3[2] != "One" { t.Fatalf("plain song tag = %s", joinTags([][]string{t3})) } // A track link without a marker keeps the marker slot open. t4 := (&Song{Index: "2", Title: "Two", Track: "32210:" | ownerPub | ":t2"}).Tag() if len(t4) != 4 || t4[3] != "32210:"|ownerPub|":t2" { t.Fatalf("track song tag = %s", joinTags([][]string{t4})) } t6 := (&Song{Index: "3", Title: "Three", Marker: SongFeat, Marker2: guestPub}).Tag() if len(t6) != 6 || t6[4] != SongFeat || t6[5] != guestPub { t.Fatalf("feat song tag = %s", joinTags([][]string{t6})) } } func TestSetlistValidateRejects(t *testing.T) { sl := &Setlist{} sl.Event = &Address{Kind: KindTrack, Pubkey: ownerPub, D: "d"} if err := sl.Validate(); err == nil { t.Fatal("a setlist on a track address was accepted") } sl2 := &Setlist{} sl2.Event = &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "d"} sl2.Songs = push(sl2.Songs, Song{Title: "no index"}) if err := sl2.Validate(); err == nil { t.Fatal("a song with no index was accepted") } sl3 := &Setlist{} sl3.Event = &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "d"} sl3.TrackLinks = push(sl3.TrackLinks, &Address{Kind: KindAlbum, Pubkey: ownerPub, D: "x"}) if err := sl3.Validate(); err == nil { t.Fatal("a non-track link in a setlist was accepted") } }