package protocol import "testing" // The vinyl product from FEATURES_MERCHANDISE.md, verbatim as content JSON. const docProductJSON = `{"id":"album-vinyl-first-press","stall_id":"band-name-releases","name":"Album Name — Vinyl (First Pressing)","description":"180g black vinyl in gatefold sleeve. Mastered for vinyl by [tagged engineer].","images":["https://blossom.musiquay.com/hash"],"currency":"EUR","price":28.00,"quantity":200,"specs":[["format","vinyl"],["weight","180g"],["color","black"],["sleeve","gatefold"],["catalog","ABC-001"],["a","32216:` + labelPub + `:edition-d"],["a","32211:` + ownerPub + `:album-d"]],"shipping":[{"id":"eu","cost":6.00},{"id":"worldwide","cost":15.00}]}` func TestParseProduct(t *testing.T) { p, err := ParseProduct(docProductJSON) if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := p.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if p.ID != "album-vinyl-first-press" || p.StallID != "band-name-releases" { t.Fatalf("ids = %q %q", p.ID, p.StallID) } if p.Currency != "EUR" || p.Price != "28.00" || p.Quantity != "200" { t.Fatalf("money fields: currency=%q price=%q quantity=%q", p.Currency, p.Price, p.Quantity) } if len(p.Images) != 1 || p.Images[0] != "https://blossom.musiquay.com/hash" { t.Fatalf("images = %s", joinWarnings(p.Images)) } if len(p.Specs) != 7 || p.Specs[0].Key != "format" || p.Specs[0].Value != "vinyl" { t.Fatalf("specs = %d", len(p.Specs)) } if len(p.Shipping) != 2 || p.Shipping[0].ID != "eu" || p.Shipping[0].Cost != "6.00" { t.Fatalf("shipping = %d", len(p.Shipping)) } // Physical music carries its provenance inside the specs, as two a keys. refs := p.EditionRefs() if len(refs) != 2 { t.Fatalf("edition refs = %d", len(refs)) } if refs[0].Kind != KindEdition || refs[0].D != "edition-d" { t.Fatalf("first ref = %v", refs[0]) } if refs[1].Kind != KindAlbum || refs[1].D != "album-d" { t.Fatalf("second ref = %v", refs[1]) } if p.Content != docProductJSON { t.Fatal("the raw content was not kept") } } // The record-shop stock item: a used copy with condition specs. func TestParseProductStockItem(t *testing.T) { const stock = `{"id":"stock-item-12345","stall_id":"loja-do-som-vinyl","name":"Artist Name — Album Title","description":"Original 1st pressing. Sleeve VG+, media NM. Includes insert.","images":["https://blossom.musiquay.com/hash"],"currency":"EUR","price":35.00,"quantity":1,"specs":[["format","vinyl"],["condition-media","NM"],["condition-sleeve","VG+"],["pressing","1st pressing, 1987"],["catalog","SST-065"],["includes","insert"],["a","32211:` + ownerPub + `:album-d"],["a","32216:` + labelPub + `:edition-d"]],"shipping":[{"id":"local-pickup","cost":0},{"id":"pt","cost":4.00},{"id":"eu","cost":8.00}]}` p, err := ParseProduct(stock) if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := p.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if len(p.Specs) != 8 { t.Fatalf("specs = %d", len(p.Specs)) } if p.Specs[1].Key != "condition-media" || p.Specs[1].Value != "NM" { t.Fatalf("condition = %+v", p.Specs[1]) } if p.Shipping[0].Cost != "0" { t.Fatalf("zero cost = %q", p.Shipping[0].Cost) } if refs := p.EditionRefs(); len(refs) != 2 || refs[0].Kind != KindAlbum { t.Fatalf("refs = %d", len(refs)) } // Every condition value the doc lists parses as a plain spec value. for _, c := range []string{"Mint", "NM", "VG+", "VG", "G+", "G", "Fair", "Poor"} { prod := p.Copy() prod.Specs[1].Value = c if prod.Specs[1].Value != c { t.Fatalf("condition %q did not survive the copy", c) } } } func TestProductJSONRoundTrip(t *testing.T) { p, err := ParseProduct(docProductJSON) if err != nil { t.Fatal(err.Error()) } rendered := p.JSON() back, err := ParseProduct(rendered) if err != nil { t.Fatalf("re-parse: %s", err.Error()) } if back.ID != p.ID || back.Name != p.Name || back.Description != p.Description { t.Fatalf("identity changed: %+v", back) } if back.Price != p.Price || back.Currency != p.Currency || back.Quantity != p.Quantity { t.Fatalf("money changed: price=%q currency=%q quantity=%q", back.Price, back.Currency, back.Quantity) } if len(back.Specs) != len(p.Specs) || len(back.Shipping) != len(p.Shipping) { t.Fatalf("specs/shipping changed: %d %d", len(back.Specs), len(back.Shipping)) } if back.Specs[5].Key != "a" || back.Specs[5].Value != p.Specs[5].Value { t.Fatalf("edition spec changed: %+v", back.Specs[5]) } if len(back.EditionRefs()) != 2 { t.Fatal("the edition refs did not survive a render") } } func TestProductValidateRejects(t *testing.T) { p, err := ParseProduct(docProductJSON) if err != nil { t.Fatal(err.Error()) } bad := p.Copy() bad.StallID = "" if verr := bad.Validate(); verr == nil { t.Fatal("a product with no stall was accepted") } bad2 := p.Copy() bad2.Price = "" if verr := bad2.Validate(); verr == nil { t.Fatal("a product with no price was accepted") } bad3 := p.Copy() bad3.Specs[0].Key = "" if verr := bad3.Validate(); verr == nil { t.Fatal("a spec with no key was accepted") } if _, perr := ParseProduct("[]"); perr == nil { t.Fatal("a non-object product content was accepted") } // Parsing is not validation: a parseable payload still has to satisfy the // fields a checkout needs. st, serr := ParseStall(`{"id":"s"}`) if serr != nil { t.Fatalf("parse stall: %s", serr.Error()) } if verr := st.Validate(); verr == nil { t.Fatal("a stall with no name passed validation") } } func TestStallJSON(t *testing.T) { s := &Stall{ ID: "band-name-merch", Name: "Band Name Merch", Currency: "EUR", } s.Shipping = push(s.Shipping, ShippingZone{ID: "eu", Cost: "5.00"}) s.Shipping = push(s.Shipping, ShippingZone{ID: "worldwide", Cost: "12.00"}) if verr := s.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } rendered := s.JSON() back, err := ParseStall(rendered) if err != nil { t.Fatalf("re-parse: %s", err.Error()) } if back.ID != s.ID || back.Name != s.Name || back.Currency != s.Currency { t.Fatalf("stall changed: %+v", back) } if len(back.Shipping) != 2 || back.Shipping[1].Cost != "12.00" { t.Fatalf("shipping changed: %d", len(back.Shipping)) } } // The resale listing from FEATURES_MERCHANDISE.md: NIP-99 tags plus the edition // address for provenance. func TestListingRoundTrip(t *testing.T) { var want [][]string want = appendKV(want, TagD, "resale-album-vinyl-001") want = appendKV(want, TagTitle, "Artist Name — Album Title (Vinyl, 1st Pressing)") want = appendKV(want, TagSummary, "NM/VG+. Original 1st pressing with insert.") want = appendKV(want, TagPublishedAt, "1740000000") want = appendKV(want, TagLocation, "Porto, Portugal") want = appendKV(want, TagG, "ez3qe") want = appendTag(want, (&Price{Amount: "45", Unit: "EUR"}).Tag()) want = appendKV(want, TagT, "vinyl") want = appendKV(want, TagT, "resale") want = appendKV(want, TagT, "post-punk") want = appendKV(want, TagT, "1st-pressing") want = appendKV(want, TagImage, "https://blossom.musiquay.com/"+coverHash) want = appendKV(want, TagStatus, StatusActive) want = appendTag(want, (&Address{Kind: KindEdition, Pubkey: labelPub, D: "edition-d", Relay: "wss://relay.musiquay.com"}).Tag()) l, err := ParseListing(want, "Selling my copy.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := l.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if l.Price == nil || l.Price.Amount != "45" || l.Price.Unit != "EUR" { t.Fatalf("price = %v", l.Price) } if l.Edition == nil || l.Edition.Kind != KindEdition { t.Fatalf("edition = %v", l.Edition) } if got := l.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } // A provenance reference of the wrong kind is rejected. bad := &Listing{D: "d", Title: "t"} bad.Edition = &Address{Kind: KindTrack, Pubkey: ownerPub, D: "x"} if verr := bad.Validate(); verr == nil { t.Fatal("a provenance reference to a track was accepted") } } // The service listing from FEATURES_MARKETPLACE.md: a frequency-priced price // tag and t-tag conventions. func TestServiceListingRoundTrip(t *testing.T) { var want [][]string want = appendKV(want, TagD, "foh-weekends") want = appendKV(want, TagTitle, "FOH Engineer Available — Weekend Festivals, Lisbon Area") want = appendKV(want, TagSummary, "10+ years mixing live rock/metal. Own console. Weekend availability.") want = appendKV(want, TagPublishedAt, "1735851600") want = appendKV(want, TagLocation, "Lisbon, Portugal") want = appendKV(want, TagG, "eyckc") want = appendTag(want, (&Price{Amount: "400", Unit: "EUR", Period: "day"}).Tag()) want = appendKV(want, TagT, "sound-engineer") want = appendKV(want, TagT, "foh") want = appendKV(want, TagT, "available") want = appendKV(want, TagT, "festival") want = appendKV(want, TagImage, "https://blossom.musiquay.com/"+coverHash) want = appendKV(want, TagStatus, StatusActive) l, err := ParseListing(want, "Full description in markdown.") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := l.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if l.Price == nil || l.Price.Period != "day" { t.Fatalf("price = %v", l.Price) } if got := l.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } } func TestJSONHelpers(t *testing.T) { obj := `{"a":"x","b":[1,2,{"c":"d"}],"e":null,"f":"has \" quote"}` if v := jsonFieldString(obj, "a"); v != "x" { t.Fatalf("a = %q", v) } if v := jsonFieldRaw(obj, "b"); v != `[1,2,{"c":"d"}]` { t.Fatalf("b = %q", v) } if v := jsonFieldRaw(obj, "e"); v != "null" { t.Fatalf("e = %q", v) } if v := jsonFieldString(obj, "f"); v != `has " quote` { t.Fatalf("f = %q", v) } if v := jsonFieldString(obj, "zz"); v != "" { t.Fatalf("missing field = %q", v) } els := jsonElements(`[1, [2,3], "four", {"five":5}]`) if len(els) != 4 || els[1] != "[2,3]" || els[2] != `"four"` || els[3] != `{"five":5}` { t.Fatalf("elements = %s", joinWarnings(els)) } if !isJSONObject(` {"a":1} `) { t.Fatal("a leading space broke object detection") } if isJSONObject("[1]") { t.Fatal("an array was reported as an object") } }