carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movementtest
2
3import (
4 "strings"
5 "testing"
6
7 movement "amadan.net/carlos/movement/internal/movement"
8)
9
10// A name is required, and a submission without one is told so rather than
11// silently dropped — the same treatment a malformed address gets, and for the
12// same reason: it is a property of what was just typed, not of any address's
13// state on the list, so saying it leaks nothing.
14func TestSignupNeedsAName(t *testing.T) {
15 h := newApp(t)
16
17 form := submission(t, "anon@example.com", "curious")
18 form.Set("name", " ")
19 rec := postSignup(t, h, form)
20
21 if got := countSignups(t, h, "anon@example.com"); got != 0 {
22 t.Errorf("a nameless submission wrote %d rows, want 0", got)
23 }
24 if !strings.Contains(rec.Body.String(), escapeForPage(movement.T("form.badname"))) {
25 t.Error("the page should say a name is needed")
26 }
27}
28
29func TestNameIsStoredAndShownToAdmin(t *testing.T) {
30 h := newAdminApp(t)
31
32 form := submission(t, "named@example.com", "curious")
33 form.Set("name", "Ada Lovelace")
34 postSignup(t, h, form)
35
36 if got := loadSignup(t, h, "named@example.com").Name; got != "Ada Lovelace" {
37 t.Errorf("stored name = %q", got)
38 }
39
40 confirmSignup(t, h, "named@example.com")
41 cookies := signInAsAdmin(t, h)
42
43 if !strings.Contains(getAs(t, h, "/admin", cookies).Body.String(), "Ada Lovelace") {
44 t.Error("the admin list does not show the name")
45 }
46 csv := getAs(t, h, "/admin/export.csv", cookies).Body.String()
47 if !strings.HasPrefix(csv, "name,email,stance") {
48 t.Errorf("the export's first column should be the name:\n%s", csv[:60])
49 }
50 if !strings.Contains(csv, "Ada Lovelace") {
51 t.Error("the name is missing from the export")
52 }
53}
54
55// The name reaches a CSV cell and an admin page, so it gets the same bounds
56// and the same formula-injection defusing as everything else somebody types.
57func TestNameIsBoundedAndDefused(t *testing.T) {
58 h := newAdminApp(t)
59
60 long := strings.Repeat("é", 400)
61 form := submission(t, "long@example.com", "curious")
62 form.Set("name", long)
63 postSignup(t, h, form)
64
65 if n := len([]rune(loadSignup(t, h, "long@example.com").Name)); n > 120 {
66 t.Errorf("stored name is %d runes; the cap is 120", n)
67 }
68
69 form = submission(t, "sneaky@example.com", "curious")
70 form.Set("name", "=cmd|'/c calc'!A1")
71 postSignup(t, h, form)
72 confirmSignup(t, h, "sneaky@example.com")
73
74 cookies := signInAsAdmin(t, h)
75 csv := getAs(t, h, "/admin/export.csv", cookies).Body.String()
76 if strings.Contains(csv, ",=cmd") || strings.Contains(csv, "\"=cmd") {
77 t.Errorf("a formula reached the export through the name field:\n%s", csv)
78 }
79}
80
81// A name is not a field the copy round has seen yet, so it must not be able
82// to reach production. This is the same gate the design was built behind.
83func TestTheNameFieldsAreStillAwaitingReview(t *testing.T) {
84 left := movement.Placeholders()
85 if len(left) == 0 {
86 t.Skip("the name copy has been reviewed; this gate has done its job")
87 }
88 if err := movement.CheckPlaceholders(
89 "https://"+movement.CanonicalHost, movement.CanonicalHost); err == nil {
90 t.Error("unreviewed copy would be served on the canonical host")
91 }
92}
93
94func escapeForPage(s string) string {
95 r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;",
96 `"`, "&#34;", "'", "&#39;")
97 return r.Replace(s)
98}
99