blob: 75f4a985f413e3b47ccd9110ad74e5a60c9ce35f [file] [log] [blame]
// 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 postgres
import (
"context"
"errors"
"io/ioutil"
"path/filepath"
"testing"
"time"
"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/licenses"
"golang.org/x/pkgsite/internal/source"
"golang.org/x/pkgsite/internal/testing/sample"
)
func TestInsertModule(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout*2)
defer cancel()
ctx = experiment.NewContext(ctx,
experiment.NewSet(map[string]bool{
internal.ExperimentInsertDirectories: true}))
for _, test := range []struct {
name string
module *internal.Module
}{
{
name: "valid test",
module: sample.Module(),
},
{
name: "valid test with internal package",
module: func() *internal.Module {
m := sample.Module()
p := sample.Package()
p.Path = sample.ModulePath + "/internal/foo"
m.Packages = []*internal.Package{p}
d1 := sample.DirectoryNewForModuleRoot(&m.ModuleInfo, p.Licenses)
d2 := sample.DirectoryNewEmpty(sample.ModulePath + "/internal")
d3 := sample.DirectoryNewForPackage(p)
m.Directories = []*internal.DirectoryNew{d1, d2, d3}
return m
}(),
},
{
name: "valid test with go.mod missing",
module: func() *internal.Module {
m := sample.Module()
m.HasGoMod = false
return m
}(),
},
{
name: "stdlib",
module: func() *internal.Module {
m := sample.Module()
m.ModulePath = "std"
m.Version = "v1.12.5"
p := &internal.Package{
Name: "context",
Path: "context",
Synopsis: "This is a package synopsis",
Licenses: sample.LicenseMetadata,
DocumentationHTML: "This is the documentation HTML",
}
m.Packages = []*internal.Package{p}
d1 := sample.DirectoryNewForModuleRoot(&m.ModuleInfo, p.Licenses)
d2 := sample.DirectoryNewForPackage(p)
m.Directories = []*internal.DirectoryNew{d1, d2}
return m
}(),
},
} {
t.Run(test.name, func(t *testing.T) {
defer ResetTestDB(testDB, t)
if err := testDB.InsertModule(ctx, test.module); err != nil {
t.Fatal(err)
}
// Test that insertion of duplicate primary key won't fail.
if err := testDB.InsertModule(ctx, test.module); err != nil {
t.Fatal(err)
}
got, err := testDB.GetModuleInfo(ctx, test.module.ModulePath, test.module.Version)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(test.module.ModuleInfo, *got, cmp.AllowUnexported(source.Info{})); diff != "" {
t.Fatalf("testDB.GetModuleInfo(%q, %q) mismatch (-want +got):\n%s", test.module.ModulePath, test.module.Version, diff)
}
for _, want := range test.module.Packages {
got, err := testDB.GetPackage(ctx, want.Path, test.module.ModulePath, test.module.Version)
if err != nil {
t.Fatal(err)
}
opts := cmp.Options{
// The packages table only includes partial
// license information; it omits the Coverage
// field.
cmpopts.IgnoreFields(internal.Package{}, "Imports"),
cmpopts.IgnoreFields(licenses.Metadata{}, "Coverage"),
cmpopts.EquateEmpty(),
}
if diff := cmp.Diff(*want, got.Package, opts...); diff != "" {
t.Fatalf("testDB.GetPackage(%q, %q) mismatch (-want +got):\n%s", want.Path, test.module.Version, diff)
}
}
for _, dir := range test.module.Directories {
got, err := testDB.getDirectoryNew(ctx, dir.Path, test.module.ModulePath, test.module.Version)
if err != nil {
t.Fatal(err)
}
want := internal.VersionedDirectory{
DirectoryNew: *dir,
ModuleInfo: test.module.ModuleInfo,
}
opts := cmp.Options{
cmpopts.IgnoreFields(internal.ModuleInfo{}, "ReadmeFilePath"),
cmpopts.IgnoreFields(internal.ModuleInfo{}, "ReadmeContents"),
cmpopts.IgnoreFields(licenses.Metadata{}, "Coverage"),
cmp.AllowUnexported(source.Info{}),
}
if diff := cmp.Diff(want, *got, opts); diff != "" {
t.Errorf("testDB.getDirectoryNew(%q, %q) mismatch (-want +got):\n%s", dir.Path, test.module.Version, diff)
}
}
})
}
}
func TestInsertModuleErrors(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout*2)
defer cancel()
testCases := []struct {
name string
module *internal.Module
// identifiers to use for fetch
wantModulePath, wantVersion, wantPkgPath string
// error conditions
wantWriteErr error
wantReadErr bool
}{
{
name: "nil version write error",
wantModulePath: sample.ModulePath,
wantVersion: sample.VersionString,
wantWriteErr: derrors.DBModuleInsertInvalid,
},
{
name: "nonexistent version",
module: sample.Module(),
wantModulePath: sample.ModulePath,
wantVersion: "v1.2.3",
},
{
name: "nonexistent module",
module: sample.Module(),
wantModulePath: "nonexistent_module_path",
wantVersion: "v1.0.0",
wantPkgPath: sample.PackagePath,
},
{
name: "missing module path",
module: func() *internal.Module {
v := sample.Module()
v.ModulePath = ""
return v
}(),
wantVersion: sample.VersionString,
wantModulePath: sample.ModulePath,
wantWriteErr: derrors.DBModuleInsertInvalid,
},
{
name: "missing version",
module: func() *internal.Module {
v := sample.Module()
v.Version = ""
return v
}(),
wantVersion: sample.VersionString,
wantModulePath: sample.ModulePath,
wantWriteErr: derrors.DBModuleInsertInvalid,
},
{
name: "empty commit time",
module: func() *internal.Module {
v := sample.Module()
v.CommitTime = time.Time{}
return v
}(),
wantVersion: sample.VersionString,
wantModulePath: sample.ModulePath,
wantWriteErr: derrors.DBModuleInsertInvalid,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer ResetTestDB(testDB, t)
if err := testDB.InsertModule(ctx, tc.module); !errors.Is(err, tc.wantWriteErr) {
t.Errorf("error: %v, want write error: %v", err, tc.wantWriteErr)
}
})
}
}
func TestPostgres_ReadAndWriteModuleOtherColumns(t *testing.T) {
// Verify that InsertModule correctly populates the columns in the versions
// table that are not in the ModuleInfo struct.
defer ResetTestDB(testDB, t)
ctx := context.Background()
type other struct {
sortVersion, seriesPath string
}
v := sample.Module()
v.ModulePath = "github.com/user/repo/path/v2"
v.Version = "v1.2.3-beta.4.a"
want := other{
sortVersion: "1,2,3,~beta,4,~a",
seriesPath: "github.com/user/repo/path",
}
if err := testDB.InsertModule(ctx, v); err != nil {
t.Fatal(err)
}
query := `
SELECT
sort_version, series_path
FROM
modules
WHERE
module_path = $1 AND version = $2`
row := testDB.db.QueryRow(ctx, query, v.ModulePath, v.Version)
var got other
if err := row.Scan(&got.sortVersion, &got.seriesPath); err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("\ngot %+v\nwant %+v", got, want)
}
}
func TestPostgres_DeleteVersion(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
defer ResetTestDB(testDB, t)
v := sample.Module()
if err := testDB.InsertModule(ctx, v); err != nil {
t.Fatal(err)
}
if _, err := testDB.GetModuleInfo(ctx, v.ModulePath, v.Version); err != nil {
t.Fatal(err)
}
if err := testDB.DeleteModule(ctx, v.ModulePath, v.Version); err != nil {
t.Fatal(err)
}
if _, err := testDB.GetModuleInfo(ctx, v.ModulePath, v.Version); !errors.Is(err, derrors.NotFound) {
t.Errorf("got %v, want NotFound", err)
}
}
func TestPostgres_NewerAlternative(t *testing.T) {
// Verify that packages are not added to search_documents if the module has a newer
// alternative version.
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
defer ResetTestDB(testDB, t)
const (
modulePath = "example.com/Mod"
packagePath = modulePath + "/p"
altVersion = "v1.2.0"
okVersion = "v1.0.0"
)
err := testDB.UpsertModuleVersionState(ctx, modulePath, altVersion, "appVersion", time.Now(),
derrors.ToHTTPStatus(derrors.AlternativeModule), "example.com/mod", derrors.AlternativeModule, nil)
if err != nil {
t.Fatal(err)
}
m := sample.Module()
m.ModulePath = modulePath
m.Version = okVersion
m.Packages[0].Name = "p"
m.Packages[0].Path = packagePath
if err := testDB.InsertModule(ctx, m); err != nil {
t.Fatal(err)
}
if _, _, found := GetFromSearchDocuments(ctx, t, testDB, packagePath); found {
t.Fatal("found package after inserting")
}
}
func TestMakeValidUnicode(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
defer ResetTestDB(testDB, t)
db := testDB.Underlying()
if _, err := db.Exec(ctx, `CREATE TABLE IF NOT EXISTS valid_unicode (contents text)`); err != nil {
t.Fatal(err)
}
defer db.Exec(ctx, `DROP TABLE valid_unicode`)
insert := func(s string) error {
_, err := db.Exec(ctx, `INSERT INTO valid_unicode VALUES($1)`, s)
return err
}
check := func(filename string, okRaw bool) {
data, err := ioutil.ReadFile(filepath.Join("testdata", filename))
if err != nil {
t.Fatal(err)
}
raw := string(data)
err = insert(raw)
if (err == nil) != okRaw {
t.Errorf("%s, raw: got %v, want error: %t", filename, err, okRaw)
}
if err := insert(makeValidUnicode(string(data))); err != nil {
t.Errorf("%s, after making valid: %v", filename, err)
}
}
check("final-nulls", false)
check("gin-gonic", true)
check("subchord", true)
}