package protocol import "testing" const ( mintURL = "https://mint.example.com" hostPub = "9999999999999999999999999999999999999999999999999999999999999999" retailPk = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" filtHash = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" aggID = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" aggID2 = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" ) // The access filter example from FEATURES_ACCESS.md / PROTOCOL_FLOWS A1. func TestAccessFilterRoundTrip(t *testing.T) { asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"} var want [][]string want = appendKV(want, TagD, FanFilterD(asset)) want = appendKV(want, TagFilter, FilterBloom) want = appendKV(want, TagFp, "1e-9") want = appendKV(want, TagN, "1234") want = appendKV(want, TagK, "30") want = appendKV(want, TagX, filtHash) want = appendKV(want, TagServer, "https://blossom.musiquay.com") want = appendKV(want, TagMint, mintURL) want = appendKV(want, TagRetailer, retailPk) want = appendKV(want, TagExpiration, "1799999999") f, err := ParseAccessFilter(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := f.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if f.D != "fan:32210:"|ownerPub|":track-d" { t.Fatalf("d = %q", f.D) } if len(f.Mints) != 1 || f.Mints[0] != mintURL || len(f.Retailers) != 1 { t.Fatalf("mints=%v retailers=%v", f.Mints, f.Retailers) } if got := f.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } } func TestFilterTierForms(t *testing.T) { asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"} fan, err := ParseFilterD(FanFilterD(asset)) if err != nil { t.Fatalf("fan: %s", err.Error()) } if fan.Tier != TierFan || fan.Asset == nil || fan.Asset.D != "track-d" { t.Fatalf("fan tier = %+v", fan) } if fan.String() != FanFilterD(asset) { t.Fatalf("fan d round trip = %q", fan.String()) } retail, err := ParseFilterD(RetailFilterD(asset)) if err != nil { t.Fatalf("retail: %s", err.Error()) } if retail.Tier != TierRetail || retail.Asset == nil { t.Fatalf("retail tier = %+v", retail) } subD := SubFilterD(retailPk, "channel-one") sub, err := ParseFilterD(subD) if err != nil { t.Fatalf("sub: %s", err.Error()) } if sub.Tier != TierSub || sub.Retailer != retailPk || sub.Channel != "channel-one" { t.Fatalf("sub tier = %+v", sub) } if sub.String() != subD { t.Fatalf("sub d round trip = %q", sub.String()) } for _, bad := range []string{ "", "fan", "owner:" | ownerPub | ":track-d", "fan:32210:short:track-d", "sub:" | retailPk, "sub:short:chan", "sub:" | retailPk | ":", } { if _, berr := ParseFilterD(bad); berr == nil { t.Errorf("ParseFilterD(%q) accepted a bad value", bad) } } } // A subscription filter with no catalog coverage is what the spec leaves open: // it warns rather than failing, and the open decision is visible. func TestSubscriptionFilterCatalog(t *testing.T) { asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"} f := &AccessFilter{ D: SubFilterD(retailPk, "chan"), Filter: FilterBloom, Fp: "1e-9", N: "10", K: "30", X: filtHash, } if verr := f.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } w := f.Warnings() found := false for i := range w { if w[i] == "subscription filter with no catalog coverage" { found = true } } if !found { t.Fatalf("warnings = %s", joinWarnings(w)) } f.Catalog = push(f.Catalog, asset.String()) tags := f.Tags() if len(tags) != 7 { t.Fatalf("tag count with catalog = %d", len(tags)) } var hasCatalog bool = false for i := range tags { if tags[i][0] == TagCatalog && tags[i][1] == asset.String() { hasCatalog = true } } if !hasCatalog { t.Fatal("catalog coverage was not emitted") } } func TestAccessFilterValidateRejects(t *testing.T) { asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"} base := func() (f *AccessFilter) { return &AccessFilter{ D: FanFilterD(asset), Filter: FilterBloom, Fp: "1e-9", N: "1", K: "30", X: filtHash, } } f := base() f.X = "nothash" if verr := f.Validate(); verr == nil { t.Fatal("a bad blob hash was accepted") } f2 := base() f2.Filter = "cuckoo" if verr := f2.Validate(); verr == nil { t.Fatal("an unknown filter structure was accepted") } f3 := base() f3.Fp = "" if verr := f3.Validate(); verr == nil { t.Fatal("a filter with no rate was accepted") } f4 := base() f4.Retailers = push(f4.Retailers, "short") if verr := f4.Validate(); verr == nil { t.Fatal("a short retailer pubkey was accepted") } f5 := base() f5.D = "owner:" | ownerPub | ":" | asset.String() if verr := f5.Validate(); verr == nil { t.Fatal("a filter with a bad tier was accepted") } } // The purchase order from PROTOCOL_FLOWS A4. func TestPurchaseOrderRoundTrip(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, ownerPub) want = appendKV(want, TagTier, TierFan) want = appendTag(want, (&Price{Amount: "500", Unit: "sat"}).Tag()) want = appendKV(want, TagPayment, PaymentCashu) want = appendKV(want, TagMint, mintURL) want = appendKV(want, TagToken, "cashuBtoken") o, err := ParsePurchaseOrder(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := o.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if o.Asset == nil || o.Asset.Kind != KindTrack { t.Fatalf("asset = %v", o.Asset) } if o.Price == nil || o.Price.Amount != "500" || o.Price.Unit != "sat" { t.Fatalf("price = %v", o.Price) } if got := o.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } } func TestPurchaseOrderValidate(t *testing.T) { asset := &Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"} base := func() (o *PurchaseOrder) { return &PurchaseOrder{ Asset: asset, Seller: ownerPub, Tier: TierFan, Price: &Price{Amount: "500", Unit: "sat"}, Payment: PaymentCashu, Mint: mintURL, Token: "cashuBx", } } if verr := base().Validate(); verr != nil { t.Fatalf("valid order rejected: %s", verr.Error()) } o := base() o.Seller = "" if verr := o.Validate(); verr == nil { t.Fatal("an order with no seller was accepted") } o2 := base() o2.Payment = "" if verr := o2.Validate(); verr == nil { t.Fatal("an order with no payment leg was accepted") } o3 := base() o3.Token = "" if verr := o3.Validate(); verr == nil { t.Fatal("a cashu order with no token was accepted") } o4 := base() o4.Tier = TierSub if verr := o4.Validate(); verr == nil { t.Fatal("a subscription order with no channel or period was accepted") } o4.Channel = "chan" o4.Period = "2592000" if verr := o4.Validate(); verr != nil { t.Fatalf("a complete subscription order was rejected: %s", verr.Error()) } o5 := base() o5.Payment = PaymentLightning o5.Token = "" o5.Mint = "" if verr := o5.Validate(); verr != nil { t.Fatalf("a lightning order was rejected: %s", verr.Error()) } // The order is private: it must never be published to a relay. if !IsPrivate(KindPurchaseOrder) { t.Fatal("the order kind is not private") } } // The delivery receipt from PROTOCOL_FLOWS D1. func TestDeliveryReceiptRoundTrip(t *testing.T) { var want [][]string want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag()) want = appendKV(want, TagP, buyerPub) want = appendKV(want, TagX, mp3Hash) want = appendKV(want, TagServer, "https://blossom.musiquay.com") want = appendKV(want, TagMode, ModeDownload) want = appendKV(want, TagStart, "1758130000") want = appendKV(want, TagEnd, "1758130000") want = appendKV(want, TagBytes, "48216387") want = appendKV(want, TagAmountSats, "0") want = appendKV(want, TagPaid, "tokenhash") r, err := ParseDeliveryReceipt(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := r.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if r.Mode != ModeDownload || r.AmountSats != "0" || r.TokenHashesUsed() != 1 { t.Fatalf("receipt = %+v", r) } if got := r.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } // A stream receipt with a settled amount. r2 := &DeliveryReceipt{ Asset: r.Asset, Buyer: buyerPub, Blob: mp3Hash, Mode: ModeStream, Start: "1", End: "601", Bytes: "100", AmountSats: "10", } if verr := r2.Validate(); verr != nil { t.Fatalf("stream receipt rejected: %s", verr.Error()) } r3 := &DeliveryReceipt{Asset: r.Asset, Buyer: buyerPub, Blob: mp3Hash, Mode: "burn", AmountSats: "0"} if verr := r3.Validate(); verr == nil { t.Fatal("an unknown mode was accepted") } } // The host aggregate from PROTOCOL_FLOWS D2: listeners is marked approximate. func TestHostAggregateRoundTrip(t *testing.T) { var want [][]string want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag()) want = appendKV(want, TagStart, "1758130000") want = appendKV(want, TagEnd, "1760808400") want = appendKV(want, TagPlays, "48213") want = appendKV(want, TagDownloads, "3921") want = appendKV(want, TagBytes, "21474836480") want = appendKV(want, TagSats, "1510000") lst := []string{:3} lst[0] = TagListeners lst[1] = "11054" lst[2] = Approx want = appendTag(want, lst) a, err := ParseHostAggregate(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := a.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if !a.Approx || a.Listeners != "11054" { t.Fatalf("aggregate = %+v", a) } if got := a.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } // A bare listener count reads as exact and is rejected. a2 := &HostAggregate{Asset: a.Asset, Start: "1", End: "2", Listeners: "10"} if verr := a2.Validate(); verr == nil { t.Fatal("an unmarked listener count was accepted") } } // The publisher rollup from PROTOCOL_FLOWS D3: the e tags carry the host // pubkey at index 3 so the authenticity chain is verifiable. func TestPublisherRollupRoundTrip(t *testing.T) { var want [][]string want = appendTag(want, (&Address{Kind: KindTrack, Pubkey: ownerPub, D: "track-d"}).Tag()) want = appendKV(want, TagStart, "1758130000") want = appendKV(want, TagEnd, "1760808400") want = appendTag(want, (&AggregateRef{ID: aggID, Host: hostPub}).Tag()) want = appendTag(want, (&AggregateRef{ID: aggID2, Host: retailPk}).Tag()) want = appendKV(want, TagReceipts, "52134") want = appendKV(want, TagPlays, "55123") want = appendKV(want, TagDownloads, "4120") want = appendKV(want, TagBytes, "22817013760") want = appendKV(want, TagSats, "1730000") r, err := ParsePublisherRollup(want, "") if err != nil { t.Fatalf("parse: %s", err.Error()) } if verr := r.Validate(); verr != nil { t.Fatalf("validate: %s", verr.Error()) } if len(r.Aggregates) != 2 || r.Aggregates[0].Host != hostPub { t.Fatalf("aggregates = %v", r.Aggregates) } if ref := (&AggregateRef{ID: aggID, Host: hostPub}).Tag(); len(ref) != 4 || ref[2] != "" { t.Fatalf("aggregate ref tag = %s", joinTags([][]string{ref})) } if got := r.Tags(); !sameTags(got, want) { t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want)) } r2 := &PublisherRollup{Asset: r.Asset, Start: "1", End: "2"} r2.Aggregates = push(r2.Aggregates, AggregateRef{ID: "short", Host: hostPub}) if verr := r2.Validate(); verr == nil { t.Fatal("a short aggregate id was accepted") } }