Parent directory

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"`, "&lt;script&gt;Public project&lt;/script&gt;", "&lt;img src=x onerror=&#34;alert(1)&#34;&gt;", "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}