commerce_test.mx raw

   1  package protocol
   2  
   3  import "testing"
   4  
   5  // The vinyl product from FEATURES_MERCHANDISE.md, verbatim as content JSON.
   6  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}]}`
   7  
   8  func TestParseProduct(t *testing.T) {
   9  	p, err := ParseProduct(docProductJSON)
  10  	if err != nil {
  11  		t.Fatalf("parse: %s", err.Error())
  12  	}
  13  	if verr := p.Validate(); verr != nil {
  14  		t.Fatalf("validate: %s", verr.Error())
  15  	}
  16  	if p.ID != "album-vinyl-first-press" || p.StallID != "band-name-releases" {
  17  		t.Fatalf("ids = %q %q", p.ID, p.StallID)
  18  	}
  19  	if p.Currency != "EUR" || p.Price != "28.00" || p.Quantity != "200" {
  20  		t.Fatalf("money fields: currency=%q price=%q quantity=%q", p.Currency, p.Price, p.Quantity)
  21  	}
  22  	if len(p.Images) != 1 || p.Images[0] != "https://blossom.musiquay.com/hash" {
  23  		t.Fatalf("images = %s", joinWarnings(p.Images))
  24  	}
  25  	if len(p.Specs) != 7 || p.Specs[0].Key != "format" || p.Specs[0].Value != "vinyl" {
  26  		t.Fatalf("specs = %d", len(p.Specs))
  27  	}
  28  	if len(p.Shipping) != 2 || p.Shipping[0].ID != "eu" || p.Shipping[0].Cost != "6.00" {
  29  		t.Fatalf("shipping = %d", len(p.Shipping))
  30  	}
  31  	// Physical music carries its provenance inside the specs, as two a keys.
  32  	refs := p.EditionRefs()
  33  	if len(refs) != 2 {
  34  		t.Fatalf("edition refs = %d", len(refs))
  35  	}
  36  	if refs[0].Kind != KindEdition || refs[0].D != "edition-d" {
  37  		t.Fatalf("first ref = %v", refs[0])
  38  	}
  39  	if refs[1].Kind != KindAlbum || refs[1].D != "album-d" {
  40  		t.Fatalf("second ref = %v", refs[1])
  41  	}
  42  	if p.Content != docProductJSON {
  43  		t.Fatal("the raw content was not kept")
  44  	}
  45  }
  46  
  47  // The record-shop stock item: a used copy with condition specs.
  48  func TestParseProductStockItem(t *testing.T) {
  49  	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}]}`
  50  	p, err := ParseProduct(stock)
  51  	if err != nil {
  52  		t.Fatalf("parse: %s", err.Error())
  53  	}
  54  	if verr := p.Validate(); verr != nil {
  55  		t.Fatalf("validate: %s", verr.Error())
  56  	}
  57  	if len(p.Specs) != 8 {
  58  		t.Fatalf("specs = %d", len(p.Specs))
  59  	}
  60  	if p.Specs[1].Key != "condition-media" || p.Specs[1].Value != "NM" {
  61  		t.Fatalf("condition = %+v", p.Specs[1])
  62  	}
  63  	if p.Shipping[0].Cost != "0" {
  64  		t.Fatalf("zero cost = %q", p.Shipping[0].Cost)
  65  	}
  66  	if refs := p.EditionRefs(); len(refs) != 2 || refs[0].Kind != KindAlbum {
  67  		t.Fatalf("refs = %d", len(refs))
  68  	}
  69  	// Every condition value the doc lists parses as a plain spec value.
  70  	for _, c := range []string{"Mint", "NM", "VG+", "VG", "G+", "G", "Fair", "Poor"} {
  71  		prod := p.Copy()
  72  		prod.Specs[1].Value = c
  73  		if prod.Specs[1].Value != c {
  74  			t.Fatalf("condition %q did not survive the copy", c)
  75  		}
  76  	}
  77  }
  78  
  79  func TestProductJSONRoundTrip(t *testing.T) {
  80  	p, err := ParseProduct(docProductJSON)
  81  	if err != nil {
  82  		t.Fatal(err.Error())
  83  	}
  84  	rendered := p.JSON()
  85  	back, err := ParseProduct(rendered)
  86  	if err != nil {
  87  		t.Fatalf("re-parse: %s", err.Error())
  88  	}
  89  	if back.ID != p.ID || back.Name != p.Name || back.Description != p.Description {
  90  		t.Fatalf("identity changed: %+v", back)
  91  	}
  92  	if back.Price != p.Price || back.Currency != p.Currency || back.Quantity != p.Quantity {
  93  		t.Fatalf("money changed: price=%q currency=%q quantity=%q", back.Price, back.Currency, back.Quantity)
  94  	}
  95  	if len(back.Specs) != len(p.Specs) || len(back.Shipping) != len(p.Shipping) {
  96  		t.Fatalf("specs/shipping changed: %d %d", len(back.Specs), len(back.Shipping))
  97  	}
  98  	if back.Specs[5].Key != "a" || back.Specs[5].Value != p.Specs[5].Value {
  99  		t.Fatalf("edition spec changed: %+v", back.Specs[5])
 100  	}
 101  	if len(back.EditionRefs()) != 2 {
 102  		t.Fatal("the edition refs did not survive a render")
 103  	}
 104  }
 105  
 106  func TestProductValidateRejects(t *testing.T) {
 107  	p, err := ParseProduct(docProductJSON)
 108  	if err != nil {
 109  		t.Fatal(err.Error())
 110  	}
 111  	bad := p.Copy()
 112  	bad.StallID = ""
 113  	if verr := bad.Validate(); verr == nil {
 114  		t.Fatal("a product with no stall was accepted")
 115  	}
 116  	bad2 := p.Copy()
 117  	bad2.Price = ""
 118  	if verr := bad2.Validate(); verr == nil {
 119  		t.Fatal("a product with no price was accepted")
 120  	}
 121  	bad3 := p.Copy()
 122  	bad3.Specs[0].Key = ""
 123  	if verr := bad3.Validate(); verr == nil {
 124  		t.Fatal("a spec with no key was accepted")
 125  	}
 126  	if _, perr := ParseProduct("[]"); perr == nil {
 127  		t.Fatal("a non-object product content was accepted")
 128  	}
 129  	// Parsing is not validation: a parseable payload still has to satisfy the
 130  	// fields a checkout needs.
 131  	st, serr := ParseStall(`{"id":"s"}`)
 132  	if serr != nil {
 133  		t.Fatalf("parse stall: %s", serr.Error())
 134  	}
 135  	if verr := st.Validate(); verr == nil {
 136  		t.Fatal("a stall with no name passed validation")
 137  	}
 138  }
 139  
 140  func TestStallJSON(t *testing.T) {
 141  	s := &Stall{
 142  		ID: "band-name-merch", Name: "Band Name Merch", Currency: "EUR",
 143  	}
 144  	s.Shipping = push(s.Shipping, ShippingZone{ID: "eu", Cost: "5.00"})
 145  	s.Shipping = push(s.Shipping, ShippingZone{ID: "worldwide", Cost: "12.00"})
 146  	if verr := s.Validate(); verr != nil {
 147  		t.Fatalf("validate: %s", verr.Error())
 148  	}
 149  	rendered := s.JSON()
 150  	back, err := ParseStall(rendered)
 151  	if err != nil {
 152  		t.Fatalf("re-parse: %s", err.Error())
 153  	}
 154  	if back.ID != s.ID || back.Name != s.Name || back.Currency != s.Currency {
 155  		t.Fatalf("stall changed: %+v", back)
 156  	}
 157  	if len(back.Shipping) != 2 || back.Shipping[1].Cost != "12.00" {
 158  		t.Fatalf("shipping changed: %d", len(back.Shipping))
 159  	}
 160  }
 161  
 162  // The resale listing from FEATURES_MERCHANDISE.md: NIP-99 tags plus the edition
 163  // address for provenance.
 164  func TestListingRoundTrip(t *testing.T) {
 165  	var want [][]string
 166  	want = appendKV(want, TagD, "resale-album-vinyl-001")
 167  	want = appendKV(want, TagTitle, "Artist Name — Album Title (Vinyl, 1st Pressing)")
 168  	want = appendKV(want, TagSummary, "NM/VG+. Original 1st pressing with insert.")
 169  	want = appendKV(want, TagPublishedAt, "1740000000")
 170  	want = appendKV(want, TagLocation, "Porto, Portugal")
 171  	want = appendKV(want, TagG, "ez3qe")
 172  	want = appendTag(want, (&Price{Amount: "45", Unit: "EUR"}).Tag())
 173  	want = appendKV(want, TagT, "vinyl")
 174  	want = appendKV(want, TagT, "resale")
 175  	want = appendKV(want, TagT, "post-punk")
 176  	want = appendKV(want, TagT, "1st-pressing")
 177  	want = appendKV(want, TagImage, "https://blossom.musiquay.com/"+coverHash)
 178  	want = appendKV(want, TagStatus, StatusActive)
 179  	want = appendTag(want, (&Address{Kind: KindEdition, Pubkey: labelPub, D: "edition-d", Relay: "wss://relay.musiquay.com"}).Tag())
 180  
 181  	l, err := ParseListing(want, "Selling my copy.")
 182  	if err != nil {
 183  		t.Fatalf("parse: %s", err.Error())
 184  	}
 185  	if verr := l.Validate(); verr != nil {
 186  		t.Fatalf("validate: %s", verr.Error())
 187  	}
 188  	if l.Price == nil || l.Price.Amount != "45" || l.Price.Unit != "EUR" {
 189  		t.Fatalf("price = %v", l.Price)
 190  	}
 191  	if l.Edition == nil || l.Edition.Kind != KindEdition {
 192  		t.Fatalf("edition = %v", l.Edition)
 193  	}
 194  	if got := l.Tags(); !sameTags(got, want) {
 195  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 196  	}
 197  	// A provenance reference of the wrong kind is rejected.
 198  	bad := &Listing{D: "d", Title: "t"}
 199  	bad.Edition = &Address{Kind: KindTrack, Pubkey: ownerPub, D: "x"}
 200  	if verr := bad.Validate(); verr == nil {
 201  		t.Fatal("a provenance reference to a track was accepted")
 202  	}
 203  }
 204  
 205  // The service listing from FEATURES_MARKETPLACE.md: a frequency-priced price
 206  // tag and t-tag conventions.
 207  func TestServiceListingRoundTrip(t *testing.T) {
 208  	var want [][]string
 209  	want = appendKV(want, TagD, "foh-weekends")
 210  	want = appendKV(want, TagTitle, "FOH Engineer Available — Weekend Festivals, Lisbon Area")
 211  	want = appendKV(want, TagSummary, "10+ years mixing live rock/metal. Own console. Weekend availability.")
 212  	want = appendKV(want, TagPublishedAt, "1735851600")
 213  	want = appendKV(want, TagLocation, "Lisbon, Portugal")
 214  	want = appendKV(want, TagG, "eyckc")
 215  	want = appendTag(want, (&Price{Amount: "400", Unit: "EUR", Period: "day"}).Tag())
 216  	want = appendKV(want, TagT, "sound-engineer")
 217  	want = appendKV(want, TagT, "foh")
 218  	want = appendKV(want, TagT, "available")
 219  	want = appendKV(want, TagT, "festival")
 220  	want = appendKV(want, TagImage, "https://blossom.musiquay.com/"+coverHash)
 221  	want = appendKV(want, TagStatus, StatusActive)
 222  
 223  	l, err := ParseListing(want, "Full description in markdown.")
 224  	if err != nil {
 225  		t.Fatalf("parse: %s", err.Error())
 226  	}
 227  	if verr := l.Validate(); verr != nil {
 228  		t.Fatalf("validate: %s", verr.Error())
 229  	}
 230  	if l.Price == nil || l.Price.Period != "day" {
 231  		t.Fatalf("price = %v", l.Price)
 232  	}
 233  	if got := l.Tags(); !sameTags(got, want) {
 234  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 235  	}
 236  }
 237  
 238  func TestJSONHelpers(t *testing.T) {
 239  	obj := `{"a":"x","b":[1,2,{"c":"d"}],"e":null,"f":"has \" quote"}`
 240  	if v := jsonFieldString(obj, "a"); v != "x" {
 241  		t.Fatalf("a = %q", v)
 242  	}
 243  	if v := jsonFieldRaw(obj, "b"); v != `[1,2,{"c":"d"}]` {
 244  		t.Fatalf("b = %q", v)
 245  	}
 246  	if v := jsonFieldRaw(obj, "e"); v != "null" {
 247  		t.Fatalf("e = %q", v)
 248  	}
 249  	if v := jsonFieldString(obj, "f"); v != `has " quote` {
 250  		t.Fatalf("f = %q", v)
 251  	}
 252  	if v := jsonFieldString(obj, "zz"); v != "" {
 253  		t.Fatalf("missing field = %q", v)
 254  	}
 255  	els := jsonElements(`[1, [2,3], "four", {"five":5}]`)
 256  	if len(els) != 4 || els[1] != "[2,3]" || els[2] != `"four"` || els[3] != `{"five":5}` {
 257  		t.Fatalf("elements = %s", joinWarnings(els))
 258  	}
 259  	if !isJSONObject(` {"a":1} `) {
 260  		t.Fatal("a leading space broke object detection")
 261  	}
 262  	if isJSONObject("[1]") {
 263  		t.Fatal("an array was reported as an object")
 264  	}
 265  }
 266