Parent directory

primitives_test.go

6454 bytes
  1package pages
  2
  3import (
  4	"errors"
  5	"os"
  6	"os/exec"
  7	"path/filepath"
  8	"strings"
  9	"testing"
 10	"time"
 11
 12	"github.com/charmbracelet/soft-serve/git"
 13	"github.com/charmbracelet/soft-serve/pkg/access"
 14	"github.com/charmbracelet/soft-serve/pkg/proto"
 15)
 16
 17type testRepository struct {
 18	name    string
 19	private bool
 20	hidden  bool
 21	updated time.Time
 22}
 23
 24func (r testRepository) ID() int64                      { return 0 }
 25func (r testRepository) Name() string                   { return r.name }
 26func (r testRepository) ProjectName() string            { return "" }
 27func (r testRepository) Description() string            { return "" }
 28func (r testRepository) IsPrivate() bool                { return r.private }
 29func (r testRepository) IsMirror() bool                 { return false }
 30func (r testRepository) IsHidden() bool                 { return r.hidden }
 31func (r testRepository) UserID() int64                  { return 0 }
 32func (r testRepository) CreatedAt() time.Time           { return r.updated }
 33func (r testRepository) UpdatedAt() time.Time           { return r.updated }
 34func (r testRepository) Open() (*git.Repository, error) { return nil, errors.New("not used") }
 35
 36func TestIsPublicReadable(t *testing.T) {
 37	public := testRepository{name: "public"}
 38	private := testRepository{name: "private", private: true}
 39	if !IsPublicReadable(public, access.ReadOnlyAccess) {
 40		t.Fatal("public read-only repository is not readable")
 41	}
 42	if IsPublicReadable(private, access.AdminAccess) {
 43		t.Fatal("private repository must not be page-readable")
 44	}
 45	if IsPublicReadable(public, access.NoAccess) {
 46		t.Fatal("anonymous no-access repository is readable")
 47	}
 48}
 49
 50func TestFilterHomepageRepositories(t *testing.T) {
 51	newer := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC)
 52	input := []proto.Repository{
 53		testRepository{name: "zebra", updated: newer},
 54		testRepository{name: "alpha", updated: newer},
 55		testRepository{name: "older", updated: newer.Add(-time.Hour)},
 56		testRepository{name: "hidden", hidden: true, updated: newer},
 57		testRepository{name: ".soft-serve", updated: newer},
 58		testRepository{name: "unreadable", updated: newer},
 59	}
 60	visible := FilterHomepageRepositories(input, func(repo proto.Repository) bool {
 61		return repo.Name() != "unreadable"
 62	})
 63	got := make([]string, len(visible))
 64	for i, repo := range visible {
 65		got[i] = repo.Name()
 66	}
 67	want := []string{"alpha", "zebra", "older"}
 68	if strings.Join(got, ",") != strings.Join(want, ",") {
 69		t.Fatalf("visible repositories = %v, want %v", got, want)
 70	}
 71}
 72
 73func TestResolveRefAndValidation(t *testing.T) {
 74	repo := newTestRepository(t, map[string]string{"README.md": "hello"})
 75	runGit(t, repo.Path, "branch", "feature/with-slash")
 76
 77	ref, err := ResolveRef(repo, "refs/heads/feature/with-slash")
 78	if err != nil || ref.Name().String() != "refs/heads/feature/with-slash" {
 79		t.Fatalf("ResolveRef slash ref = %v, %v", ref, err)
 80	}
 81	if _, err := ResolveRef(repo, "feature/with-slash^{commit}"); !errors.Is(err, ErrInvalidRef) {
 82		t.Fatalf("ResolveRef arbitrary revision error = %v, want ErrInvalidRef", err)
 83	}
 84	for _, invalid := range []string{"", "abc", strings.Repeat("z", 40), strings.Repeat("a", 39)} {
 85		if invalid != "" && ValidCommitHash(invalid) {
 86			t.Fatalf("ValidCommitHash(%q) = true", invalid)
 87		}
 88	}
 89	if !ValidCommitHash(strings.Repeat("a", 40)) || !ValidCommitHash(strings.Repeat("B", 64)) {
 90		t.Fatal("valid complete object IDs rejected")
 91	}
 92	for _, invalid := range []string{"/etc", "../outside", "a/../../outside", "\x00name"} {
 93		if _, err := ValidateTreePath(invalid); !errors.Is(err, ErrInvalidTreePath) {
 94			t.Fatalf("ValidateTreePath(%q) error = %v", invalid, err)
 95		}
 96	}
 97	if got, err := ValidateTreePath("dir/../README.md"); err != nil || got != "README.md" {
 98		t.Fatalf("ValidateTreePath cleaned = %q, %v", got, err)
 99	}
100}
101
102func TestLoadBlobRejectsOversizeBeforeRendering(t *testing.T) {
103	repo := newTestRepository(t, map[string]string{"large.txt": strings.Repeat("x", 32)})
104	ref, err := ResolveRef(repo, "")
105	if err != nil {
106		t.Fatal(err)
107	}
108	entry, err := TreeEntry(repo, ref, "large.txt")
109	if err != nil {
110		t.Fatal(err)
111	}
112	if _, err := LoadBlob(entry, 16); !errors.Is(err, ErrContentTooLarge) {
113		t.Fatalf("LoadBlob error = %v, want ErrContentTooLarge", err)
114	}
115}
116
117func TestRenderingEscapesUntrustedContent(t *testing.T) {
118	markdown, err := RenderMarkdown([]byte("<script>alert(1)</script><b>raw</b>\n\n[jump](javascript:alert(1))"))
119	if err != nil {
120		t.Fatal(err)
121	}
122	output := string(markdown)
123	for _, unsafe := range []string{"<script", "<b>raw</b>", "javascript:"} {
124		if strings.Contains(strings.ToLower(output), unsafe) {
125			t.Fatalf("markdown output contains %q: %s", unsafe, output)
126		}
127	}
128	source, err := RenderSource("example.go", []byte("<script>alert(1)</script>"))
129	if err != nil {
130		t.Fatal(err)
131	}
132	if strings.Contains(string(source), "<script>") || !strings.Contains(string(source), "&lt;") {
133		t.Fatalf("source was not escaped: %s", source)
134	}
135}
136
137func TestURLsAndRawMetadata(t *testing.T) {
138	got := TreeURL("team/a repo", "refs/heads/feature/with slash", "dir/a&b.txt")
139	want := "/team/a%20repo/@/tree?path=dir%2Fa%26b.txt&ref=refs%2Fheads%2Ffeature%2Fwith+slash"
140	if got != want {
141		t.Fatalf("TreeURL = %q, want %q", got, want)
142	}
143	unsafe := RawFileMetadata("payload.svg")
144	if unsafe.Inline || unsafe.ContentDisposition == "" {
145		t.Fatalf("SVG metadata = %#v, want attachment", unsafe)
146	}
147	if safe := RawFileMetadata("photo.PNG"); !safe.Inline || safe.ContentType != "image/png" {
148		t.Fatalf("PNG metadata = %#v", safe)
149	}
150}
151
152func newTestRepository(t *testing.T, files map[string]string) *git.Repository {
153	t.Helper()
154	dir := t.TempDir()
155	runGit(t, dir, "init", "-q")
156	runGit(t, dir, "config", "user.email", "test@example.com")
157	runGit(t, dir, "config", "user.name", "Test User")
158	for name, contents := range files {
159		filename := filepath.Join(dir, filepath.FromSlash(name))
160		if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
161			t.Fatal(err)
162		}
163		if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
164			t.Fatal(err)
165		}
166	}
167	runGit(t, dir, "add", ".")
168	runGit(t, dir, "commit", "-qm", "initial")
169	repo, err := git.Open(dir)
170	if err != nil {
171		t.Fatal(err)
172	}
173	return repo
174}
175
176func runGit(t *testing.T, dir string, args ...string) {
177	t.Helper()
178	cmd := exec.Command("git", args...)
179	cmd.Dir = dir
180	if output, err := cmd.CombinedOutput(); err != nil {
181		t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output)
182	}
183}