| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "net/http" |
| 6 | "net/url" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func TestAppSettingsSectionRoutes(t *testing.T) { |
| 12 | fx, _ := instanceFixture(t, "code-settings-sections") |
| 13 | c := newBrowser(t) |
| 14 | signUp(t, fx, c, "code-settings-sections-browser") |
| 15 | |
| 16 | for _, section := range []string{"general", "domains", "storage", "email", "features", "advanced", "delete"} { |
| 17 | status, _, body := get(t, c, fx.u("/apps/titogo/settings/"+section)) |
| 18 | if status != http.StatusOK { |
| 19 | t.Errorf("GET settings/%s = %d, want 200: %s", section, status, body) |
| 20 | } |
| 21 | } |
| 22 | _, _, general := get(t, c, fx.u("/apps/titogo/settings")) |
| 23 | for _, want := range []string{ |
| 24 | `aria-label="Settings sections"`, ">General</a>", ">Domains</a>", |
| 25 | ">Release pipeline</a>", ">Storage</a>", ">Email</a>", |
| 26 | ">Feature flags</a>", ">Advanced</a>", ">Delete</a>", |
| 27 | } { |
| 28 | if !strings.Contains(general, want) { |
| 29 | t.Errorf("Settings navigation missing %q", want) |
| 30 | } |
| 31 | } |
| 32 | if strings.Contains(general, "Domains & TLS") { |
| 33 | t.Fatal("General still renders the Domains section") |
| 34 | } |
| 35 | _, _, domains := get(t, c, fx.u("/apps/titogo/settings/domains")) |
| 36 | if !strings.Contains(domains, "Domains & TLS") || strings.Contains(domains, "<h2>Identity</h2>") { |
| 37 | t.Fatal("Domains does not render as an isolated section") |
| 38 | } |
| 39 | status, _, _ := get(t, c, fx.u("/apps/titogo/settings/not-a-section")) |
| 40 | if status != http.StatusNotFound { |
| 41 | t.Errorf("GET unknown settings section = %d, want 404", status) |
| 42 | } |
| 43 | assertNonMember404(t, fx, "/apps/titogo/settings/domains") |
| 44 | } |
| 45 | |
| 46 | func TestAppSettingsURL(t *testing.T) { |
| 47 | if got := appSettingsURL("a b", "an/app", "general"); got != "/a/a%20b/apps/an%2Fapp/settings" { |
| 48 | t.Errorf("general URL = %q", got) |
| 49 | } |
| 50 | if got := appSettingsURL("bab", "kass", "storage"); got != "/a/bab/apps/kass/settings/storage" { |
| 51 | t.Errorf("storage URL = %q", got) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func TestPipelineSettingsCanonicalSectionURL(t *testing.T) { |
| 56 | fx, _ := instanceFixture(t, "code-settings-pipeline-section") |
| 57 | c := newBrowser(t) |
| 58 | signUp(t, fx, c, "code-settings-pipeline-section-browser") |
| 59 | |
| 60 | status, _, body := get(t, c, fx.u("/apps/titogo/settings/pipeline")) |
| 61 | if status != http.StatusOK || !strings.Contains(body, "Release pipeline") { |
| 62 | t.Fatalf("GET settings/pipeline = %d: %s", status, body) |
| 63 | } |
| 64 | status, _, body = get(t, c, fx.u("/apps/titogo/pipeline")) |
| 65 | if status != http.StatusOK || !strings.Contains(body, "Release pipeline") { |
| 66 | t.Fatalf("legacy GET pipeline = %d: %s", status, body) |
| 67 | } |
| 68 | csrf := sessionToken(t, fx, c) |
| 69 | status, _, body = postForm(t, c, fx.u("/apps/titogo/pipeline"), url.Values{ |
| 70 | "csrf": {csrf}, "action": {"add"}, "ch.0.name": {"edge"}, |
| 71 | }) |
| 72 | if status != http.StatusOK || !strings.Contains(body, `aria-label="Settings sections"`) || |
| 73 | !strings.Contains(body, `aria-current="page">Release pipeline</a>`) { |
| 74 | t.Fatalf("pipeline draft rerender lost Settings navigation = %d: %s", status, body) |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | func TestSettingsValidationRetainsTypedValues(t *testing.T) { |
| 79 | fx := newConsoleFixture(t) |
| 80 | freshApp(t, fx, "sprout") |
| 81 | c := signInPaul(t, fx) |
| 82 | csrf := sessionToken(t, fx, c) |
| 83 | |
| 84 | status, _, body := postForm(t, c, fx.u("/apps/sprout/rename"), url.Values{ |
| 85 | "csrf": {csrf}, "to": {"Still Typing"}, |
| 86 | }) |
| 87 | if status != http.StatusUnprocessableEntity || !strings.Contains(body, `value="Still Typing"`) { |
| 88 | t.Fatalf("rename refusal did not retain the typed name = %d: %s", status, body) |
| 89 | } |
| 90 | |
| 91 | status, _, body = postForm(t, c, fx.u("/apps/sprout/domains/attach"), url.Values{ |
| 92 | "csrf": {csrf}, "host": {"not a host"}, "channel": {"canary/draft"}, |
| 93 | }) |
| 94 | if status != http.StatusUnprocessableEntity || !strings.Contains(body, `value="not a host"`) || |
| 95 | !strings.Contains(body, `value="canary/draft"`) { |
| 96 | t.Fatalf("domain refusal did not retain host and channel = %d: %s", status, body) |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | func TestBucketlessSettingsSectionsExplainConfiguration(t *testing.T) { |
| 101 | for section, want := range map[string]string{ |
| 102 | "storage": "no deployment bucket on this host — this console cannot serve object storage", |
| 103 | "email": "no deployment bucket on this host — this console cannot serve email", |
| 104 | "features": "no deployment bucket on this host — this console cannot read features", |
| 105 | } { |
| 106 | p := page{ |
| 107 | Title: "Settings", AppName: "sprout", ActiveTab: "settings", SettingsSection: section, |
| 108 | Account: &Account{Name: "paul@example.com", Sqid: "bab"}, |
| 109 | Detail: &AppView{App: "sprout"}, Settings: &SettingsView{NoBucket: true}, |
| 110 | } |
| 111 | var buf bytes.Buffer |
| 112 | if err := templates.ExecuteTemplate(&buf, "app_settings", p); err != nil { |
| 113 | t.Fatalf("render %s: %v", section, err) |
| 114 | } |
| 115 | body := buf.String() |
| 116 | if !strings.Contains(body, want) || strings.Contains(body, "Please try again") { |
| 117 | t.Fatalf("bucketless %s section is not explicit: %s", section, body) |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 | |