package handlers import ( "context" "fmt" "io" "log/slog" "net" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" "time" "polar/internal/auth" "polar/internal/config" "polar/internal/db" "polar/internal/repo" "polar/internal/user" "polar/internal/web/views" "github.com/gofiber/fiber/v2" ) func setupTestApp(t *testing.T) (*fiber.App, *auth.Service, *user.Service, *repo.Service) { t.Helper() tmp := t.TempDir() cfg := config.Default() cfg.Storage.DataDir = tmp ctx := context.Background() database, err := db.Open(ctx, cfg.DBPath(), true) if err != nil { t.Fatalf("open test db: %v", err) } t.Cleanup(func() { _ = database.Close() }) authSvc := auth.NewService(database) userSvc := user.NewService(database) repoSvc := repo.NewService(database, cfg.ReposDir()) viewsEngine, err := views.NewEngine() if err != nil { t.Fatalf("new views engine: %v", err) } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) app := fiber.New(fiber.Config{ DisableStartupMessage: true, BodyLimit: 50 * 1024 * 1024, }) h := New(cfg, database, authSvc, userSvc, repoSvc, viewsEngine, logger) h.RegisterRoutes(app) return app, authSvc, userSvc, repoSvc } func TestHealthzLandingAndStatic(t *testing.T) { app, _, _, _ := setupTestApp(t) // 1. /healthz req := httptest.NewRequest(http.MethodGet, "/healthz", nil) resp, err := app.Test(req, 5000) if err != nil { t.Fatalf("healthz request failed: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200 on /healthz, got %d", resp.StatusCode) } if resp.Header.Get("X-Content-Type-Options") != "nosniff" { t.Fatalf("missing security header X-Content-Type-Options") } // 2. Landing page (RU & EN) + verify zero external asset requests reqRU := httptest.NewRequest(http.MethodGet, "/?lang=ru", nil) respRU, err := app.Test(reqRU, 5000) if err != nil || respRU.StatusCode != http.StatusOK { t.Fatalf("landing RU failed: %v", err) } bodyRU, _ := io.ReadAll(respRU.Body) htmlRU := string(bodyRU) if !strings.Contains(htmlRU, "Polar") || !strings.Contains(htmlRU, "hi-git-branch") { t.Fatalf("landing RU missing expected content or Hugeicons sprite") } if strings.Contains(htmlRU, "https://fonts.googleapis.com") || strings.Contains(htmlRU, "https://unpkg.com") || strings.Contains(htmlRU, "https://cdn.") { t.Fatalf("landing page must not make external CDN requests") } reqEN := httptest.NewRequest(http.MethodGet, "/?lang=en", nil) respEN, err := app.Test(reqEN, 5000) if err != nil || respEN.StatusCode != http.StatusOK { t.Fatalf("landing EN failed: %v", err) } bodyEN, _ := io.ReadAll(respEN.Body) if !strings.Contains(string(bodyEN), "Lightweight self-hosted Git hosting") { t.Fatalf("landing EN missing translated hero title") } // 3. Static embedded CSS & JS for _, asset := range []string{"/static/css/app.css", "/static/js/app.js", "/static/js/htmx.min.js"} { r := httptest.NewRequest(http.MethodGet, asset, nil) res, err := app.Test(r, 5000) if err != nil || res.StatusCode != http.StatusOK { t.Fatalf("embedded static asset %s failed: status=%d err=%v", asset, res.StatusCode, err) } } } func TestEndToEndGitSmartHTTPAndPrivateRepos(t *testing.T) { app, authSvc, userSvc, repoSvc := setupTestApp(t) ctx := context.Background() // Start real TCP listener for system git CLI ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen tcp: %v", err) } defer ln.Close() go func() { _ = app.Listener(ln) }() baseURL := "http://" + ln.Addr().String() // Create owner user + personal access token owner, err := userSvc.Register(ctx, "alice", "alice@example.com", "strongpassword", true) if err != nil { t.Fatalf("register owner: %v", err) } rawToken, _, err := authSvc.CreateToken(ctx, owner.ID, "E2E Token") if err != nil { t.Fatalf("create token: %v", err) } // Create a public repo and a private repo pubRepo, err := repoSvc.Create(ctx, owner, "engine", "Analytical Engine", false) if err != nil { t.Fatalf("create public repo: %v", err) } privRepo, err := repoSvc.Create(ctx, owner, "vault", "Secret Notes", true) if err != nil { t.Fatalf("create private repo: %v", err) } // 1. Verify private repo is blocked on Web UI and Git HTTP without credentials webPrivResp, err := http.Get(baseURL + "/alice/vault") if err != nil { t.Fatalf("GET /alice/vault: %v", err) } _ = webPrivResp.Body.Close() if webPrivResp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404 for unauthenticated private repo web request, got %d", webPrivResp.StatusCode) } gitPrivResp, err := http.Get(baseURL + "/alice/vault.git/info/refs?service=git-upload-pack") if err != nil { t.Fatalf("GET private info/refs: %v", err) } _ = gitPrivResp.Body.Close() if gitPrivResp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401 for unauthenticated private git info/refs, got %d", gitPrivResp.StatusCode) } // 2. Push commits to public repo over HTTP using Personal Access Token workDir := filepath.Join(t.TempDir(), "work") if err := os.MkdirAll(workDir, 0o755); err != nil { t.Fatalf("mkdir work: %v", err) } runGitCmd := func(dir string, args ...string) { t.Helper() cctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() cmd := exec.CommandContext(cctx, "git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v failed: %v\nOutput: %s", args, err, string(out)) } } runGitCmd(workDir, "init", "-b", "main") runGitCmd(workDir, "config", "user.name", "Alice Lovelace") runGitCmd(workDir, "config", "user.email", "alice@example.com") if err := os.WriteFile(filepath.Join(workDir, "README.md"), []byte("# Analytical Engine\n\nHello from **Alice**!\n"), 0o644); err != nil { t.Fatalf("write README.md: %v", err) } if err := os.WriteFile(filepath.Join(workDir, "main.go"), []byte("package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Alice\")\n}\n"), 0o644); err != nil { t.Fatalf("write main.go: %v", err) } runGitCmd(workDir, "add", "README.md", "main.go") runGitCmd(workDir, "commit", "-m", "Initial commit with README and main.go") authedPubURL := fmt.Sprintf("http://%s:%s@%s/alice/%s.git", owner.Username, rawToken, ln.Addr().String(), pubRepo.Name) runGitCmd(workDir, "push", "-u", authedPubURL, "main") // Also push to private repo with token authedPrivURL := fmt.Sprintf("http://%s:%s@%s/alice/%s.git", owner.Username, rawToken, ln.Addr().String(), privRepo.Name) runGitCmd(workDir, "push", authedPrivURL, "main") // 3. Clone public repo anonymously and private repo with token clonePubDir := filepath.Join(t.TempDir(), "clone-pub") runGitCmd(t.TempDir(), "clone", baseURL+"/alice/engine.git", clonePubDir) if _, err := os.Stat(filepath.Join(clonePubDir, "main.go")); err != nil { t.Fatalf("cloned public repo missing main.go: %v", err) } clonePrivDir := filepath.Join(t.TempDir(), "clone-priv") runGitCmd(t.TempDir(), "clone", authedPrivURL, clonePrivDir) if _, err := os.Stat(filepath.Join(clonePrivDir, "README.md")); err != nil { t.Fatalf("cloned private repo missing README.md: %v", err) } // 4. Verify Web UI renders repository tree, README Markdown, syntax-highlighted blob, commits, and diff checkEndpoint := func(urlPath, expectedSubstring string) { t.Helper() resp, err := http.Get(baseURL + urlPath) if err != nil { t.Fatalf("GET %s failed: %v", urlPath, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("GET %s returned status %d", urlPath, resp.StatusCode) } b, _ := io.ReadAll(resp.Body) if !strings.Contains(string(b), expectedSubstring) { t.Fatalf("GET %s missing expected %q", urlPath, expectedSubstring) } } checkEndpoint("/alice/engine", "Analytical Engine") checkEndpoint("/alice/engine/blob/main/main.go", "Println") checkEndpoint("/alice/engine/raw/main/main.go", "package main") checkEndpoint("/alice/engine/commits/main", "Initial commit with README and main.go") checkEndpoint("/alice/engine/branches", "main") checkEndpoint("/alice", "alice") // 5. Verify ZIP archive download zipResp, err := http.Get(baseURL + "/alice/engine/archive/main.zip") if err != nil { t.Fatalf("GET archive zip failed: %v", err) } defer zipResp.Body.Close() if zipResp.StatusCode != http.StatusOK { t.Fatalf("GET archive zip status %d", zipResp.StatusCode) } zipBytes, _ := io.ReadAll(zipResp.Body) if len(zipBytes) < 4 || zipBytes[0] != 'P' || zipBytes[1] != 'K' { t.Fatalf("expected valid ZIP magic bytes, got len=%d", len(zipBytes)) } } func TestFeaturesTogglesAndProfile(t *testing.T) { app, _, userSvc, repoSvc := setupTestApp(t) ctx := context.Background() // 1. Create a user u, err := userSvc.Register(ctx, "rin", "rin@example.com", "pass12345", true) if err != nil { t.Fatalf("register: %v", err) } // 2. Update user profile with avatar and details avatarData := "data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAADwAQCdASoBAAEAAQAcJaACdLoAAP7/2wAA" if err := userSvc.UpdateProfileDetails(ctx, u.ID, "rin@example.com", "Rin Tester", "Developer", "Tokyo", "https://example.com"); err != nil { t.Fatalf("update profile details: %v", err) } if err := userSvc.UpdateAvatar(ctx, u.ID, avatarData); err != nil { t.Fatalf("update avatar: %v", err) } // Check profile page reqProfile := httptest.NewRequest(http.MethodGet, "/rin", nil) respProfile, err := app.Test(reqProfile, 5000) if err != nil || respProfile.StatusCode != http.StatusOK { t.Fatalf("profile page failed: status=%d err=%v", respProfile.StatusCode, err) } profBody, _ := io.ReadAll(respProfile.Body) profHTML := string(profBody) if !strings.Contains(profHTML, "Rin Tester") || !strings.Contains(profHTML, "data:image/webp") { t.Fatalf("profile page missing updated name or avatar") } // 3. Create a repo and an issue r, err := repoSvc.Create(ctx, u, "test-repo", "A test repo", false) if err != nil { t.Fatalf("create repo: %v", err) } issue, err := repoSvc.CreateIssue(ctx, r.ID, u.ID, "First Bug", "Something went wrong") if err != nil { t.Fatalf("create issue: %v", err) } if issue.Number != 1 { t.Fatalf("expected issue number 1, got %d", issue.Number) } if err := repoSvc.CreateIssueComment(ctx, issue.ID, u.ID, "Fixed in commit 123"); err != nil { t.Fatalf("add issue comment: %v", err) } reqIssues := httptest.NewRequest(http.MethodGet, "/rin/test-repo/issues", nil) respIssues, err := app.Test(reqIssues, 5000) if err != nil || respIssues.StatusCode != http.StatusOK { t.Fatalf("issues list failed: status=%d err=%v", respIssues.StatusCode, err) } issBody, _ := io.ReadAll(respIssues.Body) if !strings.Contains(string(issBody), "First Bug") { t.Fatalf("issues list missing issue title") } reqIssueDetail := httptest.NewRequest(http.MethodGet, "/rin/test-repo/issues/1", nil) respIssueDetail, err := app.Test(reqIssueDetail, 5000) if err != nil || respIssueDetail.StatusCode != http.StatusOK { t.Fatalf("issue detail failed: status=%d err=%v", respIssueDetail.StatusCode, err) } issDetailBody, _ := io.ReadAll(respIssueDetail.Body) if !strings.Contains(string(issDetailBody), "Fixed in commit 123") { t.Fatalf("issue detail missing comment") } }