oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "context"
5 "regexp"
6 "strings"
7 "testing"
8 "time"
9
10 calendar "amadan.net/oficina/calendar/internal/calendar"
11)
12
13// aHost is a person who has signed in and can own booking pages.
14func aHost(t *testing.T, app *calendar.App, address string) calendar.Person {
15 t.Helper()
16 app.SignInForTest(t, address)
17 return app.PersonByAddressForTest(t, address)
18}
19
20var mondayLunch = []calendar.Window{
21 {Weekday: time.Monday, StartMin: 12 * 60, EndMin: 15 * 60},
22 {Weekday: time.Tuesday, StartMin: 12 * 60, EndMin: 14 * 60},
23}
24
25func pageInput() calendar.BookingPageInput {
26 return calendar.BookingPageInput{
27 Title: "Quick chat about the acquisition",
28 Description: "Twenty minutes, no slides, bring the unmarked folder.",
29 Availability: "mondays 12-3, tuesdays 12-2",
30 DurationMin: 20,
31 BufferMin: 10,
32 NoticeMin: 120,
33 HorizonDays: 30,
34 }
35}
36
37// A page is made, read back with its text opened and its windows in
38// order, hidden from the public until it is published, and its text is
39// nowhere in the raw file.
40func TestABookingPageRoundTripsSealedWithItsWindows(t *testing.T) {
41 app := newApp(t)
42 host := aHost(t, app, "paul@example.com")
43 store := app.StoreForTest()
44 ctx := context.Background()
45
46 if err := store.SaveHandle(ctx, host.ID, "paul"); err != nil {
47 t.Fatalf("SaveHandle: %v", err)
48 }
49 in := pageInput()
50 page, err := store.CreateBookingPage(ctx, host.ID, in, mondayLunch)
51 if err != nil {
52 t.Fatalf("CreateBookingPage: %v", err)
53 }
54 if !regexp.MustCompile(`^[a-z0-9]{8}$`).MatchString(page.Code) {
55 t.Errorf("code %q is not eight of [a-z0-9]", page.Code)
56 }
57 if page.PersonID != host.ID {
58 t.Errorf("page owned by %d, want %d", page.PersonID, host.ID)
59 }
60
61 // Unpublished: the owner sees it, the public does not.
62 mine, err := store.BookingPage(ctx, host.ID, page.ID)
63 if err != nil {
64 t.Fatalf("BookingPage: %v", err)
65 }
66 if mine.Title != in.Title || mine.Description != in.Description || mine.Availability != in.Availability {
67 t.Errorf("opened text differs: %+v", mine)
68 }
69 if mine.Published {
70 t.Error("a new page reports Published")
71 }
72 if len(mine.Windows) != 2 || mine.Windows[0] != mondayLunch[0] || mine.Windows[1] != mondayLunch[1] {
73 t.Errorf("windows = %+v, want %+v", mine.Windows, mondayLunch)
74 }
75 if _, _, err := store.PublicBookingPage(ctx, "paul", page.Code); !calendar.NotFound(err) {
76 t.Errorf("unpublished page is public: err = %v", err)
77 }
78
79 if err := store.PublishBookingPage(ctx, host.ID, page.ID, true); err != nil {
80 t.Fatalf("PublishBookingPage: %v", err)
81 }
82 pub, who, err := store.PublicBookingPage(ctx, "paul", page.Code)
83 if err != nil {
84 t.Fatalf("PublicBookingPage after publish: %v", err)
85 }
86 if !pub.Published || pub.Title != in.Title || who.ID != host.ID {
87 t.Errorf("public view = %+v host %d, want published, titled, host %d", pub, who.ID, host.ID)
88 }
89 if len(pub.Windows) != 2 {
90 t.Errorf("public windows = %+v", pub.Windows)
91 }
92
93 // An update replaces the text and the windows wholesale.
94 in.Title = "Longer chat"
95 if err := store.UpdateBookingPage(ctx, host.ID, page.ID, in, mondayLunch[:1]); err != nil {
96 t.Fatalf("UpdateBookingPage: %v", err)
97 }
98 list, err := store.BookingPages(ctx, host.ID)
99 if err != nil {
100 t.Fatalf("BookingPages: %v", err)
101 }
102 if len(list) != 1 || list[0].Title != "Longer chat" || len(list[0].Windows) != 1 {
103 t.Errorf("after update: %+v", list)
104 }
105 if list[0].Code != page.Code {
106 t.Errorf("an update changed the code from %q to %q", page.Code, list[0].Code)
107 }
108
109 if _, err := app.DBForTest().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
110 t.Fatalf("checkpoint: %v", err)
111 }
112 for _, needle := range []string{
113 "Quick chat about the acquisition", "Longer chat",
114 "unmarked folder", "mondays 12-3",
115 } {
116 if found, where := app.GrepDatabaseForTest(t, needle); found {
117 t.Errorf("%q appears in plaintext in %s", needle, where)
118 }
119 }
120}
121
122// Whatever is wrong — the handle, the code, or publication — the answer
123// is one error, so nothing can be enumerated by telling them apart.
124func TestUnknownHandleUnknownCodeAndUnpublishedAllVanishTheSameWay(t *testing.T) {
125 app := newApp(t)
126 host := aHost(t, app, "paul@example.com")
127 store := app.StoreForTest()
128 ctx := context.Background()
129
130 if err := store.SaveHandle(ctx, host.ID, "paul"); err != nil {
131 t.Fatalf("SaveHandle: %v", err)
132 }
133 page, err := store.CreateBookingPage(ctx, host.ID, pageInput(), mondayLunch)
134 if err != nil {
135 t.Fatalf("CreateBookingPage: %v", err)
136 }
137 if err := store.PublishBookingPage(ctx, host.ID, page.ID, true); err != nil {
138 t.Fatalf("publish: %v", err)
139 }
140 if _, _, err := store.PublicBookingPage(ctx, "paul", page.Code); err != nil {
141 t.Fatalf("the published page should be public: %v", err)
142 }
143
144 if _, _, err := store.PublicBookingPage(ctx, "nobody", page.Code); !calendar.NotFound(err) {
145 t.Errorf("unknown handle: err = %v, want ErrNotFound", err)
146 }
147 if _, _, err := store.PublicBookingPage(ctx, "paul", "zzzzzzzz"); !calendar.NotFound(err) {
148 t.Errorf("unknown code: err = %v, want ErrNotFound", err)
149 }
150 if err := store.PublishBookingPage(ctx, host.ID, page.ID, false); err != nil {
151 t.Fatalf("unpublish: %v", err)
152 }
153 if _, _, err := store.PublicBookingPage(ctx, "paul", page.Code); !calendar.NotFound(err) {
154 t.Errorf("unpublished: err = %v, want ErrNotFound", err)
155 }
156 if _, err := store.PersonByHandle(ctx, "nobody"); !calendar.NotFound(err) {
157 t.Errorf("PersonByHandle(nobody): err = %v, want ErrNotFound", err)
158 }
159}
160
161// Every settings read or write is scoped to the owner, and a miss is
162// ErrNotFound rather than a different error for "exists but not yours".
163func TestAnotherPersonCannotReadEditOrPublishMyPage(t *testing.T) {
164 app := newApp(t)
165 paul := aHost(t, app, "paul@example.com")
166 other := aHost(t, app, "other@example.com")
167 store := app.StoreForTest()
168 ctx := context.Background()
169
170 page, err := store.CreateBookingPage(ctx, paul.ID, pageInput(), mondayLunch)
171 if err != nil {
172 t.Fatalf("CreateBookingPage: %v", err)
173 }
174
175 if _, err := store.BookingPage(ctx, other.ID, page.ID); !calendar.NotFound(err) {
176 t.Errorf("read as another person: err = %v, want ErrNotFound", err)
177 }
178 if err := store.UpdateBookingPage(ctx, other.ID, page.ID, pageInput(), mondayLunch); !calendar.NotFound(err) {
179 t.Errorf("update as another person: err = %v, want ErrNotFound", err)
180 }
181 if err := store.PublishBookingPage(ctx, other.ID, page.ID, true); !calendar.NotFound(err) {
182 t.Errorf("publish as another person: err = %v, want ErrNotFound", err)
183 }
184 list, err := store.BookingPages(ctx, other.ID)
185 if err != nil || len(list) != 0 {
186 t.Errorf("another person's list = %+v, %v; want empty", list, err)
187 }
188
189 // None of that touched the page.
190 mine, err := store.BookingPage(ctx, paul.ID, page.ID)
191 if err != nil {
192 t.Fatalf("my own read: %v", err)
193 }
194 if mine.Published {
195 t.Error("another person's publish took effect")
196 }
197 if len(mine.Windows) != 2 {
198 t.Errorf("another person's update touched the windows: %+v", mine.Windows)
199 }
200}
201
202// The limits are bytes, because the event validator's are bytes and two
203// different units for "how long is a title" is a bug waiting for a
204// multibyte name.
205func TestPageLimitsAreBytes(t *testing.T) {
206 app := newApp(t)
207 paul := aHost(t, app, "paul@example.com")
208 store := app.StoreForTest()
209 ctx := context.Background()
210
211 euro := strings.Repeat("€", 33) // 99 bytes, 33 runes
212 over := pageInput()
213 over.Title = euro + "ab" // 101 bytes, 35 runes
214 if _, err := store.CreateBookingPage(ctx, paul.ID, over, mondayLunch); err == nil {
215 t.Error("a 101-byte title was accepted")
216 } else if v, ok := calendar.Invalid(err); !ok || v.Field != "title" {
217 t.Errorf("a 101-byte title: err = %v, want a title ValidationError", err)
218 }
219
220 fits := pageInput()
221 fits.Title = euro + "a" // 100 bytes
222 if _, err := store.CreateBookingPage(ctx, paul.ID, fits, mondayLunch); err != nil {
223 t.Errorf("a 100-byte title was refused: %v", err)
224 }
225
226 longDesc := pageInput()
227 longDesc.Description = strings.Repeat("€", 333) + "ab" // 1001 bytes
228 if err := calendar.ValidateBookingPageInput(&longDesc); err == nil {
229 t.Error("a 1001-byte description was accepted")
230 }
231 longDesc.Description = strings.Repeat("€", 333) + "a" // 1000 bytes
232 if err := calendar.ValidateBookingPageInput(&longDesc); err != nil {
233 t.Errorf("a 1000-byte description was refused: %v", err)
234 }
235
236 // The knobs have ranges too.
237 for _, tc := range []struct {
238 name string
239 mut func(*calendar.BookingPageInput)
240 }{
241 {"empty title", func(in *calendar.BookingPageInput) { in.Title = " " }},
242 {"duration 4", func(in *calendar.BookingPageInput) { in.DurationMin = 4 }},
243 {"duration 481", func(in *calendar.BookingPageInput) { in.DurationMin = 481 }},
244 {"buffer 121", func(in *calendar.BookingPageInput) { in.BufferMin = 121 }},
245 {"buffer -1", func(in *calendar.BookingPageInput) { in.BufferMin = -1 }},
246 {"notice 10081", func(in *calendar.BookingPageInput) { in.NoticeMin = 10081 }},
247 {"horizon 0", func(in *calendar.BookingPageInput) { in.HorizonDays = 0 }},
248 {"horizon 91", func(in *calendar.BookingPageInput) { in.HorizonDays = 91 }},
249 } {
250 in := pageInput()
251 tc.mut(&in)
252 if err := calendar.ValidateBookingPageInput(&in); err == nil {
253 t.Errorf("%s was accepted", tc.name)
254 }
255 }
256 if err := calendar.ValidateBookingPageInput(&calendar.BookingPageInput{
257 Title: "Edges", DurationMin: 480, BufferMin: 120, NoticeMin: 10080, HorizonDays: 90,
258 }); err != nil {
259 t.Errorf("the upper edges were refused: %v", err)
260 }
261}
262