package e2e import ( "encoding/json" "fmt" "io" "net/http" "net/http/cookiejar" "net/url" "os" "path/filepath" "strings" "testing" ) // browser is an HTTP client with a cookie jar, standing in for a logged-in // user's browser. func newBrowser(t *testing.T) *http.Client { t.Helper() jar, err := cookiejar.New(nil) if err != nil { t.Fatal(err) } return &http.Client{Jar: jar} } func (i *instance) base() string { return fmt.Sprintf("http://127.0.0.1:%d", i.httpPort) } func browserGet(t *testing.T, c *http.Client, url string) (int, string) { t.Helper() resp, err := c.Get(url) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(body) } func browserPost(t *testing.T, c *http.Client, u string, form url.Values) (int, string) { t.Helper() resp, err := c.PostForm(u, form) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(body) } func TestWebAccounts(t *testing.T) { inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") aliceKey := inst.newKey(t, "alice") inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") // A repo with one file to edit. if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/site"); code != 0 { t.Fatalf("repo create: %s", errOut) } work := t.TempDir() env := inst.gitEnv(aliceKey) mustGit(t, work, env, "clone", inst.sshURL("alice/site"), "w") dir := filepath.Join(work, "w") os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("original\n"), 0o644) mustGit(t, dir, env, "checkout", "-q", "-b", "main") mustGit(t, dir, env, "add", ".") mustGit(t, dir, env, "commit", "-q", "-m", "base") mustGit(t, dir, env, "push", "-q", "origin", "main") // SSH-minted login URL. out, errOut, code := inst.ssh(t, aliceKey, "", "web", "login", "--json") if code != 0 { t.Fatalf("web login: %s", errOut) } var env2 struct { Data struct { URL string `json:"url"` } `json:"data"` } if err := json.Unmarshal([]byte(out), &env2); err != nil { t.Fatalf("web login JSON: %v\n%s", err, out) } // The URL carries the configured site host; rewrite to the test port. loginPath := env2.Data.URL[strings.Index(env2.Data.URL, "/login"):] browser := newBrowser(t) status, body := browserGet(t, browser, inst.base()+loginPath) // The rail's footer carries the signed-in account now. if status != 200 || !strings.Contains(body, ">Dashboard") || !strings.Contains(body, `class="railuser" href="/alice"`) { t.Fatalf("login redirect landed wrong: %d\n%s", status, body) } // The token is single-use. fresh := newBrowser(t) _, body = browserGet(t, fresh, inst.base()+loginPath) if !strings.Contains(body, "invalid, expired, or already used") { t.Fatalf("token reuse not refused:\n%s", body) } // Create a repo through the web. status, _ = browserPost(t, browser, inst.base()+"/new", url.Values{"name": {"webborn"}, "visibility": {"private"}}) if status != 200 { t.Fatalf("web repo create: %d", status) } if out, _, code := inst.ssh(t, aliceKey, "", "repo", "show", "alice/webborn"); code != 0 { t.Fatalf("web-created repo missing over ssh: %s", out) } // Logged-in viewer sees their private repo; anonymous still gets 404. if status, _ = browserGet(t, browser, inst.base()+"/alice/webborn"); status != 200 { t.Fatalf("owner blocked from private repo page: %d", status) } if status, _ := inst.get(t, "/alice/webborn"); status != 404 { t.Fatalf("anonymous sees private repo: %d", status) } // File edit: form loads with current content, POST commits. status, body = browserGet(t, browser, inst.base()+"/alice/site/edit/main/notes.txt") if status != 200 || !strings.Contains(body, "original") { t.Fatalf("edit form: %d\n%s", status, body) } status, _ = browserPost(t, browser, inst.base()+"/alice/site/edit/main/notes.txt", url.Values{"content": {"edited from the web\n"}, "message": {"web edit"}}) if status != 200 { t.Fatalf("edit submit: %d", status) } // A branch that does not exist is a 404; a path that does not exist // on a real branch is a new-file form that says so. if status, _ := browserGet(t, browser, inst.base()+"/alice/site/edit/nope/notes.txt"); status != 404 { t.Fatalf("edit form on a missing branch: %d", status) } status, body = browserGet(t, browser, inst.base()+"/alice/site/edit/main/new.txt") if status != 200 || !strings.Contains(body, "does not exist on main; committing creates it") || !strings.Contains(body, "