| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package main | |
| import ( | |
| "context" | |
| "io/fs" | |
| "net/http" | |
| "net/http/httptest" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "slices" | |
| "strings" | |
| "testing" | |
| "golang.org/x/pkgsite/internal/proxy/proxytest" | |
| "golang.org/x/tools/txtar" | |
| ) | |
| func TestEvaldocServer(t *testing.T) { | |
| testModules := []*proxytest.Module{ | |
| { | |
| ModulePath: "example.com/mod", | |
| Version: "v1.0.0", | |
| Files: map[string]string{ | |
| "root.go": "package mod", | |
| "foo/foo.go": "package foo\n\n// DocFunc has doc.\nfunc DocFunc() {}\n\nfunc UndocFunc() {}\n", | |
| "foo/foo_test.go": "package foo\n\nfunc TestFoo(t *testing.T) {}\n", | |
| "internal/secret/secret.go": "package secret", | |
| "testdata/ignored.go": "package testdata", | |
| "vendor/bar/bar.go": "package bar", | |
| ".ignored/ignored.go": "package ignored", | |
| "nodirs/doc.txt": "just text", | |
| }, | |
| }, | |
| } | |
| proxyClient, teardown := proxytest.SetupTestClient(t, testModules) | |
| defer teardown() | |
| ctx := context.Background() | |
| zipReader, err := proxyClient.Zip(ctx, "example.com/mod", "v1.0.0") | |
| if err != nil { | |
| t.Fatalf("proxyClient.Zip: %v", err) | |
| } | |
| contentDir, err := fs.Sub(zipReader, "example.com/mod@v1.0.0") | |
| if err != nil { | |
| t.Fatalf("fs.Sub: %v", err) | |
| } | |
| s, err := newServer("example.com/mod", "v1.0.0", contentDir) | |
| if err != nil { | |
| t.Fatalf("newServer: %v", err) | |
| } | |
| handler := s | |
| testCases := []struct { | |
| name string | |
| path string | |
| wantStatus int | |
| wantSubstrs []string | |
| }{ | |
| { | |
| name: "styles.css returns stylesheet", | |
| path: "/styles.css", | |
| wantStatus: http.StatusOK, | |
| wantSubstrs: []string{ | |
| `body { font-family: sans-serif;`, | |
| `.has-doc { color: #1a7f37;`, | |
| `.need-doc { color: #cf222e;`, | |
| }, | |
| }, | |
| { | |
| name: "root path lists import paths with links and symbol counts", | |
| path: "/", | |
| wantStatus: http.StatusOK, | |
| wantSubstrs: []string{ | |
| `<link rel="stylesheet" href="/styles.css">`, | |
| `<h1>Module example.com/mod@v1.0.0 | |
| <span class="has-doc">1</span> | |
| <span class="need-doc">1</span> | |
| <span class="need-pct">50%</span> | |
| </h1>`, | |
| `<a href="/example.com/mod/foo">example.com/mod/foo</a> | |
| <span class="has-doc">1</span> | |
| <span class="need-doc">1</span> | |
| <span class="need-pct">50%</span>`, | |
| }, | |
| }, | |
| { | |
| name: "directory view lists package files with links and symbol counts", | |
| path: "/example.com/mod/foo", | |
| wantStatus: http.StatusOK, | |
| wantSubstrs: []string{ | |
| `<link rel="stylesheet" href="/styles.css">`, | |
| `<a href="/example.com/mod/foo/foo.go">foo.go</a> | |
| <span class="has-doc">1</span> | |
| <span class="need-doc">1</span> | |
| <span class="need-pct">50%</span>`, | |
| }, | |
| }, | |
| { | |
| name: "file view shows file content with symbol highlighting", | |
| path: "/example.com/mod/foo/foo.go", | |
| wantStatus: http.StatusOK, | |
| wantSubstrs: []string{ | |
| `<link rel="stylesheet" href="/styles.css">`, | |
| `package foo`, | |
| `<span class="has-doc">DocFunc</span>`, | |
| `<span class="need-doc">UndocFunc</span>`, | |
| }, | |
| }, | |
| { | |
| name: "internal directory 404", | |
| path: "/example.com/mod/internal/secret", | |
| wantStatus: http.StatusNotFound, | |
| }, | |
| { | |
| name: "test file 404", | |
| path: "/example.com/mod/foo/foo_test.go", | |
| wantStatus: http.StatusNotFound, | |
| }, | |
| { | |
| name: "non-go file 404", | |
| path: "/example.com/mod/nodirs/doc.txt", | |
| wantStatus: http.StatusNotFound, | |
| }, | |
| { | |
| name: "non-existent directory 404", | |
| path: "/example.com/mod/nonexistent", | |
| wantStatus: http.StatusNotFound, | |
| }, | |
| { | |
| name: "non-existent file 404", | |
| path: "/example.com/mod/foo/nonexistent.go", | |
| wantStatus: http.StatusNotFound, | |
| }, | |
| } | |
| for _, tc := range testCases { | |
| t.Run(tc.name, func(t *testing.T) { | |
| req := httptest.NewRequest(http.MethodGet, tc.path, nil) | |
| w := httptest.NewRecorder() | |
| handler.ServeHTTP(w, req) | |
| if w.Code != tc.wantStatus { | |
| t.Fatalf("HTTP status = %d, want %d (body: %s)", w.Code, tc.wantStatus, w.Body.String()) | |
| } | |
| body := w.Body.String() | |
| for _, want := range tc.wantSubstrs { | |
| if !strings.Contains(body, want) { | |
| t.Errorf("body does not contain %q; body:\n%s", want, body) | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| func TestLocalDirectoryFS(t *testing.T) { | |
| dir := t.TempDir() | |
| goMod := "module example.com/localpkg\n\ngo 1.22\n" | |
| if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil { | |
| t.Fatalf("WriteFile go.mod: %v", err) | |
| } | |
| mainGo := "package main\n\n// Main is main.\nfunc main() {}\n" | |
| if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainGo), 0644); err != nil { | |
| t.Fatalf("WriteFile main.go: %v", err) | |
| } | |
| s, err := newServer("example.com/localpkg", "local", os.DirFS(dir)) | |
| if err != nil { | |
| t.Fatalf("newServer local dir: %v", err) | |
| } | |
| req := httptest.NewRequest(http.MethodGet, "/", nil) | |
| w := httptest.NewRecorder() | |
| s.ServeHTTP(w, req) | |
| if w.Code != http.StatusOK { | |
| t.Fatalf("HTTP status = %d, want %d", w.Code, http.StatusOK) | |
| } | |
| if !strings.Contains(w.Body.String(), "example.com/localpkg") { | |
| t.Errorf("body does not contain example.com/localpkg; body:\n%s", w.Body.String()) | |
| } | |
| } | |
| func TestResolveLocalPath(t *testing.T) { | |
| if runtime.GOOS != "linux" { | |
| t.Skip("GOOS is not linux") | |
| } | |
| t.Setenv("HOME", "/fake/home") | |
| got, err := resolveLocalPath(".") | |
| if err != nil { | |
| t.Fatalf("resolveLocalPath(.): %v", err) | |
| } | |
| want, _ := filepath.Abs(".") | |
| if got != want { | |
| t.Errorf("resolveLocalPath(.) = %q, want %q", got, want) | |
| } | |
| gotHome, err := resolveLocalPath("~/sub") | |
| if err != nil { | |
| t.Fatalf("resolveLocalPath(~/sub): %v", err) | |
| } | |
| wantHome := filepath.Join("/fake/home", "sub") | |
| if gotHome != wantHome { | |
| t.Errorf("resolveLocalPath(~/sub) = %q, want %q", gotHome, wantHome) | |
| } | |
| } | |
| func TestGetContentDirEmpty(t *testing.T) { | |
| _, _, _, err := getContentDir("") | |
| if err == nil { | |
| t.Fatal("getContentDir(\"\") expected error, got nil") | |
| } | |
| } | |
| func TestSortingOrder(t *testing.T) { | |
| testModules := []*proxytest.Module{ | |
| { | |
| ModulePath: "example.com/sortmod", | |
| Version: "v1.0.0", | |
| Files: map[string]string{ | |
| "pkgA/a.go": "package pkgA\n\n// A1 doc\nfunc A1() {}\nfunc A2() {}\n", // 1 red | |
| "pkgB/b.go": "package pkgB\n\nfunc B1() {}\nfunc B2() {}\nfunc B3() {}\n", // 3 red | |
| "pkgC/c1.go": "package pkgC\n\nfunc C1() {}\n", // 1 red | |
| "pkgC/c2.go": "package pkgC\n\nfunc C2() {}\nfunc C3() {}\n", // 2 red (total pkgC: 3 red) | |
| }, | |
| }, | |
| } | |
| proxyClient, teardown := proxytest.SetupTestClient(t, testModules) | |
| defer teardown() | |
| ctx := context.Background() | |
| zipReader, err := proxyClient.Zip(ctx, "example.com/sortmod", "v1.0.0") | |
| if err != nil { | |
| t.Fatalf("proxyClient.Zip: %v", err) | |
| } | |
| contentDir, err := fs.Sub(zipReader, "example.com/sortmod@v1.0.0") | |
| if err != nil { | |
| t.Fatalf("fs.Sub: %v", err) | |
| } | |
| s, err := newServer("example.com/sortmod", "v1.0.0", contentDir) | |
| if err != nil { | |
| t.Fatalf("newServer: %v", err) | |
| } | |
| wantImportPaths := []string{ | |
| "example.com/sortmod/pkgB", | |
| "example.com/sortmod/pkgC", | |
| "example.com/sortmod/pkgA", | |
| } | |
| var gotImportPaths []string | |
| for _, item := range s.importPaths { | |
| gotImportPaths = append(gotImportPaths, item.ImportPath) | |
| } | |
| if !slices.Equal(gotImportPaths, wantImportPaths) { | |
| t.Errorf("importPaths sorting mismatch: got %v, want %v", gotImportPaths, wantImportPaths) | |
| } | |
| wantFiles := []string{"c2.go", "c1.go"} | |
| var gotFiles []string | |
| for _, item := range s.dirFiles["example.com/sortmod/pkgC"] { | |
| gotFiles = append(gotFiles, item.Filename) | |
| } | |
| if !slices.Equal(gotFiles, wantFiles) { | |
| t.Errorf("file sorting mismatch in pkgC: got %v, want %v", gotFiles, wantFiles) | |
| } | |
| } | |
| func TestPackageDirs(t *testing.T) { | |
| data, err := os.ReadFile(filepath.Join("testdata", "package_dirs.txtar")) | |
| if err != nil { | |
| t.Fatalf("os.ReadFile: %v", err) | |
| } | |
| archive := txtar.Parse(data) | |
| fsys, err := txtar.FS(archive) | |
| if err != nil { | |
| t.Fatalf("txtar.FS: %v", err) | |
| } | |
| got, err := packageDirs(fsys) | |
| if err != nil { | |
| t.Fatalf("packageDirs: %v", err) | |
| } | |
| want := []string{".", "foo"} | |
| if !slices.Equal(got, want) { | |
| t.Errorf("packageDirs() = %v, want %v", got, want) | |
| } | |
| } |