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), "<") {
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}