homepage_test.go
5321 bytes
1package web
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "github.com/charmbracelet/soft-serve/pkg/access"
14 "github.com/charmbracelet/soft-serve/pkg/config"
15 "github.com/charmbracelet/soft-serve/pkg/proto"
16)
17
18func TestHomepageListsOnlyPublicVisibleRepositoriesAndProfile(t *testing.T) {
19 ctx, be, _ := newLFSTestContext(t)
20 cfg := config.FromContext(ctx)
21 cfg.HTTP.WebUI.Enabled = true
22 allowKeyless := true
23 cfg.AllowKeyless = &allowKeyless
24 anonAccess := access.ReadOnlyAccess
25 cfg.AnonAccess = &anonAccess
26
27 owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
28 if err != nil {
29 t.Fatal(err)
30 }
31 createHomepageRepository(t, be, ctx, "public-project", owner, proto.RepositoryOptions{
32 ProjectName: "<script>Public project</script>",
33 Description: `<img src=x onerror="alert(1)">`,
34 })
35 createHomepageRepository(t, be, ctx, "private-project", owner, proto.RepositoryOptions{Private: true})
36 createHomepageRepository(t, be, ctx, "hidden-project", owner, proto.RepositoryOptions{Hidden: true})
37 createHomepageRepository(t, be, ctx, ".soft-serve", owner, proto.RepositoryOptions{})
38 commitHomepageFile(t, cfg.DataPath, ".soft-serve", "README.md", "# Profile\n\nPublic *profile* content.\n")
39
40 w := httptest.NewRecorder()
41 NewRouter(ctx).ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
42
43 if w.Code != http.StatusOK {
44 t.Fatalf("homepage status = %d, want %d", w.Code, http.StatusOK)
45 }
46 body := w.Body.String()
47 for _, expected := range []string{"public-project", `href="/public-project"`, "<script>Public project</script>", "<img src=x onerror="alert(1)">", "Public <em>profile</em> content."} {
48 if !strings.Contains(body, expected) {
49 t.Errorf("homepage does not contain %q:\n%s", expected, body)
50 }
51 }
52 for _, unexpected := range []string{"private-project", "hidden-project", `href="/.soft-serve"`, "<script>Public project</script>", `<img src=x onerror="alert(1)">`} {
53 if strings.Contains(body, unexpected) {
54 t.Errorf("homepage contains %q:\n%s", unexpected, body)
55 }
56 }
57 if got := w.Header().Get("Content-Security-Policy"); got != "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" {
58 t.Errorf("Content-Security-Policy = %q", got)
59 }
60 if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
61 t.Errorf("X-Content-Type-Options = %q", got)
62 }
63 if got := w.Header().Get("Referrer-Policy"); got != "same-origin" {
64 t.Errorf("Referrer-Policy = %q", got)
65 }
66}
67
68func TestHomepageWithoutProfileIsSuccessful(t *testing.T) {
69 ctx, be, _ := newLFSTestContext(t)
70 cfg := config.FromContext(ctx)
71 cfg.HTTP.WebUI.Enabled = true
72 allowKeyless := true
73 cfg.AllowKeyless = &allowKeyless
74 anonAccess := access.ReadOnlyAccess
75 cfg.AnonAccess = &anonAccess
76
77 owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
78 if err != nil {
79 t.Fatal(err)
80 }
81 createHomepageRepository(t, be, ctx, "public-project", owner, proto.RepositoryOptions{})
82
83 w := httptest.NewRecorder()
84 NewRouter(ctx).ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
85 if w.Code != http.StatusOK {
86 t.Fatalf("homepage without profile status = %d, want %d", w.Code, http.StatusOK)
87 }
88 if strings.Contains(w.Body.String(), "profile-heading") {
89 t.Fatalf("homepage unexpectedly renders profile: %s", w.Body.String())
90 }
91}
92
93func TestHomepageRemainsUnavailableWhenDisabled(t *testing.T) {
94 ctx, _, _ := newLFSTestContext(t)
95 w := httptest.NewRecorder()
96 NewRouter(ctx).ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
97 if w.Code != http.StatusNotFound {
98 t.Fatalf("disabled homepage status = %d, want %d", w.Code, http.StatusNotFound)
99 }
100}
101
102func createHomepageRepository(t *testing.T, be interface {
103 CreateRepository(context.Context, string, proto.User, proto.RepositoryOptions) (proto.Repository, error)
104}, ctx context.Context, name string, owner proto.User, options proto.RepositoryOptions) {
105 t.Helper()
106 if _, err := be.CreateRepository(ctx, name, owner, options); err != nil {
107 t.Fatal(err)
108 }
109}
110
111func commitHomepageFile(t *testing.T, dataPath, repository, name, contents string) {
112 t.Helper()
113 worktree := t.TempDir()
114 runHomepageGit(t, "clone", filepath.Join(dataPath, "repos", repository+".git"), worktree)
115 runHomepageGitIn(t, worktree, "config", "user.email", "test@example.com")
116 runHomepageGitIn(t, worktree, "config", "user.name", "Test User")
117 file := filepath.Join(worktree, filepath.FromSlash(name))
118 if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
119 t.Fatal(err)
120 }
121 if err := os.WriteFile(file, []byte(contents), 0o600); err != nil {
122 t.Fatal(err)
123 }
124 runHomepageGitIn(t, worktree, "add", name)
125 runHomepageGitIn(t, worktree, "commit", "-m", "add homepage content")
126 runHomepageGitIn(t, worktree, "push", "origin", "HEAD")
127}
128
129func runHomepageGit(t *testing.T, args ...string) {
130 t.Helper()
131 runHomepageGitCommand(t, "", args...)
132}
133
134func runHomepageGitIn(t *testing.T, dir string, args ...string) {
135 t.Helper()
136 runHomepageGitCommand(t, dir, args...)
137}
138
139func runHomepageGitCommand(t *testing.T, dir string, args ...string) {
140 t.Helper()
141 cmd := exec.Command("git", args...)
142 cmd.Dir = dir
143 if output, err := cmd.CombinedOutput(); err != nil {
144 t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output)
145 }
146}