package app import ( "encoding/base64" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "golang.org/x/crypto/bcrypt" ) func csrfFromResponse(t *testing.T, rr *httptest.ResponseRecorder) *http.Cookie { t.Helper() for _, c := range rr.Result().Cookies() { if c.Name == webCSRFCookie { return c } } t.Fatalf("missing csrf cookie in %#v", rr.Result().Cookies()) return nil } func TestWebReposAndLogin(t *testing.T) { s, mock, cleanup := newMockServer(t) defer cleanup() repo := Repository{ID: 10, OwnerUserID: 1, Owner: "alice", Name: "demo", Visibility: "public", Description: "desc", DefaultBranch: "main"} mock.ExpectQuery("FROM repositories r JOIN users u").WithArgs("demo", "%demo%", "%demo%").WillReturnRows(repoRows(repo)) rr := httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/repos?q=demo", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "alice/demo") { t.Fatalf("repos status=%d body=%s", rr.Code, rr.Body.String()) } csrf := csrfFromResponse(t, rr) rr = httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/login", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "

Login

") { t.Fatalf("login GET status=%d body=%s", rr.Code, rr.Body.String()) } hash, err := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost) if err != nil { t.Fatal(err) } mock.ExpectQuery("SELECT id, email, username, is_admin, password_hash FROM users").WithArgs("alice", "alice").WillReturnRows(sqlmock.NewRows([]string{"id", "email", "username", "is_admin", "password_hash"}).AddRow(int64(1), "alice@example.com", "alice", false, string(hash))) mock.ExpectExec("INSERT INTO auth_tokens").WithArgs(int64(1), sqlmock.AnyArg(), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1)) rr = httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader("_csrf="+csrf.Value+"&login=alice&password=password123")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(csrf) s.ServeHTTP(rr, req) if rr.Code != http.StatusSeeOther || rr.Header().Get("Location") != "/repos" { t.Fatalf("login POST status=%d location=%q body=%s", rr.Code, rr.Header().Get("Location"), rr.Body.String()) } foundAuth := false for _, c := range rr.Result().Cookies() { if c.Name == webAuthCookie && c.Value != "" && c.HttpOnly { foundAuth = true } } if !foundAuth { t.Fatalf("missing auth cookie: %#v", rr.Result().Cookies()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func addWebRenderFilesToRepo(t *testing.T, bare string) { t.Helper() work := filepath.Join(t.TempDir(), "render-work") if err := runGit("", "clone", bare, work); err != nil { t.Fatal(err) } if err := runGit(work, "checkout", "main"); err != nil { t.Fatal(err) } for _, args := range [][]string{{"config", "user.name", "Test User"}, {"config", "user.email", "test@example.com"}} { if err := runGit(work, args...); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(work, "docs.md"), []byte("# Rendered Title\n\nThis is **markdown**.\n"), 0644); err != nil { t.Fatal(err) } png, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=") if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(work, "logo.png"), png, 0644); err != nil { t.Fatal(err) } if err := runGit(work, "add", "docs.md", "logo.png"); err != nil { t.Fatal(err) } if err := runGit(work, "commit", "-m", "add render files"); err != nil { t.Fatal(err) } if err := runGit(work, "push", "origin", "HEAD:main"); err != nil { t.Fatal(err) } } func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) { requireGitForApp(t) s, mock, cleanup := newMockServer(t) defer cleanup() user := User{ID: 1, Email: "alice@example.com", Username: "alice"} repo := Repository{ID: 10, OwnerUserID: user.ID, Owner: "alice", Name: "demo", Visibility: "public", Description: "desc", DefaultBranch: "main", CreatedAt: time.Now(), UpdatedAt: time.Now()} bare := s.repoPath(repo.Owner, repo.Name) if err := os.MkdirAll(filepath.Dir(bare), 0755); err != nil { t.Fatal(err) } seedRepoWithFeatureBranch(t, bare) addWebRenderFilesToRepo(t, bare) expectLoadRepo(mock, "alice", "demo", repo) rr := httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "README.md") || !strings.Contains(rr.Body.String(), "Clone:") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "2 commits") { t.Fatalf("repo page status=%d body=%s", rr.Code, rr.Body.String()) } csrf := csrfFromResponse(t, rr) refs, err := gitRefs(bare, "refs/heads") if err != nil { t.Fatal(err) } var mainHash string for _, ref := range refs { if ref.Name == "main" { mainHash = ref.Commit } } if mainHash == "" { t.Fatal("missing main branch hash") } expectLoadRepo(mock, "alice", "demo", repo) rr = httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/commits?ref=main", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commits for alice/demo") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "initial") || !strings.Contains(rr.Body.String(), "/alice/demo/commit?ref=main") { t.Fatalf("commit history status=%d body=%s", rr.Code, rr.Body.String()) } expectLoadRepo(mock, "alice", "demo", repo) rr = httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/commit?ref=main&id="+mainHash, nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commit ") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "diff --git") || !strings.Contains(rr.Body.String(), "docs.md") { t.Fatalf("commit view status=%d body=%s", rr.Code, rr.Body.String()) } expectLoadRepo(mock, "alice", "demo", repo) rr = httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/blob?ref=main&path=README.md", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "hello") || !strings.Contains(rr.Body.String(), `/alice/demo/tree?ref=main&path=`) { t.Fatalf("blob page status=%d body=%s", rr.Code, rr.Body.String()) } expectLoadRepo(mock, "alice", "demo", repo) rr = httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/blob?ref=main&path=docs.md", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "

Rendered Title

") || strings.Contains(rr.Body.String(), "# Rendered Title") { t.Fatalf("markdown page status=%d body=%s", rr.Code, rr.Body.String()) } expectLoadRepo(mock, "alice", "demo", repo) rr = httptest.NewRecorder() s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/blob?ref=main&path=logo.png", nil)) if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `