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