package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestRawEndpointHardening 是 P0-3 的回归测试:raw 端点不得把仓库里的可执行
// 内容以内联方式返回。修复前 evil.html 会带着 text/html + inline 在 gitcat
// 自己的源下渲染,提交一个文件等于对所有访客执行脚本。
func TestRawEndpointHardening(t *testing.T) {
srv, st, _, _ := newTestServer(t)
dir := makeRepo(t, srv, st, "files", nil)
commitFiles(t, dir, "add files", map[string]string{
"evil.html": ``,
"logo.svg": ``,
"notes.txt": "hello",
"pic.png": "\x89PNG\r\n\x1a\nfake",
})
cases := []struct {
file string
wantDisp string
denyHTML bool
}{
{"evil.html", "attachment", true},
{"logo.svg", "attachment", true},
{"notes.txt", "inline", false},
{"pic.png", "inline", false},
}
for _, tc := range cases {
t.Run(tc.file, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/files/raw/main/"+tc.file, nil)
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("状态码 = %d,期望 200", w.Code)
}
ct := w.Header().Get("Content-Type")
if disp := w.Header().Get("Content-Disposition"); !strings.Contains(disp, tc.wantDisp) {
t.Errorf("Content-Disposition = %q,期望包含 %q", disp, tc.wantDisp)
}
if tc.denyHTML && (strings.Contains(ct, "text/html") || strings.Contains(ct, "svg")) {
t.Errorf("可执行类型被原样返回: Content-Type = %q", ct)
}
if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "sandbox") {
t.Errorf("缺少 CSP sandbox,实际 = %q", got)
}
if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
t.Errorf("X-Content-Type-Options = %q,期望 nosniff", got)
}
})
}
}
// TestRawRejectsOptionInjection 确认 ref 参数无法被 git 当成选项。
func TestRawRejectsOptionInjection(t *testing.T) {
srv, st, _, _ := newTestServer(t)
makeRepo(t, srv, st, "safe", nil)
for _, ref := range []string{"--upload-pack=evil", "-x", "a%20b", "a..b", "!bang"} {
r := httptest.NewRequest(http.MethodGet, "/safe/raw/"+ref+"/f.txt", nil)
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, r)
if w.Code == http.StatusOK {
t.Errorf("非法 ref %q 被放行", ref)
}
}
}
// TestPagesHaveCSP 确认所有页面都带上了 CSP。
func TestPagesHaveCSP(t *testing.T) {
srv, st, _, _ := newTestServer(t)
makeRepo(t, srv, st, "proj", nil)
commitFiles(t, srv.repoPath("proj"), "init", map[string]string{"a.txt": "hi"})
for _, path := range []string{"/", "/proj", "/login"} {
r := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, r)
if got := w.Header().Get("Content-Security-Policy"); !strings.Contains(got, "default-src 'self'") {
t.Errorf("%s 缺少 CSP: %q", path, got)
}
}
}
// TestBlobSkipsOversizedFile 确认超大文件不进内存(只渲染下载入口)。
func TestBlobSkipsOversizedFile(t *testing.T) {
srv, st, _, _ := newTestServer(t)
dir := makeRepo(t, srv, st, "big", nil)
big := strings.Repeat("A", 3<<20) // 3MB > 2MB 预览上限
commitFiles(t, dir, "big file", map[string]string{"big.txt": big})
r := httptest.NewRequest(http.MethodGet, "/big/blob/main/big.txt", nil)
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("状态码 = %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "超过 2 MB") {
t.Error("超大文件应显示「跳过预览」提示")
}
if strings.Contains(body, "codeline") {
t.Error("超大文件不应渲染代码行")
}
}