diff --git a/pkg/index/compliance/compliance.go b/pkg/index/compliance/compliance.go index 6c1fdf1b..16f46499 100644 --- a/pkg/index/compliance/compliance.go +++ b/pkg/index/compliance/compliance.go @@ -6,12 +6,16 @@ import ( "testing" "time" + "github.com/gomods/athens/pkg/errors" "github.com/gomods/athens/pkg/index" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/technosophos/moniker" ) +// RunTests runs compliance tests for the given Indexer implementation. +// clearIndex is a function that must clear the entire storage so that +// tests can assume a clean state. func RunTests(t *testing.T, indexer index.Indexer, clearIndex func() error) { if err := clearIndex(); err != nil { t.Fatal(err) @@ -80,6 +84,23 @@ func RunTests(t *testing.T, indexer index.Indexer, clearIndex func() error) { }, limit: 0, }, + { + name: "duplicate module version", + desc: "if we try to index a module that already exists, a KindAlreadyExists must be returned", + preTest: func(t *testing.T) ([]*index.Line, time.Time) { + m := &index.Line{Path: "gomods.io/tobeduplicated", Version: "v0.1.0"} + err := indexer.Index(context.Background(), m.Path, m.Version) + if err != nil { + t.Fatal(err) + } + err = indexer.Index(context.Background(), m.Path, m.Version) + if !errors.Is(err, errors.KindAlreadyExists) { + t.Fatalf("expected an error of kind AlreadyExists but got %s", errors.KindText(err)) + } + return []*index.Line{m}, time.Time{} + }, + limit: 2000, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/index/mem/mem.go b/pkg/index/mem/mem.go index 4ffb5ffc..253f0d43 100644 --- a/pkg/index/mem/mem.go +++ b/pkg/index/mem/mem.go @@ -2,6 +2,7 @@ package mem import ( "context" + "fmt" "sync" "time" @@ -22,12 +23,17 @@ type indexer struct { func (i *indexer) Index(ctx context.Context, mod, ver string) error { const op errors.Op = "mem.Index" i.mu.Lock() + defer i.mu.Unlock() + for _, l := range i.lines { + if l.Path == mod && l.Version == ver { + return errors.E(op, fmt.Sprintf("%s@%s already indexed", mod, ver), errors.KindAlreadyExists) + } + } i.lines = append(i.lines, &index.Line{ Path: mod, Version: ver, Timestamp: time.Now(), }) - i.mu.Unlock() return nil } diff --git a/pkg/index/mysql/mysql.go b/pkg/index/mysql/mysql.go index 1ca85956..7298cd3f 100644 --- a/pkg/index/mysql/mysql.go +++ b/pkg/index/mysql/mysql.go @@ -12,6 +12,9 @@ import ( "github.com/gomods/athens/pkg/index" ) +// New returns a new Indexer with a MySQL implementation. +// It attempts to connect to the DB and create the index table +// if it doesn ot already exist. func New(cfg *config.MySQL) (index.Indexer, error) { dataSource := getMySQLSource(cfg) db, err := sql.Open("mysql", dataSource) @@ -65,7 +68,7 @@ func (i *indexer) Index(ctx context.Context, mod, ver string) error { time.Now().Format(time.RFC3339Nano), ) if err != nil { - return errors.E(op, err) + return errors.E(op, err, getKind(err)) } return nil } @@ -103,3 +106,15 @@ func getMySQLSource(cfg *config.MySQL) string { c.Params = cfg.Params return c.FormatDSN() } + +func getKind(err error) int { + mysqlErr, ok := err.(*mysql.MySQLError) + if !ok { + return errors.KindUnexpected + } + switch mysqlErr.Number { + case 1062: + return errors.KindAlreadyExists + } + return errors.KindUnexpected +} diff --git a/pkg/index/postgres/postgres.go b/pkg/index/postgres/postgres.go index 2f3c0b5a..6fb14c40 100644 --- a/pkg/index/postgres/postgres.go +++ b/pkg/index/postgres/postgres.go @@ -8,13 +8,16 @@ import ( "time" // register the driver with database/sql - _ "github.com/lib/pq" + "github.com/lib/pq" "github.com/gomods/athens/pkg/config" "github.com/gomods/athens/pkg/errors" "github.com/gomods/athens/pkg/index" ) +// New returns a new Indexer with a PostgreSQL implementation. +// It attempts to connect to the DB and create the index table +// if it doesn ot already exist. func New(cfg *config.Postgres) (index.Indexer, error) { dataSource := getPostgresSource(cfg) db, err := sql.Open("postgres", dataSource) @@ -64,7 +67,7 @@ func (i *indexer) Index(ctx context.Context, mod, ver string) error { time.Now().Format(time.RFC3339Nano), ) if err != nil { - return errors.E(op, err) + return errors.E(op, err, getKind(err)) } return nil } @@ -104,3 +107,15 @@ func getPostgresSource(cfg *config.Postgres) string { } return strings.Join(args, " ") } + +func getKind(err error) int { + pqerr, ok := err.(*pq.Error) + if !ok { + return errors.KindUnexpected + } + switch pqerr.Code { + case "23505": + return errors.KindAlreadyExists + } + return errors.KindUnexpected +} diff --git a/pkg/stash/stasher.go b/pkg/stash/stasher.go index 8d567cb5..1909ace4 100644 --- a/pkg/stash/stasher.go +++ b/pkg/stash/stasher.go @@ -74,7 +74,7 @@ func (s *stasher) Stash(ctx context.Context, mod, ver string) (string, error) { return "", errors.E(op, err) } err = s.indexer.Index(ctx, mod, v.Semver) - if err != nil { + if err != nil && !errors.Is(err, errors.KindAlreadyExists) { return "", errors.E(op, err) } return v.Semver, nil