calendar_test.mx raw

   1  package protocol
   2  
   3  import "testing"
   4  
   5  const (
   6  	venuePub = "1212121212121212121212121212121212121212121212121212121212121212"
   7  	witness  = "1313131313131313131313131313131313131313131313131313131313131313"
   8  	missing  = "1414141414141414141414141414141414141414141414141414141414141414"
   9  )
  10  
  11  // The calendar example from FEATURES_CALENDAR.md.
  12  func TestCalendarEventRoundTrip(t *testing.T) {
  13  	var want [][]string
  14  	want = appendKV(want, TagD, "event-d")
  15  	want = appendKV(want, TagTitle, "Friday Night at Lux Frágil")
  16  	want = appendKV(want, TagSummary, "DJ sets from local selectors")
  17  	want = appendKV(want, TagImage, "https://blossom.musiquay.com/"+coverHash)
  18  	want = appendKV(want, TagStart, "1735851600")
  19  	want = appendKV(want, TagEnd, "1735876800")
  20  	want = appendKV(want, TagDay, "20090")
  21  	want = appendKV(want, TagStartTzid, "Europe/Lisbon")
  22  	want = appendKV(want, TagLocation, "Lux Frágil, Av. Infante Dom Henrique, Lisboa")
  23  	want = appendKV(want, TagG, "eyckch")
  24  	want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer"}).Tag())
  25  	want = appendTag(want, (&Participant{Pubkey: venuePub, Relay: "wss://relay.musiquay.com", Role: "sound-foh"}).Tag())
  26  	want = appendKV(want, TagT, "electronic")
  27  	want = appendKV(want, TagT, "dj")
  28  	want = appendTag(want, (&Price{Amount: "10", Unit: "EUR"}).Tag())
  29  	want = appendKV(want, TagR, "https://tickets.example.com/event/123")
  30  
  31  	c, err := ParseCalendarEvent(want, "Description of the event.")
  32  	if err != nil {
  33  		t.Fatalf("parse: %s", err.Error())
  34  	}
  35  	if verr := c.Validate(); verr != nil {
  36  		t.Fatalf("validate: %s", verr.Error())
  37  	}
  38  	if c.Geohash != "eyckch" || c.StartTzid != "Europe/Lisbon" {
  39  		t.Fatalf("event = %+v", c)
  40  	}
  41  	if len(c.Participants) != 2 || c.Participants[1].Role != "sound-foh" {
  42  		t.Fatalf("participants = %d", len(c.Participants))
  43  	}
  44  	if got := c.Tags(); !sameTags(got, want) {
  45  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
  46  	}
  47  }
  48  
  49  // The roster is the calendar event after the show: same fields, every
  50  // participant, roles from the roster vocabulary.
  51  func TestRosterFormRoundTrip(t *testing.T) {
  52  	var want [][]string
  53  	want = appendKV(want, TagD, "event-d")
  54  	want = appendKV(want, TagTitle, "Friday Night at Lux Frágil")
  55  	want = appendKV(want, TagStart, "1735851600")
  56  	want = appendKV(want, TagEnd, "1735876800")
  57  	want = appendKV(want, TagDay, "20090")
  58  	want = appendKV(want, TagLocation, "Lux Frágil, Av. Infante Dom Henrique, Lisboa")
  59  	want = appendKV(want, TagG, "eyckch")
  60  	want = appendTag(want, (&Participant{Pubkey: ownerPub, Relay: "wss://relay.musiquay.com", Role: "promoter"}).Tag())
  61  	want = appendTag(want, (&Participant{Pubkey: artistPub, Relay: "wss://relay.musiquay.com", Role: "performer-headliner"}).Tag())
  62  	want = appendTag(want, (&Participant{Pubkey: venuePub, Relay: "wss://relay.musiquay.com", Role: "sound-monitors"}).Tag())
  63  	want = appendTag(want, (&Participant{Pubkey: missing, Relay: "wss://relay.musiquay.com", Role: "lighting"}).Tag())
  64  	want = appendTag(want, (&Participant{Pubkey: hostPub, Relay: "wss://relay.musiquay.com", Role: "van-rental"}).Tag())
  65  
  66  	c, err := ParseCalendarEvent(want, "")
  67  	if err != nil {
  68  		t.Fatalf("parse: %s", err.Error())
  69  	}
  70  	if verr := c.Validate(); verr != nil {
  71  		t.Fatalf("validate: %s", verr.Error())
  72  	}
  73  	if len(c.Participants) != 5 {
  74  		t.Fatalf("roster = %d participants", len(c.Participants))
  75  	}
  76  	for i := range c.Participants {
  77  		if !KnownRole(c.Participants[i].Role) {
  78  			t.Fatalf("roster role %q is not in the vocabulary", c.Participants[i].Role)
  79  		}
  80  	}
  81  	if got := c.Tags(); !sameTags(got, want) {
  82  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
  83  	}
  84  	if w := c.Warnings(); len(w) != 0 {
  85  		t.Fatalf("warnings on a complete roster: %s", joinWarnings(w))
  86  	}
  87  }
  88  
  89  func TestDayDerivation(t *testing.T) {
  90  	if d := Day(1735851600); d != "20090" {
  91  		t.Fatalf("Day(1735851600) = %s, want 20090", d)
  92  	}
  93  	if d := Day(0); d != "0" {
  94  		t.Fatalf("Day(0) = %s", d)
  95  	}
  96  	days := DaysCovering(1735851600, 1736024400)
  97  	if len(days) != 3 {
  98  		t.Fatalf("DaysCovering = %s", joinWarnings(days))
  99  	}
 100  	if days[0] != "20090" || days[2] != "20092" {
 101  		t.Fatalf("DaysCovering = %s", joinWarnings(days))
 102  	}
 103  	// A single-instant event covers one day.
 104  	if one := DaysCovering(1735851600, 1735851600); len(one) != 1 {
 105  		t.Fatalf("single day = %s", joinWarnings(one))
 106  	}
 107  }
 108  
 109  func TestCalendarValidateRejects(t *testing.T) {
 110  	c := &CalendarEvent{D: "x", Start: "1", End: "2"}
 111  	if err := c.Validate(); err == nil {
 112  		t.Fatal("a time-based event with no D tag was accepted")
 113  	}
 114  	c2 := &CalendarEvent{D: "x", Days: []string{"20090"}}
 115  	c2.Participants = push(c2.Participants, Participant{Pubkey: artistPub})
 116  	if err := c2.Validate(); err == nil {
 117  		t.Fatal("a participant with no role was accepted")
 118  	}
 119  	c3 := &CalendarEvent{D: "x", Capacity: "many"}
 120  	if err := c3.Validate(); err == nil {
 121  		t.Fatal("a non-numeric capacity was accepted")
 122  	}
 123  	c4 := &CalendarEvent{}
 124  	if err := c4.Validate(); err == nil {
 125  		t.Fatal("an event with no d tag was accepted")
 126  	}
 127  }
 128  
 129  // The confirmation and attestation examples from FEATURES_ROSTER.md.
 130  func TestCreditLabelRoundTrip(t *testing.T) {
 131  	roster := &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "event-d", Relay: "wss://relay.musiquay.com"}
 132  
 133  	var conf [][]string
 134  	conf = appendTag(conf, roster.Tag())
 135  	conf = appendKV(conf, TagP, artistPub)
 136  	conf = appendKV(conf, TagL, NamespaceCredit)
 137  	conf = appendTag(conf, []string{TagLValue, LabelConfirmed, NamespaceCredit})
 138  	conf = appendKV(conf, TagRole, "sound-foh")
 139  
 140  	c, err := ParseCreditLabel(conf, "Great gig, crowd was amazing.")
 141  	if err != nil {
 142  		t.Fatalf("parse confirmation: %s", err.Error())
 143  	}
 144  	if verr := c.Validate(); verr != nil {
 145  		t.Fatalf("validate confirmation: %s", verr.Error())
 146  	}
 147  	if !c.IsConfirmation() || c.Label != LabelConfirmed || c.Role != "sound-foh" {
 148  		t.Fatalf("confirmation = %+v", c)
 149  	}
 150  	if got := c.Tags(); !sameTags(got, conf) {
 151  		t.Fatalf("confirmation round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(conf))
 152  	}
 153  
 154  	// An attestation: a witness vouching for someone else.
 155  	var att [][]string
 156  	att = appendTag(att, roster.Tag())
 157  	att = appendKV(att, TagP, missing)
 158  	att = appendKV(att, TagL, NamespaceAttest)
 159  	att = appendTag(att, []string{TagLValue, LabelAttests, NamespaceAttest})
 160  	att = appendKV(att, TagRole, "lighting")
 161  	a, err := ParseCreditLabel(att, "")
 162  	if err != nil {
 163  		t.Fatalf("parse attestation: %s", err.Error())
 164  	}
 165  	if verr := a.Validate(); verr != nil {
 166  		t.Fatalf("validate attestation: %s", verr.Error())
 167  	}
 168  	if a.IsConfirmation() {
 169  		t.Fatal("an attestation reported as a confirmation")
 170  	}
 171  	if got := a.Tags(); !sameTags(got, att) {
 172  		t.Fatalf("attestation round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(att))
 173  	}
 174  }
 175  
 176  func TestCreditLabelValidate(t *testing.T) {
 177  	roster := &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "event-d"}
 178  	base := func() (l *CreditLabel) {
 179  		return &CreditLabel{Target: roster, Subject: artistPub, Namespace: NamespaceCredit, Label: LabelCorrected, Role: "sound-monitors"}
 180  	}
 181  	if err := base().Validate(); err != nil {
 182  		t.Fatalf("a corrected label was rejected: %s", err.Error())
 183  	}
 184  	l := base()
 185  	l.Namespace = NamespaceAttest
 186  	if err := l.Validate(); err == nil {
 187  		t.Fatal("an attest namespace with a credit label was accepted")
 188  	}
 189  	l2 := base()
 190  	l2.Label = "maybe"
 191  	if err := l2.Validate(); err == nil {
 192  		t.Fatal("an unknown confirmation label was accepted")
 193  	}
 194  	l3 := base()
 195  	l3.Role = ""
 196  	if err := l3.Validate(); err == nil {
 197  		t.Fatal("a label with no role was accepted")
 198  	}
 199  	l4 := base()
 200  	l4.Target = &Address{Kind: KindProduct, Pubkey: ownerPub, D: "p"}
 201  	if err := l4.Validate(); err == nil {
 202  		t.Fatal("a label on an unrelated kind was accepted")
 203  	}
 204  	l5 := base()
 205  	l5.Subject = "short"
 206  	if err := l5.Validate(); err == nil {
 207  		t.Fatal("a short subject pubkey was accepted")
 208  	}
 209  }
 210  
 211  // The contribution example: root and parent references, plus the missing
 212  // participant and the suggested role.
 213  func TestContributionRoundTrip(t *testing.T) {
 214  	roster := &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "event-d", Relay: "wss://relay.musiquay.com"}
 215  	var want [][]string
 216  	root := roster.Tag()
 217  	root[0] = TagARoot
 218  	want = appendTag(want, root)
 219  	want = appendKV(want, TagKRoot, "31923")
 220  	want = appendKV(want, TagPRoot, ownerPub)
 221  	want = appendTag(want, roster.Tag())
 222  	want = appendKV(want, TagE, "roster-event-id")
 223  	want = appendKV(want, TagK, "31923")
 224  	want = appendKV(want, TagP, ownerPub)
 225  	want = appendTag(want, (&Participant{Pubkey: missing, Relay: "wss://relay.musiquay.com"}).Tag())
 226  	want = appendKV(want, TagRole, "lighting")
 227  
 228  	c, err := ParseContribution(want, "Adding the lighting tech.")
 229  	if err != nil {
 230  		t.Fatalf("parse: %s", err.Error())
 231  	}
 232  	if verr := c.Validate(); verr != nil {
 233  		t.Fatalf("validate: %s", verr.Error())
 234  	}
 235  	if c.RootAddress == nil || c.ParentAddress == nil {
 236  		t.Fatalf("root=%v parent=%v", c.RootAddress, c.ParentAddress)
 237  	}
 238  	if c.Creator != ownerPub || c.Subject.Pubkey != missing || c.Subject.Relay != "wss://relay.musiquay.com" {
 239  		t.Fatalf("creator=%q subject=%+v", c.Creator, c.Subject)
 240  	}
 241  	if c.Role != "lighting" {
 242  		t.Fatalf("role = %q", c.Role)
 243  	}
 244  	if got := c.Tags(); !sameTags(got, want) {
 245  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 246  	}
 247  	if !IsRootTag(TagARoot) || !IsRootTag(TagKRoot) || IsRootTag(TagA) {
 248  		t.Fatal("root tag detection is wrong")
 249  	}
 250  }
 251  
 252  func TestContributionValidate(t *testing.T) {
 253  	roster := &Address{Kind: KindCalendar, Pubkey: ownerPub, D: "event-d"}
 254  	base := func() (c *Contribution) {
 255  		return &Contribution{
 256  			RootAddress: roster, ParentAddress: roster,
 257  			Creator: ownerPub, Subject: Participant{Pubkey: missing}, Role: "lighting",
 258  		}
 259  	}
 260  	if err := base().Validate(); err != nil {
 261  		t.Fatalf("valid contribution rejected: %s", err.Error())
 262  	}
 263  	c := base()
 264  	c.RootAddress = nil
 265  	if err := c.Validate(); err == nil {
 266  		t.Fatal("a contribution with no root was accepted")
 267  	}
 268  	c2 := base()
 269  	c2.Role = ""
 270  	if err := c2.Validate(); err == nil {
 271  		t.Fatal("a contribution with no role was accepted")
 272  	}
 273  	c3 := base()
 274  	c3.Subject.Pubkey = "short"
 275  	if err := c3.Validate(); err == nil {
 276  		t.Fatal("a short subject pubkey was accepted")
 277  	}
 278  }
 279  
 280  // The RSVP and waitlist examples from FEATURES_CALENDAR.md.
 281  func TestRSVPRoundTrip(t *testing.T) {
 282  	event := &Address{Kind: KindCalendar, Pubkey: venuePub, D: "event-d", Relay: "wss://relay.musiquay.com"}
 283  
 284  	var want [][]string
 285  	want = appendTag(want, event.Tag())
 286  	want = appendKV(want, TagD, "rsvp-d")
 287  	want = appendKV(want, TagStatus, StatusAccepted)
 288  	want = appendKV(want, TagFb, FbBusy)
 289  	r, err := ParseRSVP(want, "")
 290  	if err != nil {
 291  		t.Fatalf("parse: %s", err.Error())
 292  	}
 293  	if verr := r.Validate(); verr != nil {
 294  		t.Fatalf("validate: %s", verr.Error())
 295  	}
 296  	if r.IsWaitlist() {
 297  		t.Fatal("an accepted RSVP reported as a waitlist entry")
 298  	}
 299  	if got := r.Tags(); !sameTags(got, want) {
 300  		t.Fatalf("round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 301  	}
 302  
 303  	var wait [][]string
 304  	wait = appendTag(wait, event.Tag())
 305  	wait = appendKV(wait, TagD, "waitlist-d")
 306  	wait = appendKV(wait, TagStatus, StatusWaitlist)
 307  	wait = appendKV(wait, TagPosition, "14")
 308  	wait = appendKV(wait, TagJoinedAt, "1735851600")
 309  	w, err := ParseRSVP(wait, "")
 310  	if err != nil {
 311  		t.Fatalf("parse waitlist: %s", err.Error())
 312  	}
 313  	if verr := w.Validate(); verr != nil {
 314  		t.Fatalf("validate waitlist: %s", verr.Error())
 315  	}
 316  	if !w.IsWaitlist() || w.Position != "14" || w.JoinedAt != "1735851600" {
 317  		t.Fatalf("waitlist = %+v", w)
 318  	}
 319  	if got := w.Tags(); !sameTags(got, wait) {
 320  		t.Fatalf("waitlist round trip differs:\n got %s\nwant %s", joinTags(got), joinTags(want))
 321  	}
 322  }
 323  
 324  func TestRSVPValidate(t *testing.T) {
 325  	event := &Address{Kind: KindCalendar, Pubkey: venuePub, D: "event-d"}
 326  	r := &RSVP{Event: event, D: "d", Status: StatusAccepted}
 327  	if err := r.Validate(); err != nil {
 328  		t.Fatalf("valid rsvp rejected: %s", err.Error())
 329  	}
 330  	r2 := &RSVP{Event: event, D: "d", Status: StatusWaitlist}
 331  	if err := r2.Validate(); err == nil {
 332  		t.Fatal("a waitlist entry with no joined_at was accepted")
 333  	}
 334  	r3 := &RSVP{Event: event, D: "d", Status: "maybe"}
 335  	if err := r3.Validate(); err == nil {
 336  		t.Fatal("an unknown status was accepted")
 337  	}
 338  	r4 := &RSVP{Event: event, D: "d", Status: StatusTentative, Fb: "sleepy"}
 339  	if err := r4.Validate(); err == nil {
 340  		t.Fatal("an unknown free/busy value was accepted")
 341  	}
 342  	r5 := &RSVP{D: "d", Status: StatusAccepted}
 343  	if err := r5.Validate(); err == nil {
 344  		t.Fatal("an rsvp with no event was accepted")
 345  	}
 346  }
 347