package e2e import ( "compress/gzip" "fmt" "io" "net/http" "os" "path/filepath" "strings" "testing" "golang.org/x/crypto/ssh" "gitbay.org/gitbay/internal/sig" ) func (i *instance) get(t *testing.T, path string) (int, string) { t.Helper() resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d%s", i.httpPort, path)) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(body) } func TestWebUI(t *testing.T) { inst := startInstance(t) aliceKey := inst.newKey(t, "alice") inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") // Public repo with real content: a README, a source file, a tag, and // one SSHSIG-signed commit for the badge check. 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, "README.md"), []byte("# hello site\n\nsome *markdown*\n"), 0o644) os.MkdirAll(filepath.Join(dir, "src"), 0o755) os.WriteFile(filepath.Join(dir, "src", "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644) mustGit(t, dir, env, "checkout", "-q", "-b", "main") mustGit(t, dir, env, "add", ".") mustGit(t, dir, env, "commit", "-q", "-m", "first commit") mustGit(t, dir, env, "tag", "v1.0") mustGit(t, dir, env, "push", "-q", "origin", "main", "v1.0") // A signed commit on top, built with the M4 fixture helpers. sshRaw, _ := os.ReadFile(aliceKey) signer, err := ssh.ParsePrivateKey(sshRaw) if err != nil { t.Fatal(err) } head := strings.TrimSpace(mustGit(t, dir, env, "rev-parse", "HEAD")) buildSignedCommitOn(t, dir, env, head, "signed tip", "alice@example.test", signer) mustGit(t, dir, env, "push", "-q", "origin", "main") // Private repo must be invisible everywhere. if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private"); code != 0 { t.Fatal("create private failed") } // Index lists the public repo, not the private one. status, body := inst.get(t, "/") if status != 200 || !strings.Contains(body, "alice/site") { t.Fatalf("index: %d\n%s", status, body) } if strings.Contains(body, "secret") { t.Fatal("index leaks private repo") } // Repo home: tree entries plus rendered README. status, body = inst.get(t, "/alice/site") if status != 200 || !strings.Contains(body, "src/") || !strings.Contains(body, "README.md") { t.Fatalf("repo home: %d\n%s", status, body) } if !strings.Contains(body, "

hello site

") || !strings.Contains(body, "markdown") { t.Fatalf("README not rendered:\n%s", body) } for _, tab := range []string{">issues<", ">merge requests<"} { if !strings.Contains(body, tab) { t.Fatalf("repo header missing %s tab", tab) } } // Subdirectory tree and blob with highlighting. status, body = inst.get(t, "/alice/site/tree/main/src") if status != 200 || !strings.Contains(body, "main.go") { t.Fatalf("tree src: %d", status) } status, body = inst.get(t, "/alice/site/blob/main/src/main.go") if status != 200 || !strings.Contains(body, "package") { t.Fatalf("blob: %d", status) } // Raw serves exact bytes with nosniff. resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/site/raw/main/src/main.go", inst.httpPort)) if err != nil { t.Fatal(err) } raw, _ := io.ReadAll(resp.Body) resp.Body.Close() if string(raw) != "package main\n\nfunc main() {}\n" { t.Fatalf("raw bytes: %q", raw) } if resp.Header.Get("X-Content-Type-Options") != "nosniff" { t.Fatal("raw missing nosniff") } // Log: both commits, with badges matching the M4 states exactly. status, body = inst.get(t, "/alice/site/log") if status != 200 { t.Fatalf("log: %d", status) } if !strings.Contains(body, "badge-verified") || !strings.Contains(body, "signed tip") { t.Fatalf("log missing verified badge:\n%s", body) } if !strings.Contains(body, "badge-unsigned") || !strings.Contains(body, "first commit") { t.Fatalf("log missing unsigned badge:\n%s", body) } // Commit page for the signed tip. tip := strings.TrimSpace(mustGit(t, dir, env, "rev-parse", "HEAD")) status, body = inst.get(t, "/alice/site/commit/"+tip) if status != 200 || !strings.Contains(body, "badge-verified") || !strings.Contains(body, "alice") { t.Fatalf("commit page: %d\n%s", status, body) } // Refs page shows branch and tag. status, body = inst.get(t, "/alice/site/refs") if status != 200 || !strings.Contains(body, "main") || !strings.Contains(body, "v1.0") { t.Fatalf("refs: %d", status) } // Archive downloads a valid gzip. resp, err = http.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/site/archive/main.tar.gz", inst.httpPort)) if err != nil { t.Fatal(err) } gz, err := gzip.NewReader(resp.Body) if err != nil { t.Fatalf("archive not gzip: %v", err) } tarBytes, _ := io.ReadAll(gz) resp.Body.Close() if !strings.Contains(string(tarBytes), "README.md") { t.Fatal("archive missing content") } // README formats: org-mode renders, HTML renders sanitized, unknown // extensions fall back to plaintext, and richer formats win conflicts. readmeRepo := func(name, file, content string) { t.Helper() if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/"+name); code != 0 { t.Fatalf("repo create %s failed", name) } w := t.TempDir() mustGit(t, w, env, "clone", inst.sshURL("alice/"+name), "r") d := filepath.Join(w, "r") os.WriteFile(filepath.Join(d, file), []byte(content), 0o644) mustGit(t, d, env, "checkout", "-q", "-b", "main") mustGit(t, d, env, "add", ".") mustGit(t, d, env, "commit", "-q", "-m", "readme") mustGit(t, d, env, "push", "-q", "origin", "main") } readmeRepo("orgdoc", "README.org", "* Heading\n\nSome /emphasis/ here.\n") status, body = inst.get(t, "/alice/orgdoc") if status != 200 || !strings.Contains(body, "headline-1") || !strings.Contains(body, "emphasis") { t.Fatalf("org README not rendered:\n%s", body) } readmeRepo("htmldoc", "README.html", "

fine

") status, body = inst.get(t, "/alice/htmldoc") if status != 200 || !strings.Contains(body, "fine

") { t.Fatalf("html README not rendered:\n%s", body) } if strings.Contains(body, "