music_test.mx raw

   1  package protocol
   2  
   3  import "testing"
   4  
   5  // The track example from FEATURES_MUSIC.md, tag for tag. A parse followed by a
   6  // render must reproduce this list exactly: the struct order IS the example
   7  // order, so a field added in the wrong place shows up here.
   8  func docTrackTags() (tags [][]string) {
   9  	tags = appendKV(tags, TagD, "PT-ABC-26-00001")
  10  	tags = appendKV(tags, TagTitle, "Song Title")
  11  	tags = appendKV(tags, TagArtist, "Band Name")
  12  	tags = appendTag(tags, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag())
  13  	tags = appendTag(tags, (&Address{Kind: KindAlbum, Pubkey: ownerPub, D: "album-d", Relay: "wss://relay.musiquay.com"}).Tag())
  14  	tags = appendKV(tags, TagAlbum, "Album Name")
  15  	tags = appendKV(tags, TagTrack, "5/12")
  16  	tags = appendKV(tags, TagDisc, "1/2")
  17  	tags = appendKV(tags, TagDuration, "247.32")
  18  	tags = appendKV(tags, TagGenre, "post-punk")
  19  	tags = appendKV(tags, TagT, "postpunk")
  20  	tags = appendKV(tags, TagReleaseDate, "2026-06-15")
  21  	tags = appendKV(tags, TagISRC, "PT-ABC-26-00001")
  22  	tags = appendKV(tags, TagLanguage, "en")
  23  	tags = appendKV(tags, TagBPM, "138")
  24  	tags = appendKV(tags, TagKey, "Am")
  25  	tags = appendKV(tags, TagCopyright, "CC BY-NC-SA 4.0")
  26  	tags = appendTag(tags, (&Price{Amount: "500", Unit: "sat"}).Tag())
  27  	tags = appendTag(tags, (&BlobRef{Sha256: coverHash, Server: "https://blossom.musiquay.com"}).Tag())
  28  	tags = appendKV(tags, TagI, "musicbrainz:recording:8be5c8c0-7b52-4a2d-9d4e-1c2f3a4b5c6d")
  29  	tags = appendKV(tags, TagK, "musicbrainz:recording")
  30  	tags = appendTag(tags, (&Encoding{Sha256: flacHash, Spec: "flac-24bit-96kHz", Server: "https://blossom.musiquay.com", MIME: "audio/flac", Size: 48216387, HasSize: true}).Tag())
  31  	tags = appendTag(tags, (&Encoding{Sha256: mp3Hash, Spec: "mp3-128kbps-VBR", Server: "https://blossom.musiquay.com", MIME: "audio/mpeg", Size: 9110334, HasSize: true}).Tag())
  32  	tags = appendTag(tags, (&Encoding{Sha256: opusHash, Spec: "opus-96kbps", Server: "https://blossom.musiquay.com", MIME: "audio/ogg", Size: 3942210, HasSize: true}).Tag())
  33  	tags = appendTag(tags, (&Encoding{Sha256: flacHash}).XTag())
  34  	tags = appendTag(tags, (&Encoding{Sha256: mp3Hash}).XTag())
  35  	tags = appendTag(tags, (&Encoding{Sha256: opusHash}).XTag())
  36  	tags = appendKV(tags, TagMaster, flacHash)
  37  	return
  38  }
  39  
  40  const (
  41  	artistPub = "3333333333333333333333333333333333333333333333333333333333333333"
  42  	coverHash = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
  43  	flacHash  = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
  44  	mp3Hash   = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
  45  	opusHash  = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
  46  )
  47  
  48  func TestTrackRoundTrip(t *testing.T) {
  49  	want := docTrackTags()
  50  	tr, err := ParseTrack(want, "Liner notes.")
  51  	if err != nil {
  52  		t.Fatalf("parse: %s", err.Error())
  53  	}
  54  	if verr := tr.Validate(); verr != nil {
  55  		t.Fatalf("validate: %s", verr.Error())
  56  	}
  57  	if w := tr.Warnings(); len(w) != 0 {
  58  		t.Fatalf("warnings on a complete track: %s", joinWarnings(w))
  59  	}
  60  	got := tr.Tags()
  61  	if !sameTags(got, want) {
  62  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
  63  	}
  64  	if tr.Content != "Liner notes." {
  65  		t.Fatalf("content = %q", tr.Content)
  66  	}
  67  	if tr.Album == nil || tr.Album.Kind != KindAlbum || tr.Album.Relay != "wss://relay.musiquay.com" {
  68  		t.Fatalf("album reference = %v", tr.Album)
  69  	}
  70  	if len(tr.Encodings) != 3 {
  71  		t.Fatalf("encodings = %d, want 3", len(tr.Encodings))
  72  	}
  73  	if tr.Encodings[0].Size != 48216387 || !tr.Encodings[0].HasSize {
  74  		t.Fatalf("first encoding = %+v", tr.Encodings[0])
  75  	}
  76  	if tr.Master != flacHash {
  77  		t.Fatalf("master = %q", tr.Master)
  78  	}
  79  	if len(tr.External) != 1 || tr.External[0].I != "musicbrainz:recording:8be5c8c0-7b52-4a2d-9d4e-1c2f3a4b5c6d" || tr.External[0].K != "musicbrainz:recording" {
  80  		t.Fatalf("external ids = %v", tr.External)
  81  	}
  82  	if tr.Duration != "247.32" || tr.BPM != "138" || tr.ISRC != "PT-ABC-26-00001" {
  83  		t.Fatalf("numeric-ish fields: %+v", tr)
  84  	}
  85  }
  86  
  87  // The d tag SHOULD be the ISRC, and the address is what everything references.
  88  func TestTrackAddress(t *testing.T) {
  89  	tr, err := ParseTrack(docTrackTags(), "")
  90  	if err != nil {
  91  		t.Fatal(err.Error())
  92  	}
  93  	a := &Address{Kind: KindTrack, Pubkey: ownerPub, D: tr.D}
  94  	if a.String() != "32210:"|ownerPub|":PT-ABC-26-00001" {
  95  		t.Fatalf("address = %q", a.String())
  96  	}
  97  }
  98  
  99  func TestTrackValidateRejects(t *testing.T) {
 100  	// A master that is not one of the encodings.
 101  	tr, err := ParseTrack(docTrackTags(), "")
 102  	if err != nil {
 103  		t.Fatal(err.Error())
 104  	}
 105  	tr.Master = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
 106  	if verr := tr.Validate(); verr == nil {
 107  		t.Fatal("a master outside the encodings was accepted")
 108  	}
 109  	// A bad codec spec on an encoding.
 110  	tr2, _ := ParseTrack(docTrackTags(), "")
 111  	tr2.Encodings[0].Spec = "flac-"
 112  	if verr := tr2.Validate(); verr == nil {
 113  		t.Fatal("a malformed codec spec was accepted")
 114  	}
 115  	// A short blob hash.
 116  	tr3, _ := ParseTrack(docTrackTags(), "")
 117  	tr3.Encodings[0].Sha256 = "abcd"
 118  	if verr := tr3.Validate(); verr == nil {
 119  		t.Fatal("a short blob hash was accepted")
 120  	}
 121  	// An album reference of the wrong kind.
 122  	tr4, _ := ParseTrack(docTrackTags(), "")
 123  	tr4.Album.Kind = KindPlaylist
 124  	if verr := tr4.Validate(); verr == nil {
 125  		t.Fatal("an album reference of the wrong kind was accepted")
 126  	}
 127  	// No d tag at all: the address is the identity.
 128  	tr5 := &Track{Title: "x"}
 129  	if verr := tr5.Validate(); verr == nil {
 130  		t.Fatal("a track with no d tag was accepted")
 131  	}
 132  }
 133  
 134  func TestTrackWarnings(t *testing.T) {
 135  	tr := &Track{D: "x", Title: "T"}
 136  	tr.Encodings = push(tr.Encodings, Encoding{Sha256: flacHash, Spec: "flac-16bit-44.1kHz"})
 137  	w := tr.Warnings()
 138  	if len(w) < 2 {
 139  		t.Fatalf("warnings = %s", joinWarnings(w))
 140  	}
 141  	tr.Participants = push(tr.Participants, Participant{Pubkey: ownerPub, Role: "wizard"})
 142  	found := false
 143  	for _, s := range tr.Warnings() {
 144  		if s == "unknown role: wizard" {
 145  			found = true
 146  		}
 147  	}
 148  	if !found {
 149  		t.Fatal("an unknown role did not warn")
 150  	}
 151  }
 152  
 153  // Unknown tags survive a parse and are re-emitted: an older build must not
 154  // strip a newer field off an event it stores or forwards.
 155  func TestTrackKeepsUnknownTags(t *testing.T) {
 156  	tags := appendTags(docTrackTags(), [][]string{
 157  		{"vibe", "shoegaze"},
 158  		{"z", "future"},
 159  	})
 160  	tr, err := ParseTrack(tags, "")
 161  	if err != nil {
 162  		t.Fatal(err.Error())
 163  	}
 164  	if len(tr.Extra) != 2 {
 165  		t.Fatalf("extra = %s", joinTags(tr.Extra))
 166  	}
 167  	got := tr.Tags()
 168  	if len(got) != len(tags) {
 169  		t.Fatalf("round trip lost tags: %d vs %d", len(got), len(tags))
 170  	}
 171  	if got[len(got)-2][0] != "vibe" || got[len(got)-1][0] != "z" {
 172  		t.Fatalf("extra tags not re-emitted: %s", joinTags(got))
 173  	}
 174  }
 175  
 176  // The album example from FEATURES_MUSIC.md: the ordered a tags are the
 177  // tracklist, and the order must survive.
 178  func TestAlbumRoundTrip(t *testing.T) {
 179  	var want [][]string
 180  	want = appendKV(want, TagD, "album-d")
 181  	want = appendKV(want, TagTitle, "Album Name")
 182  	want = appendKV(want, TagArtist, "Band Name")
 183  	want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag())
 184  	want = appendTag(want, (&Participant{Pubkey: ownerPub, Relay: "wss://relay.musiquay.com", Role: "label"}).Tag())
 185  	want = appendKV(want, TagReleaseDate, "2026-06-15")
 186  	want = appendKV(want, TagGenre, "post-punk")
 187  	want = appendKV(want, TagT, "postpunk")
 188  	want = appendKV(want, TagLanguage, "en")
 189  	want = appendKV(want, TagCopyright, "CC BY-NC-SA 4.0")
 190  	want = appendTag(want, (&Price{Amount: "5000", Unit: "sat"}).Tag())
 191  	want = appendTag(want, (&BlobRef{Sha256: coverHash, Server: "https://blossom.musiquay.com"}).Tag())
 192  	want = appendKV(want, TagI, "musicbrainz:release-group:5c9e6d6c-2c2e-4a7b-9c3f-3a4b5c6d7e8f")
 193  	want = appendKV(want, TagK, "musicbrainz:release-group")
 194  	for i := 1; i <= 3; i++ {
 195  		want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d-" | intStr(int64(i)), Relay: "wss://relay.musiquay.com"}).Tag())
 196  	}
 197  	al, err := ParseAlbum(want, "Release notes.")
 198  	if err != nil {
 199  		t.Fatalf("parse: %s", err.Error())
 200  	}
 201  	if verr := al.Validate(); verr != nil {
 202  		t.Fatalf("validate: %s", verr.Error())
 203  	}
 204  	if len(al.Tracks) != 3 || al.Tracks[2].D != "track-d-3" {
 205  		t.Fatalf("tracklist = %s", joinTags(al.Tags()))
 206  	}
 207  	if got := al.Tags(); !sameTags(got, want) {
 208  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 209  	}
 210  	// A tracklist entry of the wrong kind is rejected.
 211  	al.Tracks[1].Kind = KindAlbum
 212  	if verr := al.Validate(); verr == nil {
 213  		t.Fatal("a non-track address in a tracklist was accepted")
 214  	}
 215  }
 216  
 217  // The playlist example: NIP-51 set conventions, members as ordered a tags.
 218  func TestPlaylistRoundTrip(t *testing.T) {
 219  	var want [][]string
 220  	want = appendKV(want, TagD, "my-favourite-deep-cuts")
 221  	want = appendKV(want, TagTitle, "My Favourite Deep Cuts")
 222  	want = appendKV(want, TagDescription, "Tracks that never got the attention they deserved.")
 223  	want = appendTag(want, (&BlobRef{Sha256: coverHash, Server: "https://blossom.musiquay.com"}).Tag())
 224  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d-1", Relay: "wss://relay.musiquay.com"}).Tag())
 225  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: artistPub, D: "track-d-2", Relay: "wss://relay.musiquay.com"}).Tag())
 226  	pl, err := ParsePlaylist(want, "")
 227  	if err != nil {
 228  		t.Fatalf("parse: %s", err.Error())
 229  	}
 230  	if verr := pl.Validate(); verr != nil {
 231  		t.Fatalf("validate: %s", verr.Error())
 232  	}
 233  	if pl.IsPrivate() {
 234  		t.Fatal("a public playlist reported as private")
 235  	}
 236  	if got := pl.Tags(); !sameTags(got, want) {
 237  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 238  	}
 239  	// A private playlist has no member tags: the members are in content.
 240  	priv := &Playlist{D: "secret", Title: "Secret", Content: "nip44-ciphertext"}
 241  	if !priv.IsPrivate() {
 242  		t.Fatal("a playlist with members in content is private")
 243  	}
 244  	if verr := priv.Validate(); verr != nil {
 245  		t.Fatalf("validate private: %s", verr.Error())
 246  	}
 247  }
 248  
 249  // The paid play report from FEATURES_MUSIC.md, and the statement its blind
 250  // signature covers.
 251  func TestPlayReportPaidRoundTrip(t *testing.T) {
 252  	var want [][]string
 253  	want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d", Relay: "wss://relay.musiquay.com"}).Tag())
 254  	want = appendKV(want, TagP, artistPub)
 255  	want = appendKV(want, TagX, mp3Hash)
 256  	want = appendKV(want, TagServer, "https://blossom.musiquay.com")
 257  	want = appendKV(want, TagCodec, "mp3-128kbps-VBR")
 258  	want = appendKV(want, TagT0, "1758130000")
 259  	want = appendKV(want, TagT1, "1758130247")
 260  	want = appendKV(want, TagBlind, "02blind")
 261  	want = appendKV(want, TagHost, ownerPub)
 262  	want = appendKV(want, TagStatement, "73746174656d656e74")
 263  	d := []string{:3}
 264  	d[0] = TagDleq
 265  	d[1] = "02e"
 266  	d[2] = "02s"
 267  	want = appendTag(want, d)
 268  
 269  	r, err := ParsePlayReport(want, "")
 270  	if err != nil {
 271  		t.Fatalf("parse: %s", err.Error())
 272  	}
 273  	if verr := r.Validate(); verr != nil {
 274  		t.Fatalf("validate: %s", verr.Error())
 275  	}
 276  	if !r.IsPaid() {
 277  		t.Fatal("a report with host proof is not marked paid")
 278  	}
 279  	if got := r.Tags(); !sameTags(got, want) {
 280  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 281  	}
 282  	if r.Dleq == nil || r.Dleq.E != "02e" || r.Dleq.S != "02s" {
 283  		t.Fatalf("dleq = %v", r.Dleq)
 284  	}
 285  	stmt := PlayStatement(r.Asset.String(), r.Blob, r.T0, r.T1)
 286  	if stmt != "musiquay.play:v1|32210:"|ownerPub|":track-d|"|mp3Hash|"|1758130000|1758130247" {
 287  		t.Fatalf("statement = %q", stmt)
 288  	}
 289  }
 290  
 291  func TestPlayReportFreeIsUnproven(t *testing.T) {
 292  	var tags [][]string
 293  	tags = appendTag(tags, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag())
 294  	tags = appendKV(tags, TagP, artistPub)
 295  	tags = appendKV(tags, TagX, mp3Hash)
 296  	tags = appendKV(tags, TagServer, "https://blossom.musiquay.com")
 297  	tags = appendKV(tags, TagCodec, "opus-96kbps")
 298  	tags = appendKV(tags, TagT0, "1758130000")
 299  	tags = appendKV(tags, TagT1, "1758130247")
 300  	r, err := ParsePlayReport(tags, "")
 301  	if err != nil {
 302  		t.Fatal(err.Error())
 303  	}
 304  	if r.IsPaid() {
 305  		t.Fatal("a free play reported as paid")
 306  	}
 307  	if verr := r.Validate(); verr != nil {
 308  		t.Fatalf("validate: %s", verr.Error())
 309  	}
 310  	if got := r.Tags(); !sameTags(got, tags) {
 311  		t.Fatalf("round trip differs:\n got %v\nwant %v", got, tags)
 312  	}
 313  	// A paid report missing its DLEQ proof is unverifiable by anyone but the
 314  	// host, so it is rejected rather than stored as if certified.
 315  	r.Blind = "02blind"
 316  	if verr := r.Validate(); verr == nil {
 317  		t.Fatal("a paid report without a dleq proof was accepted")
 318  	}
 319  	// The asset of a play report is a track; an album address is not an asset.
 320  	bad, _ := ParsePlayReport(tags, "")
 321  	bad.Asset.Kind = KindAlbum
 322  	if verr := bad.Validate(); verr == nil {
 323  		t.Fatal("a play report on an album address was accepted")
 324  	}
 325  }
 326  
 327  // sameTags compares tag lists element by element.
 328  func sameTags(a, b [][]string) (ok bool) {
 329  	if len(a) != len(b) {
 330  		return false
 331  	}
 332  	for i := range a {
 333  		if len(a[i]) != len(b[i]) {
 334  			return false
 335  		}
 336  		for j := range a[i] {
 337  			if a[i][j] != b[i][j] {
 338  				return false
 339  			}
 340  		}
 341  	}
 342  	return true
 343  }
 344  
 345  // joinWarnings renders a warning list for a failure message. Moxie's fmt has
 346  // no reflective slice formatting, so a list is joined explicitly.
 347  func joinWarnings(w []string) (s string) {
 348  	for i := range w {
 349  		if i > 0 {
 350  			s = s | "; "
 351  		}
 352  		s = s | w[i]
 353  	}
 354  	return s
 355  }
 356  
 357  // joinTags renders a tag list for a failure message: Moxie's fmt has no
 358  // reflective slice formatting, and "?" tells the reader nothing.
 359  func joinTags(tags [][]string) (s string) {
 360  	for i := range tags {
 361  		if i > 0 {
 362  			s = s | "\n"
 363  		}
 364  		s = s | "["
 365  		for j := range tags[i] {
 366  			if j > 0 {
 367  				s = s | " "
 368  			}
 369  			s = s | tags[i][j]
 370  		}
 371  		s = s | "]"
 372  	}
 373  	return s
 374  }
 375