package protocolwire import ( "testing" "git.smesh.lol/musiquay/pkg/protocol" "git.smesh.lol/nostr/pkg/core" ) // The tag list below is the wire form of the music spec's complete track // example, written out row for row and independently of protocol.Track.Tags(), // so that TagsJSON is measured against the spec rather than against the // encoder under the same package. The Tags() path is covered separately, by // TestTrackTagsReadsItsOwnRows. const ( fxArtist = "3333333333333333333333333333333333333333333333333333333333333333" fxOwner = "4444444444444444444444444444444444444444444444444444444444444444" fxCover = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" fxFlac = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" fxRelay = "wss://relay.musiquay.com" fxServer = "https://blossom.musiquay.com" ) func fixtureTags() (tags [][]string) { tags = [][]string{ []string{protocol.TagD, "PT-ABC-26-00001"}, []string{protocol.TagTitle, "Song Title"}, []string{protocol.TagArtist, "Band Name"}, []string{protocol.TagP, fxArtist, fxRelay, "performer"}, []string{protocol.TagA, "32211:" | fxOwner | ":album-d", fxRelay}, []string{protocol.TagAlbum, "Album Name"}, []string{protocol.TagTrack, "5/12"}, []string{protocol.TagDisc, "1/2"}, []string{protocol.TagDuration, "247.32"}, []string{protocol.TagGenre, "post-punk"}, []string{protocol.TagT, "postpunk"}, []string{protocol.TagReleaseDate, "2026-06-15"}, []string{protocol.TagISRC, "PT-ABC-26-00001"}, []string{protocol.TagLanguage, "en"}, []string{protocol.TagBPM, "138"}, []string{protocol.TagKey, "Am"}, []string{protocol.TagCopyright, "CC BY-NC-SA 4.0"}, []string{protocol.TagPrice, "500", "sat"}, []string{protocol.TagImage, fxCover, fxServer}, []string{protocol.TagI, "musicbrainz:recording:8be5c8c0"}, []string{protocol.TagK, "musicbrainz:recording"}, []string{protocol.TagAudio, fxFlac, "flac-24bit-96kHz", fxServer, "audio/flac", "48216387"}, []string{protocol.TagX, fxFlac}, []string{protocol.TagMaster, fxFlac}, } return } // toCoreTags builds the nostr tag list a reader receives. The core tag list is // [][]string, so each row is copied across explicitly. func toCoreTags(rows [][]string) (out [][]string) { out = [][]string{:len(rows)} for i := range rows { row := []string{:len(rows[i])} for j := range rows[i] { row[j] = rows[i][j] } out[i] = row } return } func contains(hay, needle string) (ok bool) { n := int32(len(needle)) if n == 0 { return true } for i := int32(0); i+n <= int32(len(hay)); i++ { if hay[i:i+n] == needle { return true } } return false } // Render a payload's tags and assert the JSON is an array of arrays holding // the rows the vocabulary defines. The d/title/audio rows are checked by // value, the frame by shape: a field emitted in the wrong place is a wire bug. func TestTagsJSONShape(t *testing.T) { tags := fixtureTags() j := TagsJSON(tags) if len(j) < 4 || j[0:2] != "[[" || j[len(j)-2:] != "]]" { t.Fatalf("tags JSON is not an array of arrays: %s", j) } wantRows := []string{ "[\"d\",\"PT-ABC-26-00001\"]", "[\"title\",\"Song Title\"]", "[\"artist\",\"Band Name\"]", "[\"audio\",\"" | fxFlac | "\",\"flac-24bit-96kHz\",\"" | fxServer | "\",\"audio/flac\",\"48216387\"]", "[\"x\",\"" | fxFlac | "\"]", "[\"master\",\"" | fxFlac | "\"]", } for i := range wantRows { if !contains(j, wantRows[i]) { t.Fatalf("tags JSON is missing %s: %s", wantRows[i], j) } } if !contains(j, "[\"price\",\"500\",\"sat\"]") { t.Fatalf("tags JSON lost the price row: %s", j) } } // The unsigned object must carry the exact key order every existing publisher // uses: kind, content, tags, created_at, pubkey. func TestUnsignedJSON(t *testing.T) { got := UnsignedJSON(protocol.KindTrack, "[[\"d\",\"x\"]]", "liner \"notes\"", "abc", 123) want := "{\"kind\":32210,\"content\":\"liner \\\"notes\\\"\",\"tags\":[[\"d\",\"x\"]],\"created_at\":123,\"pubkey\":\"abc\"}" if got != want { t.Fatalf("UnsignedJSON = %s, want %s", got, want) } } // An absent tag list is an empty JSON array, never an empty string: the value // sits beside the mandatory "tags" key of the unsigned event object. func TestTagsJSONEmpty(t *testing.T) { if got := TagsJSON(nil); got != "[]" { t.Fatalf("TagsJSON(nil) = %s", got) } var rows [][]string if got := TagsJSON(rows); got != "[]" { t.Fatalf("TagsJSON(empty) = %s", got) } } // A quote in a tag value must be escaped, not emitted raw and not dropped. func TestTagsJSONEscapes(t *testing.T) { rows := [][]string{[]string{"title", "a\"b"}} want := "[[\"title\",\"a\\\"b\"]]" if got := TagsJSON(rows); got != want { t.Fatalf("TagsJSON = %s, want %s", got, want) } } // Tags carried on an event and parsed back must survive the reader with every // field it keeps. func TestTrackFromCore(t *testing.T) { ev := &nostr.Event{ Kind: uint32(protocol.KindTrack), Tags: toCoreTags(fixtureTags()), Content: "Liner notes.", } got, err := TrackFromCore(ev) if err != nil { t.Fatalf("TrackFromCore: %s", err.Error()) } if got.D != "PT-ABC-26-00001" { t.Fatalf("d = %q", got.D) } if got.Title != "Song Title" { t.Fatalf("title = %q", got.Title) } if len(got.Artists) != 1 || got.Artists[0] != "Band Name" { t.Fatalf("artists = %d", len(got.Artists)) } if len(got.Genres) != 1 || got.Genres[0] != "post-punk" { t.Fatalf("genres = %d", len(got.Genres)) } if got.Album == nil || got.Album.Kind != protocol.KindAlbum || got.Album.D != "album-d" { t.Fatalf("album reference lost") } if len(got.Encodings) != 1 || got.Encodings[0].Sha256 != fxFlac || got.Encodings[0].Spec != "flac-24bit-96kHz" { t.Fatalf("encodings = %d", len(got.Encodings)) } if got.Master != fxFlac { t.Fatalf("master = %q", got.Master) } if got.Price == nil || got.Price.Amount != "500" || got.Price.Unit != "sat" { t.Fatalf("price lost") } if got.Content != "Liner notes." { t.Fatalf("content = %q", got.Content) } } // The d tag is the identity of an addressable kind; a payload without one is // rejected by Validate, and the reader must surface that, not return a track. func TestTrackFromCoreRejectsMissingD(t *testing.T) { ev := &nostr.Event{ Kind: uint32(protocol.KindTrack), Tags: [][]string{[]string{"title", "No D"}}, } _, err := TrackFromCore(ev) if err == nil { t.Fatal("a track event with no d tag passed validation") } } // A reader asked for one kind must refuse another; the caller gets no struct. func TestTrackFromCoreRejectsWrongKind(t *testing.T) { ev := &nostr.Event{Kind: uint32(protocol.KindAlbum)} v, err := TrackFromCore(ev) if err == nil { t.Fatal("an album event was accepted as a track") } if v != nil { t.Fatal("a kind mismatch returned a track") } } // Tags() is what the publish wrappers hand to TagsJSON, and it builds its rows // through several helpers inside the protocol package - the exact shape that // used to come back with every row after the first reading the last row's data // (the return-codec bug fixed in moxie 3eac4b0d: codecEncodeValue's KindSlice // shortcut skipped the element walk during a return, so rows pushed into a // caller-owned backing kept pointing at the callee's released frame). Three // rows that must differ is the assertion that catches it, and it is the reason // this test exists rather than only the independent fixture above. func TestTrackTagsReadsItsOwnRows(t *testing.T) { tr := &protocol.Track{D: "PT-ABC-26-00001", Title: "Song Title", Artists: []string{"Band Name"}} tags := tr.Tags() if len(tags) < 3 { t.Fatalf("expected at least 3 rows from Tags(), got %d", len(tags)) } if tags[0][0] != protocol.TagD || tags[0][1] != "PT-ABC-26-00001" { t.Fatalf("row 0 = %q %q", tags[0][0], tags[0][1]) } if tags[1][0] != protocol.TagTitle || tags[1][1] != "Song Title" { t.Fatalf("row 1 = %q %q", tags[1][0], tags[1][1]) } if tags[2][0] != protocol.TagArtist || tags[2][1] != "Band Name" { t.Fatalf("row 2 = %q %q (row 1 was %q %q)", tags[2][0], tags[2][1], tags[1][0], tags[1][1]) } // The rows must be distinct: an aliased list passes a first-row check and // fails here, because every row reads the last one written. if tags[1][1] == tags[2][1] { t.Fatal("row 1 and row 2 read the same data") } }