internal/testing/fakedatasource: implement PostgresDB interface

This change adds the rest of the methods from the PostgresDB interface
to the fake data source so it can be used to test some functions that
expect a postgres db. Most of the methods are left unimplemented. I
removed the IsPostgresDB that I originally put on the PostgresDB
interface since now we will have other types try to be PostgresDBs. In
an upcoming CL, I will extend the DataSource interface so that it's
the same as PostgresDB, and just have the frontend handle errors that
are errors.ErrUnsupported.

The CL then updates tests on some of the methods on the frontend that need
a PostgresDB to use the FakeDataSource instead.

This CL then removes the code that sets up and runs the
internal/frontend tests with a PostgresDB.

For golang/go#61399

Change-Id: I3bae5adc5ec03536b2074eee32b4e5558dfa7478
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/520878
Run-TryBot: Michael Matloob <matloob@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
kokoro-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/derrors/derrors.go b/internal/derrors/derrors.go
index 0eeb213..94f0b9a 100644
--- a/internal/derrors/derrors.go
+++ b/internal/derrors/derrors.go
@@ -16,6 +16,11 @@
 //lint:file-ignore ST1012 prefixing error values with Err would stutter
 
 var (
+	// Unsupported operation indicates that a requested operation cannot be performed, because it
+	// is unsupported. It is used here instead of errors.ErrUnsupported until we are able to depend
+	// on Go 1.21 in the pkgsite repo.
+	Unsupported = errors.New("unsupported operation")
+
 	// HasIncompletePackages indicates a module containing packages that
 	// were processed with a 60x error code.
 	HasIncompletePackages = errors.New("has incomplete packages")
diff --git a/internal/frontend/badge_test.go b/internal/frontend/badge_test.go
index 0f2d631..82513f2 100644
--- a/internal/frontend/badge_test.go
+++ b/internal/frontend/badge_test.go
@@ -11,7 +11,7 @@
 )
 
 func TestBadgeHandler_ServeSVG(t *testing.T) {
-	_, handler, _ := newTestServer(t, nil, nil)
+	_, handler := newTestServer(t, nil)
 	w := httptest.NewRecorder()
 	handler.ServeHTTP(w, httptest.NewRequest("GET", "/badge/net/http", nil))
 	if got, want := w.Result().Header.Get("Content-Type"), "image/svg+xml"; got != want {
@@ -20,7 +20,7 @@
 }
 
 func TestBadgeHandler_ServeBadgeTool(t *testing.T) {
-	_, handler, _ := newTestServer(t, nil, nil)
+	_, handler := newTestServer(t, nil)
 
 	tests := []struct {
 		url  string
diff --git a/internal/frontend/directory_test.go b/internal/frontend/directory_test.go
index 1f0c0f9..5f9a6e7 100644
--- a/internal/frontend/directory_test.go
+++ b/internal/frontend/directory_test.go
@@ -30,7 +30,7 @@
 		sample.Module("cloud.google.com/go/storage/v9/module", "v9.0.0", sample.Suffix),
 		sample.Module("cloud.google.com/go/v2", "v2.0.0", "storage", "spanner", "pubsub"),
 	} {
-		fds.MustInsertModule(m)
+		fds.MustInsertModule(ctx, m)
 	}
 
 	for _, test := range []struct {
diff --git a/internal/frontend/frontend_test.go b/internal/frontend/frontend_test.go
index 1bb7e25..7908ce3 100644
--- a/internal/frontend/frontend_test.go
+++ b/internal/frontend/frontend_test.go
@@ -18,20 +18,13 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/frontend/page"
 	"golang.org/x/pkgsite/internal/frontend/versions"
-	"golang.org/x/pkgsite/internal/postgres"
-	"golang.org/x/pkgsite/internal/proxy/proxytest"
+	"golang.org/x/pkgsite/internal/testing/fakedatasource"
 	"golang.org/x/pkgsite/static"
 	thirdparty "golang.org/x/pkgsite/third_party"
 )
 
 const testTimeout = 5 * time.Second
 
-var testDB *postgres.DB
-
-func TestMain(m *testing.M) {
-	postgres.RunDBTests("discovery_frontend_test", m, &testDB)
-}
-
 type testModule struct {
 	path            string
 	redistributable bool
@@ -47,11 +40,11 @@
 	docs           []*internal.Documentation
 }
 
-func newTestServer(t *testing.T, proxyModules []*proxytest.Module, cacher Cacher) (*Server, http.Handler, func()) {
+func newTestServer(t *testing.T, cacher Cacher) (*Server, http.Handler) {
 	t.Helper()
 
 	s, err := NewServer(ServerConfig{
-		DataSourceGetter: func(context.Context) internal.DataSource { return testDB },
+		DataSourceGetter: func(context.Context) internal.DataSource { return fakedatasource.New() },
 		TemplateFS:       template.TrustedFSFromEmbed(static.FS),
 		// Use the embedded FSs here to make sure they're tested.
 		// Integration tests will use the actual directories.
@@ -65,13 +58,11 @@
 	mux := http.NewServeMux()
 	s.Install(mux.Handle, cacher, nil)
 
-	return s, mux, func() {
-		postgres.ResetTestDB(testDB, t)
-	}
+	return s, mux
 }
 
 func TestHTMLInjection(t *testing.T) {
-	_, handler, _ := newTestServer(t, nil, nil)
+	_, handler := newTestServer(t, nil)
 	w := httptest.NewRecorder()
 	handler.ServeHTTP(w, httptest.NewRequest("GET", "/<em>UHOH</em>", nil))
 	if strings.Contains(w.Body.String(), "<em>") {
@@ -223,8 +214,7 @@
 }
 
 func TestInstallFS(t *testing.T) {
-	s, handler, teardown := newTestServer(t, nil, nil)
-	defer teardown()
+	s, handler := newTestServer(t, nil)
 	s.InstallFS("/dir", os.DirFS("."))
 	// Request this file.
 	w := httptest.NewRecorder()
diff --git a/internal/frontend/imports.go b/internal/frontend/imports.go
index 2f8f163..5935e58 100644
--- a/internal/frontend/imports.go
+++ b/internal/frontend/imports.go
@@ -113,7 +113,7 @@
 	if numImportedBy < importedByLimit && numImportedBySearch > numImportedBy {
 		// Unless we hit the limit, numImportedBySearch should never be greater
 		// than numImportedBy. If that happens, log an error so that we can
-		// debug, but continue with generating the page fo the user.
+		// debug, but continue with generating the page for the user.
 		log.Errorf(ctx, "pkg %q, module %q: search_documents.num_imported_by %d > numImportedBy %d from imports unique, which shouldn't happen",
 			pkgPath, modulePath, numImportedBySearch, numImportedBy)
 	}
diff --git a/internal/frontend/imports_test.go b/internal/frontend/imports_test.go
index 1aa5eea..3de1128 100644
--- a/internal/frontend/imports_test.go
+++ b/internal/frontend/imports_test.go
@@ -11,7 +11,6 @@
 
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/testing/fakedatasource"
 	"golang.org/x/pkgsite/internal/testing/sample"
 )
@@ -55,7 +54,7 @@
 			pkg := module.Units[1]
 			pkg.Imports = test.imports
 
-			fds.MustInsertModule(module)
+			fds.MustInsertModule(ctx, module)
 
 			got, err := fetchImportsDetails(ctx, fds, pkg.Path, pkg.ModulePath, pkg.Version)
 			if err != nil {
@@ -72,8 +71,7 @@
 }
 
 func TestFetchImportedByDetails(t *testing.T) {
-	defer postgres.ResetTestDB(testDB, t)
-
+	fds := fakedatasource.New()
 	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
 	defer cancel()
 
@@ -99,7 +97,7 @@
 	}
 
 	for _, m := range testModules {
-		postgres.MustInsertModule(ctx, t, testDB, m)
+		fds.MustInsertModule(ctx, m)
 	}
 
 	tests := []struct {
@@ -138,13 +136,13 @@
 			otherVersion := newModule(path.Dir(test.pkg.Path), test.pkg)
 			otherVersion.Version = "v1.0.5"
 			pkg := otherVersion.Units[1]
-			checkFetchImportedByDetails(ctx, t, pkg, test.wantDetails)
+			checkFetchImportedByDetails(ctx, fds, t, pkg, test.wantDetails)
 		})
 	}
 }
 
 func TestFetchImportedByDetails_ExceedsLimit(t *testing.T) {
-	defer postgres.ResetTestDB(testDB, t)
+	fds := fakedatasource.New()
 	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
 	defer cancel()
 
@@ -153,11 +151,11 @@
 	defer func() { importedByLimit = old }()
 
 	m := sample.Module("m.com/a", sample.VersionString, "foo")
-	postgres.MustInsertModule(ctx, t, testDB, m)
+	fds.MustInsertModule(ctx, m)
 	for _, mod := range []string{"m1.com/a", "m2.com/a", "m3.com/a"} {
 		m2 := sample.Module(mod, sample.VersionString, "p")
 		m2.Packages()[0].Imports = []string{"m.com/a/foo"}
-		postgres.MustInsertModule(ctx, t, testDB, m2)
+		fds.MustInsertModule(ctx, m2)
 	}
 	wantDetails := &ImportedByDetails{
 		ModulePath: "m.com/a",
@@ -169,11 +167,11 @@
 		NumImportedByDisplay: "0 (displaying more than 2 packages, including internal and invalid packages)",
 		Total:                3,
 	}
-	checkFetchImportedByDetails(ctx, t, m.Packages()[0], wantDetails)
+	checkFetchImportedByDetails(ctx, fds, t, m.Packages()[0], wantDetails)
 }
 
-func checkFetchImportedByDetails(ctx context.Context, t *testing.T, pkg *internal.Unit, wantDetails *ImportedByDetails) {
-	got, err := fetchImportedByDetails(ctx, testDB, pkg.Path, pkg.ModulePath)
+func checkFetchImportedByDetails(ctx context.Context, ds internal.DataSource, t *testing.T, pkg *internal.Unit, wantDetails *ImportedByDetails) {
+	got, err := fetchImportedByDetails(ctx, ds, pkg.Path, pkg.ModulePath)
 	if err != nil {
 		t.Fatalf("fetchImportedByDetails(ctx, db, %q) = %v err = %v, want %v",
 			pkg.Path, got, err, wantDetails)
diff --git a/internal/frontend/latest_version_test.go b/internal/frontend/latest_version_test.go
index f3c9301..be2f2f8 100644
--- a/internal/frontend/latest_version_test.go
+++ b/internal/frontend/latest_version_test.go
@@ -10,13 +10,13 @@
 	"testing"
 
 	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/source"
+	"golang.org/x/pkgsite/internal/testing/fakedatasource"
 	"golang.org/x/pkgsite/internal/testing/sample"
 )
 
 func TestLatestMinorVersion(t *testing.T) {
-	defer postgres.ResetTestDB(testDB, t)
+	fds := fakedatasource.New()
 	var persistedModules = []testModule{
 		{
 			path:            "github.com/mymodule/av1module",
@@ -58,8 +58,8 @@
 		},
 	}
 	ctx := context.Background()
-	insertTestModules(ctx, t, persistedModules)
-	svr := &Server{getDataSource: func(context.Context) internal.DataSource { return testDB }}
+	insertTestModules(ctx, t, fds, persistedModules)
+	svr := &Server{getDataSource: func(context.Context) internal.DataSource { return fds }}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
 			got := svr.GetLatestInfo(ctx, tc.fullPath, tc.modulePath, nil)
@@ -70,7 +70,7 @@
 	}
 }
 
-func insertTestModules(ctx context.Context, t *testing.T, mods []testModule) {
+func insertTestModules(ctx context.Context, t *testing.T, fds *fakedatasource.FakeDataSource, mods []testModule) {
 	for _, mod := range mods {
 		var (
 			suffixes []string
@@ -109,7 +109,7 @@
 					u.Readme = nil
 				}
 			}
-			postgres.MustInsertModule(ctx, t, testDB, m)
+			fds.MustInsertModule(ctx, m)
 		}
 	}
 }
diff --git a/internal/frontend/license_test.go b/internal/frontend/license_test.go
index 539b0cd..fb98f90 100644
--- a/internal/frontend/license_test.go
+++ b/internal/frontend/license_test.go
@@ -80,9 +80,9 @@
 
 	fds := fakedatasource.New()
 	ctx := context.Background()
-	fds.MustInsertModule(testModule)
-	fds.MustInsertModule(stdlibModule)
-	fds.MustInsertModule(crlfModule)
+	fds.MustInsertModule(ctx, testModule)
+	fds.MustInsertModule(ctx, stdlibModule)
+	fds.MustInsertModule(ctx, crlfModule)
 	for _, test := range []struct {
 		err                                 error
 		name, fullPath, modulePath, version string
diff --git a/internal/frontend/main_test.go b/internal/frontend/main_test.go
index 69fb596..04a294d 100644
--- a/internal/frontend/main_test.go
+++ b/internal/frontend/main_test.go
@@ -5,6 +5,7 @@
 package frontend
 
 import (
+	"context"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -29,8 +30,9 @@
 	mod1 := newModule(p1, nil, 2)
 	mod2 := newModule(p2, []string{p1}, 1)
 	mod3 := newModule(p3, []string{p1, p2}, 0)
+	ctx := context.Background()
 	for _, m := range []*internal.Module{mod1, mod2, mod3} {
-		fds.MustInsertModule(m)
+		fds.MustInsertModule(ctx, m)
 	}
 
 	for _, test := range []struct {
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index fa147b4..4746a78 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -25,7 +25,6 @@
 	"golang.org/x/pkgsite/internal/frontend/serrors"
 	"golang.org/x/pkgsite/internal/licenses"
 	"golang.org/x/pkgsite/internal/osv"
-	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/testing/fakedatasource"
 	"golang.org/x/pkgsite/internal/testing/sample"
 	"golang.org/x/pkgsite/internal/vuln"
@@ -38,10 +37,10 @@
 	std := sample.Module("std", sample.VersionString,
 		"cmd/go", "cmd/go/internal/auth", "fmt")
 	modules := []*internal.Module{golangTools, std}
-
+	ctx := context.Background()
 	fds := fakedatasource.New()
 	for _, v := range modules {
-		fds.MustInsertModule(v)
+		fds.MustInsertModule(ctx, v)
 	}
 	vc, err := vuln.NewInMemoryClient(testEntries)
 	if err != nil {
@@ -327,7 +326,7 @@
 	}
 
 	for _, m := range []*internal.Module{moduleFoo, moduleBar} {
-		fds.MustInsertModule(m)
+		fds.MustInsertModule(ctx, m)
 	}
 
 	for _, test := range []struct {
@@ -518,7 +517,6 @@
 	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
 
 	defer cancel()
-	defer postgres.ResetTestDB(testDB, t)
 
 	golangTools := sample.Module("golang.org/x/tools", sample.VersionString, "internal/lsp")
 	std := sample.Module("std", sample.VersionString,
@@ -527,7 +525,7 @@
 
 	fds := fakedatasource.New()
 	for _, v := range modules {
-		fds.MustInsertModule(v)
+		fds.MustInsertModule(ctx, v)
 	}
 	for _, test := range []struct {
 		name  string
@@ -788,12 +786,12 @@
 func TestSymbolSynopsis(t *testing.T) {
 	for _, test := range []struct {
 		name string
-		r    *postgres.SearchResult
+		r    *internal.SearchResult
 		want string
 	}{
 		{
 			"struct field",
-			&postgres.SearchResult{
+			&internal.SearchResult{
 				SymbolName:     "Foo.Bar",
 				SymbolSynopsis: "Bar string",
 				SymbolKind:     internal.SymbolKindField,
@@ -806,7 +804,7 @@
 		},
 		{
 			"interface method",
-			&postgres.SearchResult{
+			&internal.SearchResult{
 				SymbolName:     "Foo.Bar",
 				SymbolSynopsis: "Bar func() string",
 				SymbolKind:     internal.SymbolKindMethod,
diff --git a/internal/interfaces.go b/internal/interfaces.go
index b2331d5..3b48540 100644
--- a/internal/interfaces.go
+++ b/internal/interfaces.go
@@ -11,7 +11,6 @@
 // dependency on the database driver packages.
 type PostgresDB interface {
 	DataSource
-	IsPostgresDB()
 
 	IsExcluded(ctx context.Context, path string) (_ bool, err error)
 	GetImportedBy(ctx context.Context, pkgPath, modulePath string, limit int) (paths []string, err error)
diff --git a/internal/postgres/postgres.go b/internal/postgres/postgres.go
index 45de904..d56f08c 100644
--- a/internal/postgres/postgres.go
+++ b/internal/postgres/postgres.go
@@ -29,11 +29,6 @@
 	return newdb(db, false)
 }
 
-// Used to check that a DataSource is a PostgresDB without doing a
-// direct type assertion on *DB.
-func (*DB) IsPostgresDB() {
-}
-
 // NewBypassingLicenseCheck returns a new postgres DB that bypasses license
 // checks. That means all data will be inserted and returned for
 // non-redistributable modules, packages and directories.
diff --git a/internal/testing/fakedatasource/fakedatasource.go b/internal/testing/fakedatasource/fakedatasource.go
index a06e6c9..b1c6d0d 100644
--- a/internal/testing/fakedatasource/fakedatasource.go
+++ b/internal/testing/fakedatasource/fakedatasource.go
@@ -19,42 +19,28 @@
 	"golang.org/x/pkgsite/internal/version"
 )
 
+var errNotImplemented = fmt.Errorf("not implemented: %w", derrors.Unsupported)
+
 // FakeDataSource provides a fake implementation of the internal.DataSource interface.
 type FakeDataSource struct {
-	modules map[module.Version]*internal.Module
+	modules    map[module.Version]*internal.Module
+	importedBy map[string][]string
 }
 
 // New returns an initialized FakeDataSource.
 func New() *FakeDataSource {
-	return &FakeDataSource{modules: make(map[module.Version]*internal.Module)}
+	return &FakeDataSource{
+		modules:    make(map[module.Version]*internal.Module),
+		importedBy: make(map[string][]string),
+	}
 }
 
 // InsertModule adds the module to the FakeDataSource.
-func (ds *FakeDataSource) MustInsertModule(m *internal.Module) {
-	if m != nil {
-		for _, u := range m.Units {
-			ds.populateUnitSubdirectories(u, m)
-
-			// Make license info consistent.
-			if u.Licenses != nil {
-				// Sort licenses as postgres database does.
-				sort.Slice(u.Licenses, func(i, j int) bool {
-					return compareLicenses(u.Licenses[i], u.Licenses[j])
-				})
-				// Make sure LicenseContents match up with Licenses
-				u.LicenseContents = nil
-				for _, ul := range u.Licenses {
-					for _, ml := range m.Licenses {
-						if sameLicense(*ul, *ml.Metadata) {
-							u.LicenseContents = append(u.LicenseContents, ml)
-						}
-					}
-				}
-			}
-		}
+func (ds *FakeDataSource) MustInsertModule(ctx context.Context, m *internal.Module) {
+	_, err := ds.InsertModule(ctx, m, nil)
+	if err != nil {
+		panic(fmt.Errorf("error returned by InsertModule: %w", err))
 	}
-
-	ds.modules[module.Version{Path: m.ModulePath, Version: m.Version}] = m
 }
 
 // compareLicenses reports whether i < j according to our license sorting
@@ -257,7 +243,53 @@
 // GetLatestInfo gets information about the latest versions of a unit and module.
 // See LatestInfo for documentation.
 func (ds *FakeDataSource) GetLatestInfo(ctx context.Context, unitPath, modulePath string, latestUnitMeta *internal.UnitMeta) (latest internal.LatestInfo, err error) {
-	return internal.LatestInfo{}, nil
+	latestModule := ds.getLatestModule(modulePath)
+	if latestModule == nil {
+		return internal.LatestInfo{}, fmt.Errorf("could not find module %s: %w", modulePath, derrors.NotFound)
+	}
+	var unitFound bool
+	for _, unit := range latestModule.Units {
+		if unit.Path == unitPath {
+			unitFound = true
+		}
+	}
+
+	// Determine MajorModulePath and MajorUnitPath
+	if !strings.HasPrefix(unitPath, modulePath) {
+		panic(fmt.Errorf("module path %q is not a prefix of unit path %q", modulePath, unitPath))
+	}
+	rel := strings.TrimPrefix(unitPath, modulePath)
+	prefix, _, _ := module.SplitPathVersion(modulePath)
+	var latestMajorModule *internal.Module
+	for _, m := range ds.modules {
+		curPrefix, _, _ := module.SplitPathVersion(m.ModulePath)
+		if curPrefix != prefix {
+			continue
+		}
+		if latestMajorModule == nil || compareVersion(&m.ModuleInfo, &latestMajorModule.ModuleInfo) > 0 {
+			latestMajorModule = m
+		}
+	}
+	if latestMajorModule == nil {
+		panic(fmt.Errorf("a module exists with the module path %q at the same major version,"+
+			"but we couldn't find the latest version of the module", modulePath))
+	}
+	majorModulePath := latestMajorModule.ModulePath
+	majorUnitPath := majorModulePath // We don't set it to the unit path unless one is found
+	expectedMajorUnitPath := majorModulePath + rel
+	for _, unit := range latestMajorModule.Units {
+		if unit.Path == expectedMajorUnitPath {
+			majorUnitPath = unit.Path
+		}
+	}
+
+	return internal.LatestInfo{
+		MinorVersion:      latestModule.Version,
+		MinorModulePath:   latestModule.ModulePath,
+		UnitExistsAtMinor: unitFound,
+		MajorModulePath:   majorModulePath,
+		MajorUnitPath:     majorUnitPath,
+	}, nil
 }
 
 // SearchSupport reports the search types supported by this datasource.
@@ -305,3 +337,91 @@
 	}
 	return results, nil
 }
+
+func (ds *FakeDataSource) IsExcluded(ctx context.Context, path string) (_ bool, err error) {
+	return false, errNotImplemented
+}
+
+// GetImportedBy returns the set of packages importing the given pkgPath.
+func (ds *FakeDataSource) GetImportedBy(ctx context.Context, pkgPath, modulePath string, limit int) (paths []string, err error) {
+	importedBy := append([]string{}, ds.importedBy[pkgPath]...)
+	sort.Strings(importedBy)
+	if len(importedBy) > limit {
+		importedBy = importedBy[:limit]
+	}
+	return importedBy, nil
+}
+
+func (ds *FakeDataSource) GetImportedByCount(ctx context.Context, pkgPath, modulePath string) (int, error) {
+	return 0, nil
+}
+
+func (ds *FakeDataSource) GetLatestMajorPathForV1Path(ctx context.Context, v1path string) (string, int, error) {
+	return "", 0, errNotImplemented
+}
+
+func (ds *FakeDataSource) GetStdlibPathsWithSuffix(ctx context.Context, suffix string) ([]string, error) {
+	return nil, errNotImplemented
+}
+
+func (ds *FakeDataSource) GetSymbolHistory(ctx context.Context, packagePath, modulePath string) (*internal.SymbolHistory, error) {
+	return nil, errNotImplemented
+}
+
+func (ds *FakeDataSource) GetVersionMap(ctx context.Context, modulePath, requestedVersion string) (*internal.VersionMap, error) {
+	return nil, errNotImplemented
+}
+
+func (ds *FakeDataSource) GetVersionMaps(ctx context.Context, paths []string, requestedVersion string) ([]*internal.VersionMap, error) {
+	return nil, errNotImplemented
+}
+
+func (ds *FakeDataSource) GetVersionsForPath(ctx context.Context, path string) ([]*internal.ModuleInfo, error) {
+	return nil, errNotImplemented
+}
+
+// InsertModule inserts m into the FakeDataSource. It is only implemented for
+// lmv == nil.
+func (ds *FakeDataSource) InsertModule(ctx context.Context, m *internal.Module, lmv *internal.LatestModuleVersions) (isLatest bool, err error) {
+	if lmv != nil {
+		return false, errNotImplemented
+	}
+
+	if m != nil {
+		for _, u := range m.Units {
+			ds.populateUnitSubdirectories(u, m)
+
+			// Make license info consistent.
+			if u.Licenses != nil {
+				// Sort licenses as postgres database does.
+				sort.Slice(u.Licenses, func(i, j int) bool {
+					return compareLicenses(u.Licenses[i], u.Licenses[j])
+				})
+				// Make sure LicenseContents match up with Licenses
+				u.LicenseContents = nil
+				for _, ul := range u.Licenses {
+					for _, ml := range m.Licenses {
+						if sameLicense(*ul, *ml.Metadata) {
+							u.LicenseContents = append(u.LicenseContents, ml)
+						}
+					}
+				}
+			}
+
+			for _, pkg := range u.Imports {
+				ds.importedBy[pkg] = append(ds.importedBy[pkg], u.Path)
+			}
+		}
+	}
+
+	ds.modules[module.Version{Path: m.ModulePath, Version: m.Version}] = m
+	latest := ds.getLatestModule(m.ModulePath)
+	if latest == nil {
+		panic(fmt.Errorf("getLatestModule returned no modules for %v, even though we just inserted a module with that path", m.ModulePath))
+	}
+	return m == latest, nil
+}
+
+func (ds *FakeDataSource) UpsertVersionMap(ctx context.Context, vm *internal.VersionMap) error {
+	return errNotImplemented
+}
diff --git a/internal/testing/fakedatasource/fakedatasource_test.go b/internal/testing/fakedatasource/fakedatasource_test.go
new file mode 100644
index 0000000..d49c96f
--- /dev/null
+++ b/internal/testing/fakedatasource/fakedatasource_test.go
@@ -0,0 +1,89 @@
+// Copyright 2023 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 fakedatasource
+
+import (
+	"context"
+	"testing"
+
+	"golang.org/x/pkgsite/internal/testing/sample"
+)
+
+func TestGetLatestInfo_MajorPath(t *testing.T) {
+	type testModule struct {
+		path    string
+		version string
+		suffix  string
+	}
+	testCases := []struct {
+		modules             []testModule
+		modulePath          string
+		unitPath            string
+		wantMajorModulePath string
+		wantMajorUnitPath   string
+	}{
+		{
+			modules: []testModule{
+				{path: "example.com/mod", version: "v1.0.0", suffix: "a"},
+				{path: "example.com/mod/v2", version: "v2.0.0", suffix: "a/b"},
+			},
+			modulePath:          "example.com/mod",
+			unitPath:            "example.com/mod/a/b",
+			wantMajorModulePath: "example.com/mod/v2",
+			wantMajorUnitPath:   "example.com/mod/v2/a/b",
+		},
+		{
+			modules: []testModule{
+				{path: "example.com/mod", version: "v1.0.0", suffix: "a"},
+				{path: "example.com/mod/v2", version: "v2.0.0", suffix: "a"},
+			},
+			modulePath:          "example.com/mod",
+			unitPath:            "example.com/mod/a/b",
+			wantMajorModulePath: "example.com/mod/v2",
+			wantMajorUnitPath:   "example.com/mod/v2",
+		},
+		{
+			modules: []testModule{
+				{path: "example.com/mod", version: "v1.0.0", suffix: "a"},
+				{path: "example.com/mod/v2", version: "v2.0.0", suffix: "a"},
+				{path: "example.com/mod/v2", version: "v2.1.0", suffix: "a/b"},
+			},
+			modulePath:          "example.com/mod",
+			unitPath:            "example.com/mod/a/b",
+			wantMajorModulePath: "example.com/mod/v2",
+			wantMajorUnitPath:   "example.com/mod/v2/a/b",
+		},
+		{
+			modules: []testModule{
+				{path: "example.com/mod", version: "v1.0.0", suffix: "a"},
+				{path: "example.com/mod/v2", version: "v2.0.0", suffix: "a/b"},
+				{path: "example.com/mod/v3", version: "v3.0.0", suffix: "a"},
+			},
+			modulePath:          "example.com/mod",
+			unitPath:            "example.com/mod/a/b",
+			wantMajorModulePath: "example.com/mod/v3",
+			wantMajorUnitPath:   "example.com/mod/v3",
+		},
+	}
+
+	ctx := context.Background()
+	for _, tc := range testCases {
+		fds := New()
+		for _, m := range tc.modules {
+			fds.MustInsertModule(ctx, sample.Module(m.path, m.version, m.suffix))
+		}
+		latest, err := fds.GetLatestInfo(ctx, tc.unitPath, tc.modulePath, nil)
+		if err != nil {
+			t.Errorf("fds.GetLatestInfo(%q, %q): got error %v; expected none", tc.modulePath, tc.unitPath, err)
+			continue
+		}
+		if latest.MajorModulePath != tc.wantMajorModulePath {
+			t.Errorf("fds.GetLatestInfo(%q, %q).MajorModulePath: got %q, want %q", tc.modulePath, tc.unitPath, latest.MajorModulePath, tc.wantMajorModulePath)
+		}
+		if latest.MajorUnitPath != tc.wantMajorUnitPath {
+			t.Errorf("fds.GetLatestInfo(%q, %q).MajorUnitPath: got %q, want %q", tc.modulePath, tc.unitPath, latest.MajorUnitPath, tc.wantMajorUnitPath)
+		}
+	}
+}