| 1 | package movementtest |
| 2 | |
| 3 | import ( |
| 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. |
| 14 | func 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 | |
| 29 | func 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. |
| 57 | func 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. |
| 83 | func 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 | |
| 94 | func escapeForPage(s string) string { |
| 95 | r := strings.NewReplacer("&", "&", "<", "<", ">", ">", |
| 96 | `"`, """, "'", "'") |
| 97 | return r.Replace(s) |
| 98 | } |
| 99 | |