internal/frontend: separate fetch and 404 logic into fetchserver
ServeFetch and ServePathNotFoundPage only work when a postgres
database is present. Separate them out into a different fetchserver
type which can be moved into a different package (in a followup cl).
This will allow their behavior, which is not used by cmd/pkgsite, to
be removed from that server. More importantly, their tests, which
depend on a real postgres database can be separated from the tests of
package internal/frontend.
For golang/go#61399
Change-Id: I73677fd06750fd48580071b8a895b322d9e3ac5d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/518817
kokoro-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index aec6179..d2a4fd1 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -113,19 +113,25 @@
log.Fatalf(ctx, "vuln.NewClient: %v", err)
}
staticSource := template.TrustedSourceFromFlag(flag.Lookup("static").Value)
- server, err := frontend.NewServer(frontend.ServerConfig{
- Config: cfg,
- DataSourceGetter: dsg,
+ // TODO: Can we use a separate queue for the fetchServer and for the Server?
+ // It would help differentiate ownership.
+ fetchServer := &frontend.FetchServer{
Queue: fetchQueue,
TaskIDChangeInterval: config.TaskIDChangeIntervalFrontend,
- TemplateFS: template.TrustedFSFromTrustedSource(staticSource),
- StaticFS: os.DirFS(*staticFlag),
- StaticPath: *staticFlag,
- ThirdPartyFS: os.DirFS(*thirdPartyPath),
- DevMode: *devMode,
- LocalMode: *localMode,
- Reporter: reporter,
- VulndbClient: vc,
+ }
+ server, err := frontend.NewServer(frontend.ServerConfig{
+ Config: cfg,
+ FetchServer: fetchServer,
+ DataSourceGetter: dsg,
+ Queue: fetchQueue,
+ TemplateFS: template.TrustedFSFromTrustedSource(staticSource),
+ StaticFS: os.DirFS(*staticFlag),
+ StaticPath: *staticFlag,
+ ThirdPartyFS: os.DirFS(*thirdPartyPath),
+ DevMode: *devMode,
+ LocalMode: *localMode,
+ Reporter: reporter,
+ VulndbClient: vc,
})
if err != nil {
log.Fatalf(ctx, "frontend.NewServer: %v", err)
diff --git a/internal/frontend/404.go b/internal/frontend/404.go
index 9824dd2..958701a 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -45,14 +45,10 @@
// servePathNotFoundPage serves a 404 page for the requested path, or redirects
// the user to an appropriate location.
-func (s *Server) servePathNotFoundPage(w http.ResponseWriter, r *http.Request,
- ds internal.DataSource, fullPath, modulePath, requestedVersion string) (err error) {
+func (s *FetchServer) ServePathNotFoundPage(w http.ResponseWriter, r *http.Request,
+ db internal.PostgresDB, fullPath, modulePath, requestedVersion string) (err error) {
defer derrors.Wrap(&err, "servePathNotFoundPage(w, r, %q, %q)", fullPath, requestedVersion)
- db, ok := ds.(internal.PostgresDB)
- if !ok {
- return datasourceNotSupportedErr()
- }
ctx := r.Context()
if stdlib.Contains(fullPath) {
@@ -142,13 +138,13 @@
// If a module has a status of 404, but s.taskIDChangeInterval has
// passed, allow the module to be refetched.
- if fr.status == http.StatusNotFound && time.Since(fr.updatedAt) > s.taskIDChangeInterval {
+ if fr.status == http.StatusNotFound && time.Since(fr.updatedAt) > s.TaskIDChangeInterval {
return pathNotFoundError(ctx, fullPath, requestedVersion)
}
// Redirect to the search result page for an empty directory that is above nested modules.
// See https://golang.org/issue/43725 for context.
- nm, err := ds.GetNestedModules(ctx, fullPath)
+ nm, err := db.GetNestedModules(ctx, fullPath)
if err == nil && len(nm) > 0 {
http.Redirect(w, r, "/search?q="+url.QueryEscape(fullPath), http.StatusFound)
return nil
@@ -317,3 +313,21 @@
err: err,
}
}
+
+// stdlibPathForShortcut returns a path in the stdlib that shortcut should redirect to,
+// or the empty string if there is no such path.
+func stdlibPathForShortcut(ctx context.Context, db internal.PostgresDB, shortcut string) (path string, err error) {
+ defer derrors.Wrap(&err, "stdlibPathForShortcut(ctx, %q)", shortcut)
+ if !stdlib.Contains(shortcut) {
+ return "", nil
+ }
+ matches, err := db.GetStdlibPathsWithSuffix(ctx, shortcut)
+ if err != nil {
+ return "", err
+ }
+ if len(matches) == 1 {
+ return matches[0], nil
+ }
+ // No matches, or ambiguous.
+ return "", nil
+}
diff --git a/internal/frontend/404_test.go b/internal/frontend/404_test.go
index e7bd27c..145ca2a 100644
--- a/internal/frontend/404_test.go
+++ b/internal/frontend/404_test.go
@@ -8,13 +8,20 @@
"context"
"errors"
"net/http"
+ "net/http/httptest"
+ "net/url"
"testing"
+ "github.com/alicebob/miniredis/v2"
+ "github.com/go-redis/redis/v8"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/pkgsite/internal"
+ "golang.org/x/pkgsite/internal/cookie"
"golang.org/x/pkgsite/internal/derrors"
+ "golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/postgres"
+ "golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/testing/sample"
"golang.org/x/pkgsite/internal/version"
)
@@ -247,3 +254,258 @@
})
}
}
+
+func TestStdlibPathForShortcut(t *testing.T) {
+ defer postgres.ResetTestDB(testDB, t)
+
+ m := sample.Module(stdlib.ModulePath, "v1.2.3",
+ "encoding/json", // one match for "json"
+ "text/template", "html/template", // two matches for "template"
+ )
+ ctx := context.Background()
+ postgres.MustInsertModule(ctx, t, testDB, m)
+
+ for _, test := range []struct {
+ path string
+ want string
+ }{
+ {"foo", ""},
+ {"json", "encoding/json"},
+ {"template", ""},
+ } {
+ got, err := stdlibPathForShortcut(ctx, testDB, test.path)
+ if err != nil {
+ t.Fatalf("%q: %v", test.path, err)
+ }
+ if got != test.want {
+ t.Errorf("%q: got %q, want %q", test.path, got, test.want)
+ }
+ }
+}
+
+// Verify that some paths that aren't found will redirect to valid pages.
+// Sometimes redirection sets the AlternativeModuleFlash cookie and puts
+// up a banner.
+func TestServer404Redirect(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+ defer cancel()
+
+ defer postgres.ResetTestDB(testDB, t)
+ sampleModule := sample.DefaultModule()
+ postgres.MustInsertModule(ctx, t, testDB, sampleModule)
+ alternativeModule := &internal.VersionMap{
+ ModulePath: "module.path/alternative",
+ GoModPath: sample.ModulePath,
+ RequestedVersion: version.Latest,
+ ResolvedVersion: sample.VersionString,
+ Status: derrors.ToStatus(derrors.AlternativeModule),
+ }
+ if err := testDB.UpsertVersionMap(ctx, alternativeModule); err != nil {
+ t.Fatal(err)
+ }
+
+ v1modpath := "notinv1.mod"
+ v1path := "notinv1.mod/foo"
+ postgres.MustInsertModule(ctx, t, testDB, sample.Module(v1modpath+"/v4", "v4.0.0", "foo"))
+ for _, mod := range []struct {
+ path, version string
+ status int
+ }{
+ {v1modpath, "v1.0.0", http.StatusNotFound},
+ {v1path, "v4.0.0", http.StatusNotFound},
+ {v1modpath + "/v4", "v4.0.0", http.StatusOK},
+ } {
+ if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
+ ModulePath: mod.path,
+ RequestedVersion: version.Latest,
+ ResolvedVersion: mod.version,
+ Status: mod.status,
+ GoModPath: mod.path,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
+ ModulePath: sample.ModulePath + "/blob/master",
+ RequestedVersion: version.Latest,
+ ResolvedVersion: sample.VersionString,
+ Status: http.StatusNotFound,
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := miniredis.Run()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rs.Close()
+
+ _, _, handler, _ := newTestServerWithFetch(t, nil, middleware.NewCacher(redis.NewClient(&redis.Options{Addr: rs.Addr()})))
+
+ for _, test := range []struct {
+ name, path, flash string
+ }{
+ {"github url", "/" + sample.ModulePath + "/blob/master", ""},
+ {"alternative module", "/" + alternativeModule.ModulePath, "module.path/alternative"},
+ {"module not in v1", "/" + v1modpath, "notinv1.mod"},
+ {"import path not in v1", "/" + v1path, "notinv1.mod/foo"},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
+ // Check for http.StatusFound, which indicates a redirect.
+ if w.Code != http.StatusFound {
+ t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, http.StatusFound)
+ }
+ res := w.Result()
+ c := findCookie(cookie.AlternativeModuleFlash, res.Cookies())
+ if c == nil && test.flash != "" {
+ t.Error("got no flash cookie, expected one")
+ } else if c != nil {
+ val, err := cookie.Base64Value(c)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if val != test.flash {
+ t.Fatalf("got cookie value %q, want %q", val, test.flash)
+ }
+ // If we have a cookie, then following the redirect URL with the cookie
+ // should serve a "redirected from" banner.
+ loc := res.Header.Get("Location")
+ r := httptest.NewRequest("GET", loc, nil)
+ r.AddCookie(c)
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, r)
+ err = checkBody(w.Result().Body, in(`[data-test-id="redirected-banner-text"]`, hasText(val)))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Visiting the same page again without the cookie should not
+ // display the banner.
+ r = httptest.NewRequest("GET", loc, nil)
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, r)
+ err = checkBody(w.Result().Body, notIn(`[data-test-id="redirected-banner-text"]`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ })
+ }
+}
+
+func TestServer404Redirect_NoLoop(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+ defer cancel()
+
+ altPath := "module.path/alternative"
+ goModPath := "module.path/alternative/pkg"
+ defer postgres.ResetTestDB(testDB, t)
+ sampleModule := sample.DefaultModule()
+ postgres.MustInsertModule(ctx, t, testDB, sampleModule)
+ alternativeModule := &internal.VersionMap{
+ ModulePath: altPath,
+ GoModPath: goModPath,
+ RequestedVersion: version.Latest,
+ ResolvedVersion: sample.VersionString,
+ Status: derrors.ToStatus(derrors.AlternativeModule),
+ }
+ alternativeModulePkg := &internal.VersionMap{
+ ModulePath: goModPath,
+ GoModPath: goModPath,
+ RequestedVersion: version.Latest,
+ ResolvedVersion: sample.VersionString,
+ Status: http.StatusNotFound,
+ }
+ if err := testDB.UpsertVersionMap(ctx, alternativeModule); err != nil {
+ t.Fatal(err)
+ }
+ if err := testDB.UpsertVersionMap(ctx, alternativeModulePkg); err != nil {
+ t.Fatal(err)
+ }
+
+ rs, err := miniredis.Run()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rs.Close()
+
+ _, _, handler, _ := newTestServerWithFetch(t, nil, middleware.NewCacher(redis.NewClient(&redis.Options{Addr: rs.Addr()})))
+
+ for _, test := range []struct {
+ name, path string
+ status int
+ }{
+ {"do not redirect if alternative module does not successfully return", "/" + altPath, http.StatusNotFound},
+ {"do not redirect go mod path endlessly", "/" + goModPath, http.StatusNotFound},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
+ // Check for http.StatusFound, which indicates a redirect.
+ if w.Code != test.status {
+ t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.status)
+ }
+ })
+ }
+}
+
+func TestEmptyDirectoryBetweenNestedModulesRedirect(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+ defer cancel()
+ defer postgres.ResetTestDB(testDB, t)
+
+ postgres.MustInsertModule(ctx, t, testDB, sample.Module(sample.ModulePath, sample.VersionString, ""))
+ postgres.MustInsertModule(ctx, t, testDB, sample.Module(sample.ModulePath+"/missing/dir/c", sample.VersionString, ""))
+
+ missingPath := sample.ModulePath + "/missing"
+ notInsertedPath := sample.ModulePath + "/missing/dir"
+ if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
+ ModulePath: missingPath,
+ RequestedVersion: version.Latest,
+ ResolvedVersion: sample.VersionString,
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ _, _, handler, _ := newTestServerWithFetch(t, nil, nil)
+ for _, test := range []struct {
+ name, path string
+ wantStatus int
+ wantLocation string
+ }{
+ {"want 404 for unknown version of module", sample.ModulePath + "@v0.5.0", http.StatusNotFound, ""},
+ {"want 404 for never fetched directory", notInsertedPath, http.StatusNotFound, ""},
+ {"want 302 for previously fetched directory", missingPath, http.StatusFound, "/search?q=" + url.PathEscape(missingPath)},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", "/"+test.path, nil))
+ if w.Code != test.wantStatus {
+ t.Errorf("%q: got status code = %d, want %d", "/"+test.path, w.Code, test.wantStatus)
+ }
+ if got := w.Header().Get("Location"); got != test.wantLocation {
+ t.Errorf("got location = %q, want %q", got, test.wantLocation)
+ }
+ })
+ }
+}
+
+func TestServerErrors(t *testing.T) {
+ _, _, handler, _ := newTestServerWithFetch(t, nil, nil)
+ for _, test := range []struct {
+ name, path string
+ wantCode int
+ }{
+ {"not found", "/invalid-page", http.StatusNotFound},
+ {"bad request", "/gocloud.dev/@latest/blob", http.StatusBadRequest},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
+ if w.Code != test.wantCode {
+ t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
+ }
+ })
+ }
+}
diff --git a/internal/frontend/fetch.go b/internal/frontend/fetch.go
index 29fdff0..dd725e6 100644
--- a/internal/frontend/fetch.go
+++ b/internal/frontend/fetch.go
@@ -86,13 +86,18 @@
statusNotFoundInVersionMap = 470
)
+type FetchServer struct {
+ Queue queue.Queue
+ TaskIDChangeInterval time.Duration
+}
+
// serveFetch checks if a requested path and version exists in the database.
// If not, it will enqueue potential module versions that could contain
// the requested path and version to a task queue, to be fetched by the worker.
// Meanwhile, the request will poll the database until a row is found, or a
// timeout occurs. A status and responseText will be returned based on the
// result of the request.
-func (s *Server) serveFetch(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
+func (s *FetchServer) ServeFetch(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
defer derrors.Wrap(&err, "serveFetch(%q)", r.URL.Path)
if _, ok := ds.(internal.PostgresDB); !ok {
// There's no reason for other DataSources to need this codepath.
@@ -127,7 +132,7 @@
resolvedVersion string
}
-func (s *Server) fetchAndPoll(ctx context.Context, ds internal.DataSource, modulePath, fullPath, requestedVersion string) (status int, responseText string) {
+func (s *FetchServer) fetchAndPoll(ctx context.Context, ds internal.DataSource, modulePath, fullPath, requestedVersion string) (status int, responseText string) {
start := time.Now()
defer func() {
log.Infof(ctx, "fetchAndPoll(ctx, ds, q, %q, %q, %q): status=%d, responseText=%q",
@@ -172,7 +177,7 @@
// checkPossibleModulePaths will then poll the database for each module path,
// until a result is returned or the request times out. If shouldQueue is false,
// it will return the fetchResult, regardless of what the status is.
-func (s *Server) checkPossibleModulePaths(ctx context.Context, db internal.PostgresDB,
+func (s *FetchServer) checkPossibleModulePaths(ctx context.Context, db internal.PostgresDB,
fullPath, requestedVersion string, modulePaths []string, shouldQueue bool) []*fetchResult {
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(ctx, fetchTimeout)
@@ -189,8 +194,8 @@
// Before enqueuing the module version to be fetched, check if we
// have already attempted to fetch it in the past. If so, just
// return the result from that fetch process.
- fr := checkForPath(ctx, db, fullPath, modulePath, requestedVersion, s.taskIDChangeInterval)
- log.Debugf(ctx, "initial checkForPath(ctx, db, %q, %q, %q, %d): status=%d, err=%v", fullPath, modulePath, requestedVersion, s.taskIDChangeInterval, fr.status, fr.err)
+ fr := checkForPath(ctx, db, fullPath, modulePath, requestedVersion, s.TaskIDChangeInterval)
+ log.Debugf(ctx, "initial checkForPath(ctx, db, %q, %q, %q, %d): status=%d, err=%v", fullPath, modulePath, requestedVersion, s.TaskIDChangeInterval, fr.status, fr.err)
if !shouldQueue || fr.status != statusNotFoundInVersionMap {
results[i] = fr
return
@@ -199,7 +204,7 @@
// A row for this modulePath and requestedVersion combination does not
// exist in version_map. Enqueue the module version to be fetched.
opts := &queue.Options{Source: queue.SourceFrontendValue}
- if _, err := s.queue.ScheduleFetch(ctx, modulePath, requestedVersion, opts); err != nil {
+ if _, err := s.Queue.ScheduleFetch(ctx, modulePath, requestedVersion, opts); err != nil {
fr.err = err
fr.status = http.StatusInternalServerError
log.Errorf(ctx, "enqueuing %s@%s to frontend-fetch task queue: %v", modulePath, requestedVersion, err)
@@ -210,7 +215,7 @@
// After the fetch request is enqueued, poll the database until it has been
// inserted or the request times out.
- fr = pollForPath(ctx, db, pollEvery, fullPath, modulePath, requestedVersion, s.taskIDChangeInterval)
+ fr = pollForPath(ctx, db, pollEvery, fullPath, modulePath, requestedVersion, s.TaskIDChangeInterval)
logf := log.Infof
if fr.status == http.StatusInternalServerError {
logf = log.Errorf
diff --git a/internal/frontend/fetch_test.go b/internal/frontend/fetch_test.go
index 4463ece..7a90a38 100644
--- a/internal/frontend/fetch_test.go
+++ b/internal/frontend/fetch_test.go
@@ -80,13 +80,13 @@
},
} {
t.Run(test.name, func(t *testing.T) {
- s, _, teardown := newTestServer(t, testModulesForProxy, nil)
+ s, f, _, teardown := newTestServerWithFetch(t, testModulesForProxy, nil)
defer teardown()
ctx, cancel := context.WithTimeout(context.Background(), testFetchTimeout)
defer cancel()
- status, responseText := s.fetchAndPoll(ctx, s.getDataSource(ctx), testModulePath, test.fullPath, test.version)
+ status, responseText := f.fetchAndPoll(ctx, s.getDataSource(ctx), testModulePath, test.fullPath, test.version)
if status != http.StatusOK {
t.Fatalf("fetchAndPoll(%q, %q, %q) = %d, %s; want status = %d",
testModulePath, test.fullPath, test.version, status, responseText, http.StatusOK)
@@ -144,9 +144,9 @@
ctx, cancel := context.WithTimeout(context.Background(), test.fetchTimeout)
defer cancel()
- s, _, teardown := newTestServer(t, testModulesForProxy, nil)
+ s, f, _, teardown := newTestServerWithFetch(t, testModulesForProxy, nil)
defer teardown()
- got, err := s.fetchAndPoll(ctx, s.getDataSource(ctx), test.modulePath, test.fullPath, test.version)
+ got, err := f.fetchAndPoll(ctx, s.getDataSource(ctx), test.modulePath, test.fullPath, test.version)
if got != test.want {
t.Fatalf("fetchAndPoll(ctx, testDB, q, %q, %q, %q): %d; want = %d",
@@ -181,9 +181,9 @@
t.Fatal(err)
}
- s, _, teardown := newTestServer(t, testModulesForProxy, nil)
+ s, f, _, teardown := newTestServerWithFetch(t, testModulesForProxy, nil)
defer teardown()
- got, _ := s.fetchAndPoll(ctx, s.getDataSource(ctx), sample.ModulePath, sample.PackagePath, sample.VersionString)
+ got, _ := f.fetchAndPoll(ctx, s.getDataSource(ctx), sample.ModulePath, sample.PackagePath, sample.VersionString)
if got != test.want {
t.Fatalf("fetchAndPoll for status %d: %d; want = %d)", test.status, got, test.want)
}
diff --git a/internal/frontend/frontend_test.go b/internal/frontend/frontend_test.go
index 7a7066a..0c2c411 100644
--- a/internal/frontend/frontend_test.go
+++ b/internal/frontend/frontend_test.go
@@ -46,20 +46,10 @@
func newTestServer(t *testing.T, proxyModules []*proxytest.Module, cacher Cacher) (*Server, http.Handler, func()) {
t.Helper()
- proxyClient, teardown := proxytest.SetupTestClient(t, proxyModules)
- sourceClient := source.NewClient(sourceTimeout)
- ctx := context.Background()
-
- q := queue.NewInMemory(ctx, 1, nil,
- func(ctx context.Context, mpath, version string) (int, error) {
- return FetchAndUpdateState(ctx, mpath, version, proxyClient, sourceClient, testDB)
- })
s, err := NewServer(ServerConfig{
- DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
- Queue: q,
- TaskIDChangeInterval: 10 * time.Minute,
- TemplateFS: template.TrustedFSFromEmbed(static.FS),
+ DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
+ TemplateFS: template.TrustedFSFromEmbed(static.FS),
// Use the embedded FSs here to make sure they're tested.
// Integration tests will use the actual directories.
StaticFS: static.FS,
@@ -73,6 +63,44 @@
s.Install(mux.Handle, cacher, nil)
return s, mux, func() {
+ postgres.ResetTestDB(testDB, t)
+ }
+}
+
+func newTestServerWithFetch(t *testing.T, proxyModules []*proxytest.Module, cacher Cacher) (*Server, *FetchServer, http.Handler, func()) {
+ t.Helper()
+ proxyClient, teardown := proxytest.SetupTestClient(t, proxyModules)
+ sourceClient := source.NewClient(sourceTimeout)
+ ctx := context.Background()
+
+ q := queue.NewInMemory(ctx, 1, nil,
+ func(ctx context.Context, mpath, version string) (int, error) {
+ return FetchAndUpdateState(ctx, mpath, version, proxyClient, sourceClient, testDB)
+ })
+
+ f := &FetchServer{
+ Queue: q,
+ TaskIDChangeInterval: 10 * time.Minute,
+ }
+
+ s, err := NewServer(ServerConfig{
+ FetchServer: f,
+ DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
+ Queue: q,
+ TemplateFS: template.TrustedFSFromEmbed(static.FS),
+ // Use the embedded FSs here to make sure they're tested.
+ // Integration tests will use the actual directories.
+ StaticFS: static.FS,
+ ThirdPartyFS: thirdparty.FS,
+ StaticPath: "../../static",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ mux := http.NewServeMux()
+ s.Install(mux.Handle, cacher, nil)
+
+ return s, f, mux, func() {
teardown()
postgres.ResetTestDB(testDB, t)
}
diff --git a/internal/frontend/package_test.go b/internal/frontend/package_test.go
deleted file mode 100644
index 1b26cfd..0000000
--- a/internal/frontend/package_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2019 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 (
- "context"
- "testing"
-
- "golang.org/x/pkgsite/internal/postgres"
- "golang.org/x/pkgsite/internal/stdlib"
- "golang.org/x/pkgsite/internal/testing/sample"
-)
-
-func TestStdlibPathForShortcut(t *testing.T) {
- defer postgres.ResetTestDB(testDB, t)
-
- m := sample.Module(stdlib.ModulePath, "v1.2.3",
- "encoding/json", // one match for "json"
- "text/template", "html/template", // two matches for "template"
- )
- ctx := context.Background()
- postgres.MustInsertModule(ctx, t, testDB, m)
-
- for _, test := range []struct {
- path string
- want string
- }{
- {"foo", ""},
- {"json", "encoding/json"},
- {"template", ""},
- } {
- got, err := stdlibPathForShortcut(ctx, testDB, test.path)
- if err != nil {
- t.Fatalf("%q: %v", test.path, err)
- }
- if got != test.want {
- t.Errorf("%q: got %q, want %q", test.path, got, test.want)
- }
- }
-}
diff --git a/internal/frontend/redirect.go b/internal/frontend/redirect.go
index 8ce7235..4d33342 100644
--- a/internal/frontend/redirect.go
+++ b/internal/frontend/redirect.go
@@ -5,13 +5,8 @@
package frontend
import (
- "context"
"net/http"
"strings"
-
- "golang.org/x/pkgsite/internal"
- "golang.org/x/pkgsite/internal/derrors"
- "golang.org/x/pkgsite/internal/stdlib"
)
// handlePackageDetailsRedirect redirects all redirects to "/pkg" to "/".
@@ -25,21 +20,3 @@
urlPath := strings.TrimPrefix(r.URL.Path, "/mod")
http.Redirect(w, r, urlPath, http.StatusMovedPermanently)
}
-
-// stdlibPathForShortcut returns a path in the stdlib that shortcut should redirect to,
-// or the empty string if there is no such path.
-func stdlibPathForShortcut(ctx context.Context, db internal.PostgresDB, shortcut string) (path string, err error) {
- defer derrors.Wrap(&err, "stdlibPathForShortcut(ctx, %q)", shortcut)
- if !stdlib.Contains(shortcut) {
- return "", nil
- }
- matches, err := db.GetStdlibPathsWithSuffix(ctx, shortcut)
- if err != nil {
- return "", err
- }
- if len(matches) == 1 {
- return matches[0], nil
- }
- // No matches, or ambiguous.
- return "", nil
-}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 5556cf2..264669b 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -44,48 +44,60 @@
// Server can be installed to serve the go discovery frontend.
type Server struct {
+ fetchServer FetchServerInterface
// getDataSource should never be called from a handler. It is called only in Server.errorHandler.
- getDataSource func(context.Context) internal.DataSource
- queue queue.Queue
- taskIDChangeInterval time.Duration
- templateFS template.TrustedFS
- staticFS fs.FS
- thirdPartyFS fs.FS
- devMode bool
- localMode bool // running locally (i.e. ./cmd/pkgsite)
- localModules []LocalModule // locally hosted modules; empty in production
- staticPath string // used only for dynamic loading in dev mode
- errorPage []byte
- appVersionLabel string
- googleTagManagerID string
- serveStats bool
- reporter derrors.Reporter
- fileMux *http.ServeMux
- vulnClient *vuln.Client
- versionID string
- instanceID string
+ getDataSource func(context.Context) internal.DataSource
+ queue queue.Queue
+ templateFS template.TrustedFS
+ staticFS fs.FS
+ thirdPartyFS fs.FS
+ devMode bool
+ localMode bool // running locally (i.e. ./cmd/pkgsite)
+ localModules []LocalModule // locally hosted modules; empty in production
+ staticPath string // used only for dynamic loading in dev mode
+ errorPage []byte
+ appVersionLabel string
+ googleTagManagerID string
+ serveStats bool
+ reporter derrors.Reporter
+ fileMux *http.ServeMux
+ vulnClient *vuln.Client
+ versionID string
+ instanceID string
mu sync.Mutex // Protects all fields below
templates map[string]*template.Template
}
+// FetchServerInterface is an interface for the parts of the server
+// that support adding packages to a queue for fetching by a worker
+// server.
+// TODO(matloob): rename to FetchServer once the FetchServer type moves
+// to its own package
+type FetchServerInterface interface {
+ ServeFetch(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error)
+ ServePathNotFoundPage(w http.ResponseWriter, r *http.Request,
+ ds internal.PostgresDB, fullPath, modulePath, requestedVersion string) (err error)
+}
+
// ServerConfig contains everything needed by a Server.
type ServerConfig struct {
Config *config.Config
+ // Note that FetchServer may be nil.
+ FetchServer FetchServerInterface
// DataSourceGetter should return a DataSource on each call.
// It should be goroutine-safe.
- DataSourceGetter func(context.Context) internal.DataSource
- Queue queue.Queue
- TaskIDChangeInterval time.Duration
- TemplateFS template.TrustedFS // for loading templates safely
- StaticFS fs.FS // for static/ directory
- ThirdPartyFS fs.FS // for third_party/ directory
- DevMode bool
- LocalMode bool
- LocalModules []LocalModule
- StaticPath string // used only for dynamic loading in dev mode
- Reporter derrors.Reporter
- VulndbClient *vuln.Client
+ DataSourceGetter func(context.Context) internal.DataSource
+ Queue queue.Queue
+ TemplateFS template.TrustedFS // for loading templates safely
+ StaticFS fs.FS // for static/ directory
+ ThirdPartyFS fs.FS // for third_party/ directory
+ DevMode bool
+ LocalMode bool
+ LocalModules []LocalModule
+ StaticPath string // used only for dynamic loading in dev mode
+ Reporter derrors.Reporter
+ VulndbClient *vuln.Client
}
// NewServer creates a new Server for the given database and template directory.
@@ -97,20 +109,20 @@
}
dochtml.LoadTemplates(scfg.TemplateFS)
s := &Server{
- getDataSource: scfg.DataSourceGetter,
- queue: scfg.Queue,
- templateFS: scfg.TemplateFS,
- staticFS: scfg.StaticFS,
- thirdPartyFS: scfg.ThirdPartyFS,
- devMode: scfg.DevMode,
- localMode: scfg.LocalMode,
- localModules: scfg.LocalModules,
- staticPath: scfg.StaticPath,
- templates: ts,
- taskIDChangeInterval: scfg.TaskIDChangeInterval,
- reporter: scfg.Reporter,
- fileMux: http.NewServeMux(),
- vulnClient: scfg.VulndbClient,
+ fetchServer: scfg.FetchServer,
+ getDataSource: scfg.DataSourceGetter,
+ queue: scfg.Queue,
+ templateFS: scfg.TemplateFS,
+ staticFS: scfg.StaticFS,
+ thirdPartyFS: scfg.ThirdPartyFS,
+ devMode: scfg.DevMode,
+ localMode: scfg.LocalMode,
+ localModules: scfg.LocalModules,
+ staticPath: scfg.StaticPath,
+ templates: ts,
+ reporter: scfg.Reporter,
+ fileMux: http.NewServeMux(),
+ vulnClient: scfg.VulndbClient,
}
if scfg.Config != nil {
s.appVersionLabel = scfg.Config.AppVersionLabel()
@@ -145,10 +157,13 @@
func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authValues []string) {
var (
detailHandler http.Handler = s.errorHandler(s.serveDetails)
- fetchHandler http.Handler = s.errorHandler(s.serveFetch)
+ fetchHandler http.Handler
searchHandler http.Handler = s.errorHandler(s.serveSearch)
vulnHandler http.Handler = s.errorHandler(s.serveVuln)
)
+ if s.fetchServer != nil {
+ fetchHandler = s.errorHandler(s.fetchServer.ServeFetch)
+ }
if cacher != nil {
// The cache middleware uses the URL string as the key for content served
// by the handlers it wraps. Be careful not to wrap the handler it returns
@@ -178,7 +193,9 @@
handle("/sitemap/", http.StripPrefix("/sitemap/", http.FileServer(http.Dir("private/sitemap"))))
handle("/mod/", http.HandlerFunc(s.handleModuleDetailsRedirect))
handle("/pkg/", http.HandlerFunc(s.handlePackageDetailsRedirect))
- handle("/fetch/", fetchHandler)
+ if fetchHandler != nil {
+ handle("/fetch/", fetchHandler)
+ }
handle("/play/compile", http.HandlerFunc(s.proxyPlayground))
handle("/play/fmt", http.HandlerFunc(s.handleFmt))
handle("/play/share", http.HandlerFunc(s.proxyPlayground))
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index bf2918b..f566ab0 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -15,28 +15,21 @@
"io"
"net/http"
"net/http/httptest"
- "net/url"
"os"
"regexp"
"strings"
"testing"
"time"
- "github.com/alicebob/miniredis/v2"
- "github.com/go-redis/redis/v8"
"github.com/google/safehtml/template"
"github.com/jba/templatecheck"
"golang.org/x/net/html"
"golang.org/x/pkgsite/internal"
- "golang.org/x/pkgsite/internal/cookie"
- "golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/frontend/page"
- "golang.org/x/pkgsite/internal/middleware"
"golang.org/x/pkgsite/internal/postgres"
"golang.org/x/pkgsite/internal/testing/htmlcheck"
"golang.org/x/pkgsite/internal/testing/pagecheck"
"golang.org/x/pkgsite/internal/testing/sample"
- "golang.org/x/pkgsite/internal/version"
"golang.org/x/pkgsite/static"
)
@@ -1125,7 +1118,7 @@
if err := testDB.InsertExcludedPrefix(ctx, excludedModulePath, "testuser", "testreason"); err != nil {
t.Fatal(err)
}
- _, handler, _ := newTestServer(t, nil, nil)
+ _, _, handler, _ := newTestServerWithFetch(t, nil, nil)
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) { // remove initial '/' for name
@@ -1158,192 +1151,6 @@
}
}
-func TestServerErrors(t *testing.T) {
- _, handler, _ := newTestServer(t, nil, nil)
- for _, test := range []struct {
- name, path string
- wantCode int
- }{
- {"not found", "/invalid-page", http.StatusNotFound},
- {"bad request", "/gocloud.dev/@latest/blob", http.StatusBadRequest},
- } {
- t.Run(test.name, func(t *testing.T) {
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
- if w.Code != test.wantCode {
- t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
- }
- })
- }
-}
-
-func TestServer404Redirect_NoLoop(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
-
- altPath := "module.path/alternative"
- goModPath := "module.path/alternative/pkg"
- defer postgres.ResetTestDB(testDB, t)
- sampleModule := sample.DefaultModule()
- postgres.MustInsertModule(ctx, t, testDB, sampleModule)
- alternativeModule := &internal.VersionMap{
- ModulePath: altPath,
- GoModPath: goModPath,
- RequestedVersion: version.Latest,
- ResolvedVersion: sample.VersionString,
- Status: derrors.ToStatus(derrors.AlternativeModule),
- }
- alternativeModulePkg := &internal.VersionMap{
- ModulePath: goModPath,
- GoModPath: goModPath,
- RequestedVersion: version.Latest,
- ResolvedVersion: sample.VersionString,
- Status: http.StatusNotFound,
- }
- if err := testDB.UpsertVersionMap(ctx, alternativeModule); err != nil {
- t.Fatal(err)
- }
- if err := testDB.UpsertVersionMap(ctx, alternativeModulePkg); err != nil {
- t.Fatal(err)
- }
-
- rs, err := miniredis.Run()
- if err != nil {
- t.Fatal(err)
- }
- defer rs.Close()
-
- _, handler, _ := newTestServer(t, nil, middleware.NewCacher(redis.NewClient(&redis.Options{Addr: rs.Addr()})))
-
- for _, test := range []struct {
- name, path string
- status int
- }{
- {"do not redirect if alternative module does not successfully return", "/" + altPath, http.StatusNotFound},
- {"do not redirect go mod path endlessly", "/" + goModPath, http.StatusNotFound},
- } {
- t.Run(test.name, func(t *testing.T) {
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
- // Check for http.StatusFound, which indicates a redirect.
- if w.Code != test.status {
- t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.status)
- }
- })
- }
-}
-
-// Verify that some paths that aren't found will redirect to valid pages.
-// Sometimes redirection sets the AlternativeModuleFlash cookie and puts
-// up a banner.
-func TestServer404Redirect(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
-
- defer postgres.ResetTestDB(testDB, t)
- sampleModule := sample.DefaultModule()
- postgres.MustInsertModule(ctx, t, testDB, sampleModule)
- alternativeModule := &internal.VersionMap{
- ModulePath: "module.path/alternative",
- GoModPath: sample.ModulePath,
- RequestedVersion: version.Latest,
- ResolvedVersion: sample.VersionString,
- Status: derrors.ToStatus(derrors.AlternativeModule),
- }
- if err := testDB.UpsertVersionMap(ctx, alternativeModule); err != nil {
- t.Fatal(err)
- }
-
- v1modpath := "notinv1.mod"
- v1path := "notinv1.mod/foo"
- postgres.MustInsertModule(ctx, t, testDB, sample.Module(v1modpath+"/v4", "v4.0.0", "foo"))
- for _, mod := range []struct {
- path, version string
- status int
- }{
- {v1modpath, "v1.0.0", http.StatusNotFound},
- {v1path, "v4.0.0", http.StatusNotFound},
- {v1modpath + "/v4", "v4.0.0", http.StatusOK},
- } {
- if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
- ModulePath: mod.path,
- RequestedVersion: version.Latest,
- ResolvedVersion: mod.version,
- Status: mod.status,
- GoModPath: mod.path,
- }); err != nil {
- t.Fatal(err)
- }
- }
- if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
- ModulePath: sample.ModulePath + "/blob/master",
- RequestedVersion: version.Latest,
- ResolvedVersion: sample.VersionString,
- Status: http.StatusNotFound,
- }); err != nil {
- t.Fatal(err)
- }
-
- rs, err := miniredis.Run()
- if err != nil {
- t.Fatal(err)
- }
- defer rs.Close()
-
- _, handler, _ := newTestServer(t, nil, middleware.NewCacher(redis.NewClient(&redis.Options{Addr: rs.Addr()})))
-
- for _, test := range []struct {
- name, path, flash string
- }{
- {"github url", "/" + sample.ModulePath + "/blob/master", ""},
- {"alternative module", "/" + alternativeModule.ModulePath, "module.path/alternative"},
- {"module not in v1", "/" + v1modpath, "notinv1.mod"},
- {"import path not in v1", "/" + v1path, "notinv1.mod/foo"},
- } {
- t.Run(test.name, func(t *testing.T) {
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
- // Check for http.StatusFound, which indicates a redirect.
- if w.Code != http.StatusFound {
- t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, http.StatusFound)
- }
- res := w.Result()
- c := findCookie(cookie.AlternativeModuleFlash, res.Cookies())
- if c == nil && test.flash != "" {
- t.Error("got no flash cookie, expected one")
- } else if c != nil {
- val, err := cookie.Base64Value(c)
- if err != nil {
- t.Fatal(err)
- }
- if val != test.flash {
- t.Fatalf("got cookie value %q, want %q", val, test.flash)
- }
- // If we have a cookie, then following the redirect URL with the cookie
- // should serve a "redirected from" banner.
- loc := res.Header.Get("Location")
- r := httptest.NewRequest("GET", loc, nil)
- r.AddCookie(c)
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- err = checkBody(w.Result().Body, in(`[data-test-id="redirected-banner-text"]`, hasText(val)))
- if err != nil {
- t.Fatal(err)
- }
- // Visiting the same page again without the cookie should not
- // display the banner.
- r = httptest.NewRequest("GET", loc, nil)
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- err = checkBody(w.Result().Body, notIn(`[data-test-id="redirected-banner-text"]`))
- if err != nil {
- t.Fatal(err)
- }
- }
- })
- }
-}
-
func findCookie(name string, cookies []*http.Cookie) *http.Cookie {
for _, c := range cookies {
if c.Name == name {
@@ -1489,47 +1296,6 @@
}
}
-func TestEmptyDirectoryBetweenNestedModulesRedirect(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
- defer postgres.ResetTestDB(testDB, t)
-
- postgres.MustInsertModule(ctx, t, testDB, sample.Module(sample.ModulePath, sample.VersionString, ""))
- postgres.MustInsertModule(ctx, t, testDB, sample.Module(sample.ModulePath+"/missing/dir/c", sample.VersionString, ""))
-
- missingPath := sample.ModulePath + "/missing"
- notInsertedPath := sample.ModulePath + "/missing/dir"
- if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
- ModulePath: missingPath,
- RequestedVersion: version.Latest,
- ResolvedVersion: sample.VersionString,
- }); err != nil {
- t.Fatal(err)
- }
-
- _, handler, _ := newTestServer(t, nil, nil)
- for _, test := range []struct {
- name, path string
- wantStatus int
- wantLocation string
- }{
- {"want 404 for unknown version of module", sample.ModulePath + "@v0.5.0", http.StatusNotFound, ""},
- {"want 404 for never fetched directory", notInsertedPath, http.StatusNotFound, ""},
- {"want 302 for previously fetched directory", missingPath, http.StatusFound, "/search?q=" + url.PathEscape(missingPath)},
- } {
- t.Run(test.name, func(t *testing.T) {
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, httptest.NewRequest("GET", "/"+test.path, nil))
- if w.Code != test.wantStatus {
- t.Errorf("%q: got status code = %d, want %d", "/"+test.path, w.Code, test.wantStatus)
- }
- if got := w.Header().Get("Location"); got != test.wantLocation {
- t.Errorf("got location = %q, want %q", got, test.wantLocation)
- }
- })
- }
-}
-
func TestStripScheme(t *testing.T) {
for _, test := range []struct {
url, want string
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index c9b3882..c7e3eb5 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -127,7 +127,11 @@
if !errors.Is(err, derrors.NotFound) {
return err
}
- return s.servePathNotFoundPage(w, r, ds, info.FullPath, info.ModulePath, info.RequestedVersion)
+ db, ok := ds.(internal.PostgresDB)
+ if !ok || s.fetchServer == nil {
+ return datasourceNotSupportedErr()
+ }
+ return s.fetchServer.ServePathNotFoundPage(w, r, db, info.FullPath, info.ModulePath, info.RequestedVersion)
}
makeDepsDevURL := depsDevURLGenerator(ctx, um)
diff --git a/internal/frontend/urlinfo/urlinfo.go b/internal/frontend/urlinfo/urlinfo.go
index b089873..a1ec34b 100644
--- a/internal/frontend/urlinfo/urlinfo.go
+++ b/internal/frontend/urlinfo/urlinfo.go
@@ -35,15 +35,15 @@
type UserError struct {
UserMessage string
- Err error
+ err error
}
func (e *UserError) Error() string {
- return e.Err.Error()
+ return e.err.Error()
}
func (e *UserError) Unwrap() error {
- return e.Err
+ return e.err
}
// ExtractURLPathInfo extracts information from a request to pkg.go.dev.
@@ -106,7 +106,7 @@
// You cannot explicitly write "latest" for the version.
if endParts[0] == version.Latest {
return nil, &UserError{
- Err: fmt.Errorf("invalid version: %q", info.RequestedVersion),
+ err: fmt.Errorf("invalid version: %q", info.RequestedVersion),
UserMessage: fmt.Sprintf("%q is not a valid version", endParts[0]),
}
}
@@ -123,7 +123,7 @@
}
if !IsValidPath(info.FullPath) {
return nil, &UserError{
- Err: fmt.Errorf("IsValidPath(%q) is false", info.FullPath),
+ err: fmt.Errorf("IsValidPath(%q) is false", info.FullPath),
UserMessage: fmt.Sprintf("%q is not a valid import path", info.FullPath),
}
}
@@ -139,7 +139,7 @@
fullPath = strings.TrimSuffix(strings.TrimPrefix(fullPath, "/"), "/")
if !IsValidPath(fullPath) {
return nil, &UserError{
- Err: fmt.Errorf("IsValidPath(%q) is false", fullPath),
+ err: fmt.Errorf("IsValidPath(%q) is false", fullPath),
UserMessage: fmt.Sprintf("%q is not a valid import path", fullPath),
}
}
@@ -161,7 +161,7 @@
return info, nil
}
return nil, &UserError{
- Err: fmt.Errorf("invalid Go tag for url: %q", urlPath),
+ err: fmt.Errorf("invalid Go tag for url: %q", urlPath),
UserMessage: fmt.Sprintf("%q is not a valid tag for the standard library", tag),
}
}
diff --git a/internal/testing/integration/frontend_test.go b/internal/testing/integration/frontend_test.go
index ceaea33..4e75204 100644
--- a/internal/testing/integration/frontend_test.go
+++ b/internal/testing/integration/frontend_test.go
@@ -33,14 +33,18 @@
func setupFrontend(ctx context.Context, t *testing.T, q queue.Queue, rc *redis.Client) *httptest.Server {
t.Helper()
const staticDir = "../../../static"
- s, err := frontend.NewServer(frontend.ServerConfig{
- DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
- TaskIDChangeInterval: 10 * time.Minute,
- TemplateFS: template.TrustedFSFromTrustedSource(template.TrustedSourceFromConstant(staticDir)),
- StaticFS: os.DirFS(staticDir),
- ThirdPartyFS: os.DirFS("../../../third_party"),
+ fs := &frontend.FetchServer{
Queue: q,
- Config: &config.Config{ServeStats: true},
+ TaskIDChangeInterval: 10 * time.Minute,
+ }
+ s, err := frontend.NewServer(frontend.ServerConfig{
+ FetchServer: fs,
+ DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
+ TemplateFS: template.TrustedFSFromTrustedSource(template.TrustedSourceFromConstant(staticDir)),
+ StaticFS: os.DirFS(staticDir),
+ ThirdPartyFS: os.DirFS("../../../third_party"),
+ Queue: q,
+ Config: &config.Config{ServeStats: true},
})
if err != nil {
t.Fatal(err)