package movementtest import ( "strings" "testing" movement "amadan.net/carlos/movement/internal/movement" ) // A name is required, and a submission without one is told so rather than // silently dropped — the same treatment a malformed address gets, and for the // same reason: it is a property of what was just typed, not of any address's // state on the list, so saying it leaks nothing. func TestSignupNeedsAName(t *testing.T) { h := newApp(t) form := submission(t, "anon@example.com", "curious") form.Set("name", " ") rec := postSignup(t, h, form) if got := countSignups(t, h, "anon@example.com"); got != 0 { t.Errorf("a nameless submission wrote %d rows, want 0", got) } if !strings.Contains(rec.Body.String(), escapeForPage(movement.T("form.badname"))) { t.Error("the page should say a name is needed") } } func TestNameIsStoredAndShownToAdmin(t *testing.T) { h := newAdminApp(t) form := submission(t, "named@example.com", "curious") form.Set("name", "Ada Lovelace") postSignup(t, h, form) if got := loadSignup(t, h, "named@example.com").Name; got != "Ada Lovelace" { t.Errorf("stored name = %q", got) } confirmSignup(t, h, "named@example.com") cookies := signInAsAdmin(t, h) if !strings.Contains(getAs(t, h, "/admin", cookies).Body.String(), "Ada Lovelace") { t.Error("the admin list does not show the name") } csv := getAs(t, h, "/admin/export.csv", cookies).Body.String() if !strings.HasPrefix(csv, "name,email,stance") { t.Errorf("the export's first column should be the name:\n%s", csv[:60]) } if !strings.Contains(csv, "Ada Lovelace") { t.Error("the name is missing from the export") } } // The name reaches a CSV cell and an admin page, so it gets the same bounds // and the same formula-injection defusing as everything else somebody types. func TestNameIsBoundedAndDefused(t *testing.T) { h := newAdminApp(t) long := strings.Repeat("é", 400) form := submission(t, "long@example.com", "curious") form.Set("name", long) postSignup(t, h, form) if n := len([]rune(loadSignup(t, h, "long@example.com").Name)); n > 120 { t.Errorf("stored name is %d runes; the cap is 120", n) } form = submission(t, "sneaky@example.com", "curious") form.Set("name", "=cmd|'/c calc'!A1") postSignup(t, h, form) confirmSignup(t, h, "sneaky@example.com") cookies := signInAsAdmin(t, h) csv := getAs(t, h, "/admin/export.csv", cookies).Body.String() if strings.Contains(csv, ",=cmd") || strings.Contains(csv, "\"=cmd") { t.Errorf("a formula reached the export through the name field:\n%s", csv) } } // A name is not a field the copy round has seen yet, so it must not be able // to reach production. This is the same gate the design was built behind. func TestTheNameFieldsAreStillAwaitingReview(t *testing.T) { left := movement.Placeholders() if len(left) == 0 { t.Skip("the name copy has been reviewed; this gate has done its job") } if err := movement.CheckPlaceholders( "https://"+movement.CanonicalHost, movement.CanonicalHost); err == nil { t.Error("unreviewed copy would be served on the canonical host") } } func escapeForPage(s string) string { r := strings.NewReplacer("&", "&", "<", "<", ">", ">", `"`, """, "'", "'") return r.Replace(s) }