package protocol import "testing" // The track example from FEATURES_MUSIC.md, tag for tag. A parse followed by a // render must reproduce this list exactly: the struct order IS the example // order, so a field added in the wrong place shows up here. func docTrackTags() (tags [][]string) { tags = appendKV(tags, TagD, "PT-ABC-26-00001") tags = appendKV(tags, TagTitle, "Song Title") tags = appendKV(tags, TagArtist, "Band Name") tags = appendTag(tags, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag()) tags = appendTag(tags, (&Address{Kind: KindAlbum, Pubkey: ownerPub, D: "album-d", Relay: "wss://relay.musiquay.com"}).Tag()) tags = appendKV(tags, TagAlbum, "Album Name") tags = appendKV(tags, TagTrack, "5/12") tags = appendKV(tags, TagDisc, "1/2") tags = appendKV(tags, TagDuration, "247.32") tags = appendKV(tags, TagGenre, "post-punk") tags = appendKV(tags, TagT, "postpunk") tags = appendKV(tags, TagReleaseDate, "2026-06-15") tags = appendKV(tags, TagISRC, "PT-ABC-26-00001") tags = appendKV(tags, TagLanguage, "en") tags = appendKV(tags, TagBPM, "138") tags = appendKV(tags, TagKey, "Am") tags = appendKV(tags, TagCopyright, "CC BY-NC-SA 4.0") tags = appendTag(tags, (&Price{Amount: "500", Unit: "sat"}).Tag()) tags = appendTag(tags, (&BlobRef{Sha256: coverHash, Server: "https://blossom.musiquay.com"}).Tag()) tags = appendKV(tags, TagI, "musicbrainz:recording:8be5c8c0-7b52-4a2d-9d4e-1c2f3a4b5c6d") tags = appendKV(tags, TagK, "musicbrainz:recording") tags = appendTag(tags, (&Encoding{Sha256: flacHash, Spec: "flac-24bit-96kHz", Server: "https://blossom.musiquay.com", MIME: "audio/flac", Size: 48216387, HasSize: true}).Tag()) tags = appendTag(tags, (&Encoding{Sha256: mp3Hash, Spec: "mp3-128kbps-VBR", Server: "https://blossom.musiquay.com", MIME: "audio/mpeg", Size: 9110334, HasSize: true}).Tag()) tags = appendTag(tags, (&Encoding{Sha256: opusHash, Spec: "opus-96kbps", Server: "https://blossom.musiquay.com", MIME: "audio/ogg", Size: 3942210, HasSize: true}).Tag()) tags = appendTag(tags, (&Encoding{Sha256: flacHash}).XTag()) tags = appendTag(tags, (&Encoding{Sha256: mp3Hash}).XTag()) tags = appendTag(tags, (&Encoding{Sha256: opusHash}).XTag()) tags = appendKV(tags, TagMaster, flacHash) return } const ( artistPub = "3333333333333333333333333333333333333333333333333333333333333333" coverHash = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" flacHash = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" mp3Hash = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" opusHash = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ) func TestTrackRoundTrip(t *testing.T) { want := docTrackTags() tr, err := ParseTrack(want, "Liner notes.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := tr.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if w := tr.Warnings(); len(w) != 0 { t.Fatalf("warnings on a complete track: %s", joinWarnings(w)) } got := tr.Tags() if !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } if tr.Content != "Liner notes." { t.Fatalf("content = %q", tr.Content) } if tr.Album == nil || tr.Album.Kind != KindAlbum || tr.Album.Relay != "wss://relay.musiquay.com" { t.Fatalf("album reference = %v", tr.Album) } if len(tr.Encodings) != 3 { t.Fatalf("encodings = %d, want 3", len(tr.Encodings)) } if tr.Encodings[0].Size != 48216387 || !tr.Encodings[0].HasSize { t.Fatalf("first encoding = %+v", tr.Encodings[0]) } if tr.Master != flacHash { t.Fatalf("master = %q", tr.Master) } if len(tr.External) != 1 || tr.External[0].I != "musicbrainz:recording:8be5c8c0-7b52-4a2d-9d4e-1c2f3a4b5c6d" || tr.External[0].K != "musicbrainz:recording" { t.Fatalf("external ids = %v", tr.External) } if tr.Duration != "247.32" || tr.BPM != "138" || tr.ISRC != "PT-ABC-26-00001" { t.Fatalf("numeric-ish fields: %+v", tr) } } // The d tag SHOULD be the ISRC, and the address is what everything references. func TestTrackAddress(t *testing.T) { tr, err := ParseTrack(docTrackTags(), "") if err != nil { t.Fatal(err.Error()) } a := &Address{Kind: KindTrack, Pubkey: ownerPub, D: tr.D} if a.String() != "32210:"|ownerPub|":PT-ABC-26-00001" { t.Fatalf("address = %q", a.String()) } } func TestTrackValidateRejects(t *testing.T) { // A master that is not one of the encodings. tr, err := ParseTrack(docTrackTags(), "") if err != nil { t.Fatal(err.Error()) } tr.Master = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" if verr := tr.Validate(); verr == nil { t.Fatal("a master outside the encodings was accepted") } // A bad codec spec on an encoding. tr2, _ := ParseTrack(docTrackTags(), "") tr2.Encodings[0].Spec = "flac-" if verr := tr2.Validate(); verr == nil { t.Fatal("a malformed codec spec was accepted") } // A short blob hash. tr3, _ := ParseTrack(docTrackTags(), "") tr3.Encodings[0].Sha256 = "abcd" if verr := tr3.Validate(); verr == nil { t.Fatal("a short blob hash was accepted") } // An album reference of the wrong kind. tr4, _ := ParseTrack(docTrackTags(), "") tr4.Album.Kind = KindPlaylist if verr := tr4.Validate(); verr == nil { t.Fatal("an album reference of the wrong kind was accepted") } // No d tag at all: the address is the identity. tr5 := &Track{Title: "x"} if verr := tr5.Validate(); verr == nil { t.Fatal("a track with no d tag was accepted") } } func TestTrackWarnings(t *testing.T) { tr := &Track{D: "x", Title: "T"} tr.Encodings = push(tr.Encodings, Encoding{Sha256: flacHash, Spec: "flac-16bit-44.1kHz"}) w := tr.Warnings() if len(w) < 2 { t.Fatalf("warnings = %s", joinWarnings(w)) } tr.Participants = push(tr.Participants, Participant{Pubkey: ownerPub, Role: "wizard"}) found := false for _, s := range tr.Warnings() { if s == "unknown role: wizard" { found = true } } if !found { t.Fatal("an unknown role did not warn") } } // Unknown tags survive a parse and are re-emitted: an older build must not // strip a newer field off an event it stores or forwards. func TestTrackKeepsUnknownTags(t *testing.T) { tags := appendTags(docTrackTags(), [][]string{ {"vibe", "shoegaze"}, {"z", "future"}, }) tr, err := ParseTrack(tags, "") if err != nil { t.Fatal(err.Error()) } if len(tr.Extra) != 2 { t.Fatalf("extra = %s", joinTags(tr.Extra)) } got := tr.Tags() if len(got) != len(tags) { t.Fatalf("round trip lost tags: %d vs %d", len(got), len(tags)) } if got[len(got)-2][0] != "vibe" || got[len(got)-1][0] != "z" { t.Fatalf("extra tags not re-emitted: %s", joinTags(got)) } } // The album example from FEATURES_MUSIC.md: the ordered a tags are the // tracklist, and the order must survive. func TestAlbumRoundTrip(t *testing.T) { var want [][]string want = appendKV(want, TagD, "album-d") want = appendKV(want, TagTitle, "Album Name") want = appendKV(want, TagArtist, "Band Name") want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag()) want = appendTag(want, (&Participant{Pubkey: ownerPub, Relay: "wss://relay.musiquay.com", Role: "label"}).Tag()) want = appendKV(want, TagReleaseDate, "2026-06-15") want = appendKV(want, TagGenre, "post-punk") want = appendKV(want, TagT, "postpunk") want = appendKV(want, TagLanguage, "en") want = appendKV(want, TagCopyright, "CC BY-NC-SA 4.0") want = appendTag(want, (&Price{Amount: "5000", Unit: "sat"}).Tag()) want = appendTag(want, (&BlobRef{Sha256: coverHash, Server: "https://blossom.musiquay.com"}).Tag()) want = appendKV(want, TagI, "musicbrainz:release-group:5c9e6d6c-2c2e-4a7b-9c3f-3a4b5c6d7e8f") want = appendKV(want, TagK, "musicbrainz:release-group") for i := 1; i <= 3; i++ { want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d-" | intStr(int64(i)), Relay: "wss://relay.musiquay.com"}).Tag()) } al, err := ParseAlbum(want, "Release notes.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := al.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if len(al.Tracks) != 3 || al.Tracks[2].D != "track-d-3" { t.Fatalf("tracklist = %s", joinTags(al.Tags())) } if got := al.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } // A tracklist entry of the wrong kind is rejected. al.Tracks[1].Kind = KindAlbum if verr := al.Validate(); verr == nil { t.Fatal("a non-track address in a tracklist was accepted") } } // The playlist example: NIP-51 set conventions, members as ordered a tags. func TestPlaylistRoundTrip(t *testing.T) { var want [][]string want = appendKV(want, TagD, "my-favourite-deep-cuts") want = appendKV(want, TagTitle, "My Favourite Deep Cuts") want = appendKV(want, TagDescription, "Tracks that never got the attention they deserved.") want = appendTag(want, (&BlobRef{Sha256: coverHash, Server: "https://blossom.musiquay.com"}).Tag()) want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d-1", Relay: "wss://relay.musiquay.com"}).Tag()) want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: artistPub, D: "track-d-2", Relay: "wss://relay.musiquay.com"}).Tag()) pl, err := ParsePlaylist(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := pl.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if pl.IsPrivate() { t.Fatal("a public playlist reported as private") } if got := pl.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } // A private playlist has no member tags: the members are in content. priv := &Playlist{D: "secret", Title: "Secret", Content: "nip44-ciphertext"} if !priv.IsPrivate() { t.Fatal("a playlist with members in content is private") } if verr := priv.Validate(); verr != nil { t.Fatalf("validate private: %s", verr.Error()) } } // The paid play report from FEATURES_MUSIC.md, and the statement its blind // signature covers. func TestPlayReportPaidRoundTrip(t *testing.T) { var want [][]string want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d", Relay: "wss://relay.musiquay.com"}).Tag()) want = appendKV(want, TagP, artistPub) want = appendKV(want, TagX, mp3Hash) want = appendKV(want, TagServer, "https://blossom.musiquay.com") want = appendKV(want, TagCodec, "mp3-128kbps-VBR") want = appendKV(want, TagT0, "1758130000") want = appendKV(want, TagT1, "1758130247") want = appendKV(want, TagBlind, "02blind") want = appendKV(want, TagHost, ownerPub) want = appendKV(want, TagStatement, "73746174656d656e74") d := []string{:3} d[0] = TagDleq d[1] = "02e" d[2] = "02s" want = appendTag(want, d) r, err := ParsePlayReport(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := r.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if !r.IsPaid() { t.Fatal("a report with host proof is not marked paid") } if got := r.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } if r.Dleq == nil || r.Dleq.E != "02e" || r.Dleq.S != "02s" { t.Fatalf("dleq = %v", r.Dleq) } stmt := PlayStatement(r.Asset.String(), r.Blob, r.T0, r.T1) if stmt != "musiquay.play:v1|32210:"|ownerPub|":track-d|"|mp3Hash|"|1758130000|1758130247" { t.Fatalf("statement = %q", stmt) } } func TestPlayReportFreeIsUnproven(t *testing.T) { var tags [][]string tags = appendTag(tags, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag()) tags = appendKV(tags, TagP, artistPub) tags = appendKV(tags, TagX, mp3Hash) tags = appendKV(tags, TagServer, "https://blossom.musiquay.com") tags = appendKV(tags, TagCodec, "opus-96kbps") tags = appendKV(tags, TagT0, "1758130000") tags = appendKV(tags, TagT1, "1758130247") r, err := ParsePlayReport(tags, "") if err != nil { t.Fatal(err.Error()) } if r.IsPaid() { t.Fatal("a free play reported as paid") } if verr := r.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if got := r.Tags(); !sameTags(got, tags) { t.Fatalf("round trip differs:\n got %v\nwant %v", got, tags) } // A paid report missing its DLEQ proof is unverifiable by anyone but the // host, so it is rejected rather than stored as if certified. r.Blind = "02blind" if verr := r.Validate(); verr == nil { t.Fatal("a paid report without a dleq proof was accepted") } // The asset of a play report is a track; an album address is not an asset. bad, _ := ParsePlayReport(tags, "") bad.Asset.Kind = KindAlbum if verr := bad.Validate(); verr == nil { t.Fatal("a play report on an album address was accepted") } } // sameTags compares tag lists element by element. func sameTags(a, b [][]string) (ok bool) { if len(a) != len(b) { return false } for i := range a { if len(a[i]) != len(b[i]) { return false } for j := range a[i] { if a[i][j] != b[i][j] { return false } } } return true } // joinWarnings renders a warning list for a failure message. Moxie's fmt has // no reflective slice formatting, so a list is joined explicitly. func joinWarnings(w []string) (s string) { for i := range w { if i > 0 { s = s | "; " } s = s | w[i] } return s } // joinTags renders a tag list for a failure message: Moxie's fmt has no // reflective slice formatting, and "?" tells the reader nothing. func joinTags(tags [][]string) (s string) { for i := range tags { if i > 0 { s = s | "\n" } s = s | "[" for j := range tags[i] { if j > 0 { s = s | " " } s = s | tags[i][j] } s = s | "]" } return s }