blob: d338bf32ee7ee63ac859cf8ee8e94a4a42684c49 [file]
// Copyright 2025 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 frontend
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"golang.org/x/pkgsite/internal"
)
func expectedCodeWikiURL(baseURL, path string) string {
return fmt.Sprintf("%s/%s?utm_source=first_party_link&utm_medium=go_pkg_web&utm_campaign=%s", baseURL, path, path)
}
func TestCodeWikiURLGenerator(t *testing.T) {
// The log package is periodically used to log warnings on a
// separate goroutine, which can pollute test output.
// For this test, we can discard all of that output.
log.SetOutput(io.Discard)
t.Cleanup(func() {
log.SetOutput(os.Stderr)
})
mux := http.NewServeMux()
mux.HandleFunc("/_/exists/github.com/owner/repo", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("/_/exists/github.com/golang/glog", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
oldCodeWikiURLBase := codeWikiURLBase
oldCodeWikiExistsURL := codeWikiExistsURL
codeWikiURLBase = server.URL + "/"
codeWikiExistsURL = server.URL + "/_/exists/"
t.Cleanup(func() {
codeWikiURLBase = oldCodeWikiURLBase
codeWikiExistsURL = oldCodeWikiExistsURL
})
testCases := []struct {
name, modulePath, path string
want string
}{
{
name: "github repo",
modulePath: "github.com/owner/repo",
want: expectedCodeWikiURL(server.URL, "github.com/owner/repo"),
},
{
name: "github repo subpackage",
modulePath: "github.com/owner/repo",
want: expectedCodeWikiURL(server.URL, "github.com/owner/repo"),
},
{
name: "github repo not found",
modulePath: "github.com/owner/repo-not-found",
want: "",
},
{
name: "non-github repo",
modulePath: "example.com/owner/repo",
want: "",
},
{
name: "golang.org/x/ repo",
modulePath: "golang.org/x/glog",
want: expectedCodeWikiURL(server.URL, "github.com/golang/glog"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
um := &internal.UnitMeta{ModuleInfo: internal.ModuleInfo{ModulePath: tc.modulePath}}
url := codeWikiURLGenerator(t.Context(), server.Client(), um, false)()
if url != tc.want {
t.Errorf("codeWikiURLGenerator(ctx, client, %q) = %q, want %q", tc.path, url, tc.want)
}
})
}
}
// recordingTransport is an http.RoundTripper that records every outgoing
// request URL and fails the test if invoked.
type recordingTransport struct {
t *testing.T
requests []string
}
func (rt *recordingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
rt.requests = append(rt.requests, r.URL.String())
rt.t.Errorf("unexpected external request: %s", r.URL)
return nil, fmt.Errorf("unexpected request: %s", r.URL)
}
func TestExternalLinkGeneratorsSkipsPrivateModules(t *testing.T) {
for _, tc := range []struct {
name string
goprivate string
gonoproxy string
}{
{name: "GOPRIVATE match", goprivate: "github.com/owner/*"},
{name: "GONOPROXY match", gonoproxy: "github.com/owner/*"},
} {
t.Run(tc.name, func(t *testing.T) {
old := goPrivatePatterns
goPrivatePatterns = func() goPrivateConfig {
return goPrivateConfig{goprivate: tc.goprivate, gonoproxy: tc.gonoproxy}
}
t.Cleanup(func() { goPrivatePatterns = old })
rt := &recordingTransport{t: t}
client := &http.Client{Transport: rt}
um := &internal.UnitMeta{ModuleInfo: internal.ModuleInfo{ModulePath: "github.com/owner/private-repo"}}
depsDev, codeWiki := externalLinkGenerators(t.Context(), client, um, false, false)
if got := depsDev(); got != "" {
t.Errorf("depsDev() = %q, want empty for private module", got)
}
if got := codeWiki(); got != "" {
t.Errorf("codeWiki() = %q, want empty for private module", got)
}
})
}
}
func TestExternalLinkGeneratorsCallsForPublicModules(t *testing.T) {
// Sanity check: when no privacy env var matches, the generators should
// invoke the HTTP client. We intercept and return 404 to keep the test hermetic.
mux := http.NewServeMux()
var depsHits, codeHits int
mux.HandleFunc("/_/s/go/", func(w http.ResponseWriter, r *http.Request) {
depsHits++
w.WriteHeader(http.StatusNotFound)
})
mux.HandleFunc("/_/exists/", func(w http.ResponseWriter, r *http.Request) {
codeHits++
w.WriteHeader(http.StatusNotFound)
})
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
oldCodeWikiExistsURL := codeWikiExistsURL
codeWikiExistsURL = server.URL + "/_/exists/"
t.Cleanup(func() { codeWikiExistsURL = oldCodeWikiExistsURL })
old := goPrivatePatterns
goPrivatePatterns = func() goPrivateConfig {
return goPrivateConfig{goprivate: "internal.example.com"}
}
t.Cleanup(func() { goPrivatePatterns = old })
// HTTP client whose RoundTripper rewrites deps.dev requests to the test server.
client := &http.Client{
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Host == "deps.dev" {
r.URL.Scheme = "http"
r.URL.Host = server.Listener.Addr().String()
}
return http.DefaultTransport.RoundTrip(r)
}),
}
um := &internal.UnitMeta{ModuleInfo: internal.ModuleInfo{ModulePath: "github.com/public/repo"}}
depsDev, codeWiki := externalLinkGenerators(t.Context(), client, um, false, false)
depsDev()
codeWiki()
if depsHits == 0 {
t.Error("expected deps.dev to be contacted for a public module")
}
if codeHits == 0 {
t.Error("expected codewiki.google to be contacted for a public module")
}
}
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }