interanl: add experiment not-at-v1

GetLatestMajorVersionForV1Path is taking a long time to execute
(up to 30s). This logic is added behind a feature flag for
investigation.

Change-Id: I6d0c1e9937a7d4690dfa6393f157b3f0be99a05b
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/283259
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/experiment.go b/internal/experiment.go
index 0c0d8c4..88a40a9 100644
--- a/internal/experiment.go
+++ b/internal/experiment.go
@@ -7,12 +7,14 @@
 
 const (
 	ExperimentNotAtLatest = "not-at-latest"
+	ExperimentNotAtV1     = "not-at-v1"
 )
 
 // Experiments represents all of the active experiments in the codebase and
 // a description of each experiment.
 var Experiments = map[string]string{
 	ExperimentNotAtLatest: "Enable the display of a 'not at latest' badge.",
+	ExperimentNotAtV1:     "Redirect requests to a path not at v1 to the highest major version of that path.",
 }
 
 // Experiment holds data associated with an experimental feature for frontend
diff --git a/internal/frontend/404.go b/internal/frontend/404.go
index 50a73ea..09ca6df 100644
--- a/internal/frontend/404.go
+++ b/internal/frontend/404.go
@@ -15,6 +15,7 @@
 	"github.com/google/safehtml/template/uncheckedconversions"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
+	"golang.org/x/pkgsite/internal/experiment"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/stdlib"
@@ -137,25 +138,27 @@
 		}, fullPath, requestedVersion)
 	}
 
-	// Check if the unit path exists at a higher major version.
-	// For example, my.module might not exist, but my.module/v3 might.
-	// Similarly, my.module/foo might not exist, but my.module/v3/foo might.
-	// In either case, the user will be redirected to the highest major version
-	// of the path.
-	//
-	// Do not bother to look for a specific version if this case. If
-	// my.module/foo@v2.1.0 was requested, and my.module/foo/v2 exists, just
-	// return the latest version of my.module/foo/v2.
-	majPath, err := db.GetLatestMajorPathForV1Path(ctx, fullPath)
-	if err != nil && err != derrors.NotFound {
-		return nil, err
-	}
-	if majPath != "" {
-		return &fetchResult{
-			modulePath: majPath,
-			goModPath:  majPath,
-			status:     http.StatusFound,
-		}, nil
+	if experiment.IsActive(ctx, internal.ExperimentNotAtV1) {
+		// Check if the unit path exists at a higher major version.
+		// For example, my.module might not exist, but my.module/v3 might.
+		// Similarly, my.module/foo might not exist, but my.module/v3/foo might.
+		// In either case, the user will be redirected to the highest major version
+		// of the path.
+		//
+		// Do not bother to look for a specific version if this case. If
+		// my.module/foo@v2.1.0 was requested, and my.module/foo/v2 exists, just
+		// return the latest version of my.module/foo/v2.
+		majPath, err := db.GetLatestMajorPathForV1Path(ctx, fullPath)
+		if err != nil && err != derrors.NotFound {
+			return nil, err
+		}
+		if majPath != "" {
+			return &fetchResult{
+				modulePath: majPath,
+				goModPath:  majPath,
+				status:     http.StatusFound,
+			}, nil
+		}
 	}
 
 	// The full path does not exist in our database, but its module might.
@@ -183,10 +186,14 @@
 }
 
 func fetchResultFromVersionMap(vm *internal.VersionMap) *fetchResult {
+	var err error
+	if vm.Error != "" {
+		err = errors.New(vm.Error)
+	}
 	return &fetchResult{
 		modulePath: vm.ModulePath,
 		goModPath:  vm.GoModPath,
 		status:     vm.Status,
-		err:        errors.New(vm.Error),
+		err:        err,
 	}
 }
diff --git a/internal/frontend/404_test.go b/internal/frontend/404_test.go
index 538a8f1..aa30240 100644
--- a/internal/frontend/404_test.go
+++ b/internal/frontend/404_test.go
@@ -8,11 +8,15 @@
 	"context"
 	"errors"
 	"net/http"
+	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
+	"golang.org/x/pkgsite/internal/experiment"
+	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/testing/sample"
 )
 
@@ -120,6 +124,23 @@
 		}
 	}
 
+	checkPath := func(ctx context.Context, t *testing.T, testDB *postgres.DB, path, wantPath string, status int) {
+		got, err := previousFetchStatusAndResponse(ctx, testDB, path, internal.LatestVersion)
+		if err != nil {
+			t.Fatal(err)
+		}
+		want := &fetchResult{
+			modulePath: wantPath,
+			goModPath:  wantPath,
+			status:     status,
+		}
+		if diff := cmp.Diff(want, got,
+			cmp.AllowUnexported(fetchResult{}),
+			cmpopts.IgnoreFields(fetchResult{}, "responseText")); diff != "" {
+			t.Errorf("mismatch (-want, +got):\n%s", diff)
+		}
+	}
+
 	for _, test := range []struct {
 		name, path, want string
 	}{
@@ -127,18 +148,12 @@
 		{"import path not at v1", sample.ModulePath + "/foo", sample.ModulePath + "/v4/foo"},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			got, err := previousFetchStatusAndResponse(ctx, testDB, test.path, internal.LatestVersion)
-			if err != nil {
-				t.Fatal(err)
-			}
-			want := &fetchResult{
-				modulePath: test.want,
-				goModPath:  test.want,
-				status:     http.StatusFound,
-			}
-			if diff := cmp.Diff(want, got, cmp.AllowUnexported(fetchResult{})); diff != "" {
-				t.Errorf("mismatch (-want, +got):\n%s", diff)
-			}
+			want := strings.ReplaceAll(test.want, "/v4", "")
+			checkPath(ctx, t, testDB, test.path, want, http.StatusNotFound)
+		})
+		t.Run(test.name+"not-at-v1", func(t *testing.T) {
+			ctx := experiment.NewContext(ctx, internal.ExperimentNotAtV1)
+			checkPath(ctx, t, testDB, test.path, test.want, http.StatusFound)
 		})
 	}
 }
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index e82b4d2..06b6df6 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -1226,7 +1226,8 @@
 		}
 	}
 
-	_, handler, _ := newTestServer(t, nil)
+	_, handler, _ := newTestServer(t, nil, internal.ExperimentNotAtV1)
+	_, handlerNoExp, _ := newTestServer(t, nil)
 
 	for _, test := range []struct {
 		name, path string
@@ -1244,6 +1245,14 @@
 			if w.Code != test.wantCode {
 				t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
 			}
+
+			handlerNoExp.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
+			if test.path == v1modpath || test.path == v1path {
+				test.wantCode = http.StatusNotFound
+			}
+			if w.Code != test.wantCode {
+				t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
+			}
 		})
 	}
 }