internal/frontend: handle deep-linking on 404

Instead of always returning a 404 if the fullPath does not exist, check
the longest candidate module path does not exist instead. This matches
the frontend fetch logic.

For example, if a request is made to
github.com/kubernetes/client-go@v0.20.1/informers/admissionregistration/v1,
we now check if github.com/kubernetes/client-go@v0.20.1 has ever been
fetched, instead of
github.com/kubernetes/client-go/informers/admissionregistration/v1@v0.20.1
(since we know the latter is not a module).

Change-Id: I5ef37e4634c11900cf2225def01169dd0d2d73af
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/284575
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/frontend/404.go b/internal/frontend/404.go
index e3f3718..d213b16 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -38,7 +38,7 @@
 // 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, requestedVersion string) (err error) {
+	ds internal.DataSource, fullPath, modulePath, requestedVersion string) (err error) {
 	defer derrors.Wrap(&err, "servePathNotFoundPage(w, r, %q, %q)", fullPath, requestedVersion)
 
 	db, ok := ds.(*postgres.DB)
@@ -62,7 +62,7 @@
 		return &serverError{status: http.StatusNotFound}
 	}
 
-	fr, err := previousFetchStatusAndResponse(ctx, db, fullPath, requestedVersion)
+	fr, err := previousFetchStatusAndResponse(ctx, db, fullPath, modulePath, requestedVersion)
 	if err != nil {
 		if err != nil {
 			log.Error(ctx, err)
@@ -73,7 +73,7 @@
 	case http.StatusFound, derrors.ToStatus(derrors.AlternativeModule):
 		u := constructUnitURL(fr.goModPath, fr.goModPath, internal.LatestVersion)
 		cookie.Set(w, cookie.AlternativeModuleFlash, fullPath, u)
-		http.Redirect(w, r, constructUnitURL(fr.goModPath, fr.goModPath, internal.LatestVersion), http.StatusFound)
+		http.Redirect(w, r, u, http.StatusFound)
 		return
 	case http.StatusInternalServerError:
 		return pathNotFoundError(fullPath, requestedVersion)
@@ -114,22 +114,29 @@
 
 // previousFetchStatusAndResponse returns the fetch result from a
 // previous fetch of the fullPath and requestedVersion.
-func previousFetchStatusAndResponse(ctx context.Context, db *postgres.DB, fullPath, requestedVersion string) (_ *fetchResult, err error) {
+func previousFetchStatusAndResponse(ctx context.Context, db *postgres.DB, fullPath, modulePath, requestedVersion string) (_ *fetchResult, err error) {
 	defer derrors.Wrap(&err, "previousFetchStatusAndResponse(w, r, %q, %q)", fullPath, requestedVersion)
 
-	// Check if a row exists in the version_map table for the requested path
-	// and version. If not, this path may have never been fetched.
-	// In that case, a derrors.NotFound will be returned.
-	vm, err := db.GetVersionMap(ctx, fullPath, requestedVersion)
+	// Get all candidate module paths for this path.
+	paths, err := modulePathsToFetch(ctx, db, fullPath, modulePath)
 	if err != nil {
 		return nil, err
 	}
-
-	// If the row has been fetched before, and the result was either a 490 or
-	// 491, return that result, since it is a final state.
-	if vm.Status >= 500 ||
-		vm.Status == derrors.ToStatus(derrors.AlternativeModule) ||
-		vm.Status == derrors.ToStatus(derrors.BadModule) {
+	// Check if a row exists in the version_map table for the longest candidate
+	// path and version.
+	//
+	// If we have not fetched the path before, a derrors.NotFound will be
+	// returned.
+	vm, err := db.GetVersionMap(ctx, paths[0], requestedVersion)
+	if err != nil {
+		return nil, err
+	}
+	// If the row has been fetched before, and the result was either a 490,
+	// 491, or 5xx, return that result, since it is a final state.
+	if vm != nil &&
+		(vm.Status >= 500 ||
+			vm.Status == derrors.ToStatus(derrors.AlternativeModule) ||
+			vm.Status == derrors.ToStatus(derrors.BadModule)) {
 		return resultFromFetchRequest([]*fetchResult{
 			{
 				modulePath: vm.ModulePath,
@@ -164,21 +171,10 @@
 			status:     http.StatusFound,
 		}, nil
 	}
-
-	// The full path does not exist in our database, but its module might.
-	// This could be be because the path is in an alternative module or a bad
-	// module, or it was fetched previously and 404ed.
-	paths, err := candidateModulePaths(fullPath)
-	if err != nil {
-		return nil, err
-	}
 	vms, err := db.GetVersionMapsNon2xxStatus(ctx, paths, requestedVersion)
 	if err != nil {
 		return nil, err
 	}
-	if len(vms) == 0 {
-		return nil, nil
-	}
 	var fetchResults []*fetchResult
 	for _, vm := range vms {
 		fetchResults = append(fetchResults, fetchResultFromVersionMap(vm))
diff --git a/internal/frontend/404_test.go b/internal/frontend/404_test.go
index b95f2db..e181e15 100644
--- a/internal/frontend/404_test.go
+++ b/internal/frontend/404_test.go
@@ -20,6 +20,7 @@
 
 func TestPreviousFetchStatusAndResponse(t *testing.T) {
 	ctx := context.Background()
+	defer postgres.ResetTestDB(testDB, t)
 
 	for _, mod := range []struct {
 		path      string
@@ -34,6 +35,7 @@
 		{"github.com/alternative/ok", "github.com/vanity", 491},
 		{"github.com/alternative/ok/path", "", 404},
 		{"github.com/alternative/bad", "vanity", 491},
+		{"github.com/kubernetes/client-go", "k8s.io/client-go", 491},
 		{"bad.mod/foo/bar", "", 490},
 		{"bad.mod/foo", "", 404},
 		{"bad.mod", "", 490},
@@ -71,7 +73,7 @@
 		{"mod to reprocess", "reprocess.mod/foo", 404},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			fr, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.LatestVersion)
+			fr, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.UnknownModulePath, internal.LatestVersion)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -84,11 +86,11 @@
 	for _, test := range []struct {
 		name, path string
 	}{
-		{"path never fetched", "github.com/nonexistent"},
+		{"path never fetched", "github.com/non/existent"},
 		{"path never fetched, but top level mod fetched", "mvdan.cc/sh/v3"},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.LatestVersion)
+			_, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.UnknownModulePath, internal.LatestVersion)
 			if !errors.Is(err, derrors.NotFound) {
 				t.Errorf("got %v; want %v", err, derrors.NotFound)
 			}
@@ -96,8 +98,67 @@
 	}
 }
 
+func TestPreviousFetchStatusAndResponse_AlternativeModuleWithDeepLinking(t *testing.T) {
+	ctx := context.Background()
+	defer postgres.ResetTestDB(testDB, t)
+
+	for _, mod := range []struct {
+		path      string
+		goModPath string
+		status    int
+	}{
+		{"k8s.io/client-go", "k8s.io/client-go", 200},
+		{"github.com/kubernetes/client-go", "k8s.io/client-go", 491},
+	} {
+		goModPath := mod.goModPath
+		if goModPath == "" {
+			goModPath = mod.path
+		}
+		if err := testDB.UpsertVersionMap(ctx, &internal.VersionMap{
+			ModulePath:       mod.path,
+			RequestedVersion: internal.LatestVersion,
+			ResolvedVersion:  sample.VersionString,
+			Status:           mod.status,
+			GoModPath:        goModPath,
+		}); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	for _, test := range []struct {
+		name, path, mod string
+		status          int
+	}{
+		{"path with specified module", "github.com/kubernetes/client-go/informers/admissionregistration/v1", "github.com/kubernetes/client-go", 491},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			fr, err := previousFetchStatusAndResponse(ctx, testDB, test.path, test.mod, internal.LatestVersion)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if fr.status != test.status {
+				t.Errorf("got %v; want %v", fr.status, test.status)
+			}
+		})
+	}
+	for _, test := range []struct {
+		name, path, mod string
+	}{
+		{"path with unknown module", "github.com/kubernetes/client-go/informers/admissionregistration/v1", internal.UnknownModulePath},
+		{"module nonexistent module", "github.com/kubernetes/client-go/typo", "github.com/kubernetes/client-go/typo"},
+		{"path with specified nonexistent module", "github.com/kubernetes/client-go/typo/informers/admissionregistration/v1", "github.com/kubernetes/client-go/typo"},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if _, err := previousFetchStatusAndResponse(ctx, testDB, test.path, test.mod, internal.LatestVersion); !errors.Is(err, derrors.NotFound) {
+				t.Fatal(err)
+			}
+		})
+	}
+}
+
 func TestPreviousFetchStatusAndResponse_PathExistsAtNonV1(t *testing.T) {
 	ctx := context.Background()
+	defer postgres.ResetTestDB(testDB, t)
 
 	if err := testDB.InsertModule(ctx, sample.Module(sample.ModulePath+"/v4", "v4.0.0", "foo")); err != nil {
 		t.Fatal(err)
@@ -123,7 +184,7 @@
 	}
 
 	checkPath := func(ctx context.Context, t *testing.T, testDB *postgres.DB, path, version, wantPath string, wantStatus int) {
-		got, err := previousFetchStatusAndResponse(ctx, testDB, path, version)
+		got, err := previousFetchStatusAndResponse(ctx, testDB, path, internal.UnknownModulePath, version)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -156,7 +217,7 @@
 		{"import path v1 missing version", sample.ModulePath + "/foo", "v1.5.2"},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := previousFetchStatusAndResponse(ctx, testDB, test.path, test.version)
+			_, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.UnknownModulePath, test.version)
 			if !errors.Is(err, derrors.NotFound) {
 				t.Fatal(err)
 			}
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index e69fe38..6649397 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -93,7 +93,7 @@
 		if !errors.Is(err, derrors.NotFound) {
 			return err
 		}
-		return s.servePathNotFoundPage(w, r, ds, info.fullPath, info.requestedVersion)
+		return s.servePathNotFoundPage(w, r, ds, info.fullPath, info.modulePath, info.requestedVersion)
 	}
 
 	recordVersionTypeMetric(ctx, info.requestedVersion)