Ver Fonte

Working MySQL repository tests, 88% coverage.

Frederic G. MARAND há 5 anos atrás
pai
commit
1a263defd2

+ 1 - 0
cmd/kurzd/dist.config.yml

@@ -2,3 +2,4 @@ database:
   # Code likely not to work on another engine because it adds ?parseTime=true
   driver: mysql
   dsn: <user>:<pass>@[<server>]/<database>
+  test_dsn: <user>:<pass>@[<server>]/<database>

+ 3 - 2
cmd/kurzd/export_content.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"database/sql"
 	"fmt"
 
@@ -48,8 +49,8 @@ ORDER BY url`)
 }
 
 func exportContentHandler(cmd *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 0 - 21
cmd/kurzd/kurzd.go

@@ -2,12 +2,9 @@
 package main
 
 import (
-	"github.com/spf13/viper"
 	"os"
 	"time"
 
-	"database/sql"
-
 	_ "github.com/go-sql-driver/mysql"
 )
 
@@ -43,21 +40,3 @@ type MapEntry struct {
 	Date1, Date2, Date3 time.Time
 	RefCount            uint32
 }
-
-func dbDial(dbDriver, dbDsn string) (*sql.DB, error) {
-	db, err := sql.Open(dbDriver, dbDsn)
-	if err != nil {
-		return nil, err
-	}
-	return db, nil
-}
-
-func parseDbCred() (driver, dsn string) {
-	viper.SetDefault("database.driver", "mysql")
-	viper.SetDefault("database.dsn", "root:root@tcp(localhost:3306)/kurz")
-
-	driver = viper.Get("database.driver").(string)
-	dsn = viper.Get("database.dsn").(string)
-	dsn += "?parseTime=true"
-	return
-}

+ 3 - 2
cmd/kurzd/migrate_down.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateDownHandler implements the "kurzd migrate down" command.
 func migrateDownHandler(_ *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 3 - 2
cmd/kurzd/migrate_downto.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateDownToHandler implements the "kurzd migrate down-to" command.
 func migrateDownToHandler(cmd *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 3 - 2
cmd/kurzd/migrate_redo.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateRedoHandler implements the "kurzd migrate redo" command.
 func migrateRedoHandler(_ *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 3 - 2
cmd/kurzd/migrate_status.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateStatusHandler implements the "kurzd migrate status" command.
 func migrateStatusHandler(_ *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 3 - 2
cmd/kurzd/migrate_up.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateUpHandler implements the "kurzd migrate up" command.
 func migrateUpHandler(_ *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 3 - 2
cmd/kurzd/migrate_upto.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateUpToHandler implements the "kurzd migrate up-to" command.
 func migrateUpToHandler(cmd *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 3 - 2
cmd/kurzd/migrate_version.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/infrastructure"
 	"os"
 
 	_ "code.osinet.fr/fgm/kurz/migrations"
@@ -22,8 +23,8 @@ func init() {
 
 // migrateVersionHandler implements the "kurzd migrate version" command.
 func migrateVersionHandler(_ *cobra.Command, args []string) {
-	dbDriver, dbDsn := parseDbCred()
-	db, err := dbDial(dbDriver, dbDsn)
+	dbDriver, dbDsn := infrastructure.ParseDbCred()
+	db, err := infrastructure.DbDial(dbDriver, dbDsn)
 	if err != nil {
 		panic(err)
 	}

+ 8 - 4
domain/domain_api.go

@@ -1,5 +1,7 @@
 package domain
 
+import "errors"
+
 func GetTargetURL(shortURL string) (target string, err error) {
 	su := ShortURL{URL: URL(shortURL)}
 	tu, err := shortURLRepository.GetTarget(su)
@@ -14,11 +16,13 @@ func GetTargetURL(shortURL string) (target string, err error) {
 
 func GetShortURL(targetURL string) (short string, isNew bool, err error) {
 	tu := TargetURL{URL: URL(targetURL)}
+	if tu.IsEmpty() {
+		// Zero values are OK for short and isNew.
+		err = errors.New("Empty target is not valid")
+		return
+	}
 	su, isNew, err := targetURLRepository.GetShort(tu)
-	if err != nil {
-		short = ""
-		isNew = false
-	} else {
+	if err == nil {
 		short = string(su.URL)
 	}
 

+ 0 - 1
domain/short_url.go

@@ -29,4 +29,3 @@ func NewUnspecifiedShortURL(target TargetURL) (ShortURL, error) {
 	su = ShortURL{url, target}
 	return su, nil
 }
-

+ 46 - 0
infrastructure/infratructure.go

@@ -0,0 +1,46 @@
+package infrastructure
+
+import (
+	"database/sql"
+	"github.com/spf13/viper"
+)
+
+const exampleValidHTTPURL = "https://example.com"
+
+/**
+DbDial is almost a proxy to sql.Open but guarantees that db will be nil if err is not nil.
+ */
+func DbDial(dbDriver, dbDsn string) (*sql.DB, error) {
+	db, err := sql.Open(dbDriver, dbDsn)
+	if err != nil {
+		return nil, err
+	}
+	return db, nil
+}
+
+/**
+ParseDbCred returns DB information from the supported configuration sources.
+ */
+func ParseDbCred() (driver, dsn string) {
+	viper.SetDefault("database.driver", "mysql")
+	viper.SetDefault("database.dsn", "root:root@tcp(localhost:3306)/kurz")
+
+	driver = viper.Get("database.driver").(string)
+	dsn = viper.Get("database.dsn").(string)
+	dsn += "?parseTime=true"
+	return
+}
+
+/**
+ParseDbCred returns Test DB information from the supported configuration sources.
+ */
+func ParseTestDbCred() (driver, dsn string) {
+	viper.SetDefault("database.driver", "mysql")
+	viper.SetDefault("database.dsn", "root:root@tcp(localhost:3306)/kurz")
+	viper.SetDefault("database.test_dsn", "root:root@tcp(localhost:3306)/kurz_test")
+
+	driver = viper.Get("database.driver").(string)
+	dsn = viper.Get("database.test_dsn").(string)
+	dsn += "?parseTime=true"
+	return
+}

+ 10 - 10
infrastructure/memory.go

@@ -8,21 +8,22 @@ import (
 
 type urlmap map[domain.URL]domain.URL
 
-var targets = make(urlmap)
-var shorts = make(urlmap)
+var shortRepo = MemoryShortURLRepository{make(urlmap)}
+var targetRepo = MemoryTargetURLRepository{make(urlmap)}
 
 type MemoryShortURLRepository struct {
+	targets urlmap
 }
 
 type MemoryTargetURLRepository struct {
-
+	shorts urlmap
 }
 
-func (r MemoryShortURLRepository) GetTarget(su domain.ShortURL) (domain.TargetURL, error) {
+func (sr MemoryShortURLRepository) GetTarget(su domain.ShortURL) (domain.TargetURL, error) {
 	var tu domain.TargetURL
 	var err error
 
-	if t, ok := targets[su.URL]; ok {
+	if t, ok := sr.targets[su.URL]; ok {
 		tu = domain.TargetURL{URL: t}
 	} else {
 		err = errors.New("not found")
@@ -30,9 +31,9 @@ func (r MemoryShortURLRepository) GetTarget(su domain.ShortURL) (domain.TargetUR
 	return tu, err
 }
 
-func (s MemoryTargetURLRepository) GetShort(tu domain.TargetURL) (su domain.ShortURL, isNew bool, err error) {
+func (tr MemoryTargetURLRepository) GetShort(tu domain.TargetURL) (su domain.ShortURL, isNew bool, err error) {
 	// If short exists, just return it.
-	if s, ok := shorts[tu.URL]; ok {
+	if s, ok := tr.shorts[tu.URL]; ok {
 		su = domain.ShortURL{URL: s}
 		return su, false, err
 	}
@@ -44,9 +45,8 @@ func (s MemoryTargetURLRepository) GetShort(tu domain.TargetURL) (su domain.Shor
 		return su, false, err
 	}
 
-	shorts[tu.URL] = su.URL
-	targets[su.URL] = tu.URL
+	tr.shorts[tu.URL] = su.URL
+	shortRepo.targets[su.URL] = tu.URL
 
 	return su, true, err
 }
-

+ 19 - 8
infrastructure/memory_test.go

@@ -5,13 +5,11 @@ import (
 	"testing"
 	)
 
-const example_valid_http_url = "https://example.com"
-
-func TestEmptyRepo(test *testing.T) {
+func TestMemoryEmptyRepo(test *testing.T) {
 
 	domain.RegisterRepositories(
-		MemoryShortURLRepository{},
-		MemoryTargetURLRepository{},
+		shortRepo,
+		targetRepo,
 	)
 
 	_, err := domain.GetTargetURL("whatever")
@@ -19,14 +17,14 @@ func TestEmptyRepo(test *testing.T) {
 		test.Error("Empty repository should not find a target for any URL")
 	}
 
-	s1, isNew, err := domain.GetShortURL(example_valid_http_url)
+	s1, isNew, err := domain.GetShortURL(exampleValidHTTPURL)
 	if err != nil {
 		test.Error("Creating a short URL for a valid URL should not fail")
 	}
 	if !isNew {
 		test.Error("The first short URL in an empty repository should be new")
 	}
-	s2, isNew, err := domain.GetShortURL(example_valid_http_url)
+	s2, isNew, err := domain.GetShortURL(exampleValidHTTPURL)
 	if err != nil {
 		test.Error("Creating a short URL for a valid URL should not fail")
 	}
@@ -41,7 +39,20 @@ func TestEmptyRepo(test *testing.T) {
 	if err != nil {
 		test.Error("Repository should find a target for an existing short URL")
 	}
-	if t != example_valid_http_url {
+	if t != exampleValidHTTPURL {
 		test.Error("Target URL for a short URL should match the target it was created for")
 	}
 }
+
+func TestMemorySad(test *testing.T) {
+
+	domain.RegisterRepositories(
+		MemoryShortURLRepository{},
+		MemoryTargetURLRepository{},
+	)
+
+	_, _, err := domain.GetShortURL("")
+	if err == nil {
+		test.Error("Empty target URL has no valid short URL")
+	}
+}

+ 57 - 7
infrastructure/mysql.go

@@ -1,21 +1,71 @@
 package infrastructure
 
 import (
-	"database/sql"
-
 	"code.osinet.fr/fgm/kurz/domain"
+	"database/sql"
+	"errors"
 )
 
 type MySQLShortURLRepository struct {
 	db *sql.DB
 }
 
-func (r MySQLShortURLRepository) GetTarget(su domain.ShortURL) (domain.TargetURL, error) {
+type MySQLTargetURLRepository struct {
+	db *sql.DB
+}
+
+func (sr MySQLShortURLRepository) GetTarget(su domain.ShortURL) (domain.TargetURL, error) {
 	var tu domain.TargetURL
-	return tu, nil
+	row := sr.db.QueryRow(`
+SELECT map.url
+FROM map
+WHERE map.hash = ?
+`, su.URL)
+	err := row.Scan(&tu.URL)
+	switch err {
+	case sql.ErrNoRows:
+		err = errors.New("target not found")
+	case nil:
+		break
+	default:
+		err = errors.New("storage read error")
+	}
+	return tu, err
 }
 
-func (r MySQLShortURLRepository) GetByTarget(tu domain.TargetURL) (domain.ShortURL, error) {
-	var su domain.ShortURL
-	return su, nil
+func (tr MySQLTargetURLRepository) GetShort(tu domain.TargetURL) (su domain.ShortURL, isNew bool, err error) {
+	// TODO future versions may have multiple shorts for a target, and choose a
+	// specific short based on the domain and Kurz user. For now just ensure we
+	// don't get more than one.
+	row := tr.db.QueryRow(`
+SELECT map.hash
+FROM map
+LIMIT 1
+`)
+	err = row.Scan(&su.URL)
+	switch err {
+	case sql.ErrNoRows:
+		// If it doesn't exist, attempt to create it.
+		su, err = domain.NewUnspecifiedShortURL(tu)
+		if err != nil {
+			// Creation failed.
+			return su, false, err
+		}
+		_, err = tr.db.Exec(`
+INSERT INTO map(hash, url, date1, date2, date3, refcount)
+VALUES (?, ?, CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), 0)
+`, su.URL, tu.URL)
+		if err != nil {
+			err = errors.New("storage write error")
+		}
+		isNew = true
+
+	case nil:
+		break
+
+	default:
+		err = errors.New("storage read error")
+	}
+
+	return
 }

+ 101 - 0
infrastructure/mysql_test.go

@@ -0,0 +1,101 @@
+package infrastructure
+
+import (
+	"code.osinet.fr/fgm/kurz/domain"
+	"database/sql"
+	"github.com/pressly/goose"
+	"os"
+	"testing"
+
+	_ "github.com/go-sql-driver/mysql"
+)
+
+func TestMySQLEmptyRepo(test *testing.T) {
+	db := mySQLTestSetup(test)
+	defer db.Close()
+	test.Run("empty repo", func(subTest *testing.T) {
+		_, err := domain.GetTargetURL("whatever")
+		if err == nil {
+			subTest.Error("Empty repository should not find a target for any URL")
+		}
+
+		s1, isNew, err := domain.GetShortURL(exampleValidHTTPURL)
+		if err != nil {
+			subTest.Error("Creating a short URL for a valid URL should not fail")
+		}
+		if !isNew {
+			subTest.Error("The first short URL in an empty repository should be new")
+		}
+		s2, isNew, err := domain.GetShortURL(exampleValidHTTPURL)
+		if err != nil {
+			subTest.Error("Creating a short URL for a valid URL should not fail")
+		}
+		if isNew {
+			subTest.Error("The second short URL for an already shortened URL should not be new")
+		}
+		if s1 != s2 {
+			subTest.Error("The second short URL for an already shortened URL should be the same as the first one")
+		}
+
+		t, err := domain.GetTargetURL(s1)
+		if err != nil {
+			subTest.Error("Repository should find a target for an existing short URL")
+		}
+		if t != exampleValidHTTPURL {
+			subTest.Error("Target URL for a short URL should match the target it was created for")
+		}
+
+	})
+
+	mySQLTestTeardown(test, db)
+}
+
+func TestMySQLSad(test *testing.T) {
+	db := mySQLTestSetup(test)
+	defer db.Close()
+
+	test.Run("Sad", func(subTest *testing.T) {
+		_, _, err := domain.GetShortURL("")
+		if err == nil {
+			test.Error("Empty target URL has no valid short URL")
+		}
+	})
+	mySQLTestTeardown(test, db)
+}
+
+/**
+mySQLTestSetup opens the database and initialized it from the production database schema.
+ */
+func mySQLTestSetup(t *testing.T) *sql.DB {
+	db, err := DbDial(ParseTestDbCred())
+	if err != nil {
+		panic(err)
+	}
+
+	// Ensure current schema for test DB.
+	cwd, _ := os.Getwd()
+	goose.Up(db, cwd)
+
+	// Ensure empty test DB.
+	_, err = db.Exec("DELETE FROM map")
+	if err != nil {
+		t.Error("setup failed to clean test database")
+	}
+
+	sr := MySQLShortURLRepository{db}
+	tr := MySQLTargetURLRepository{db}
+	domain.RegisterRepositories(sr, tr)
+	return db
+}
+
+func mySQLTestTeardown(t *testing.T, db *sql.DB) {
+	_, err := db.Exec("DELETE FROM map")
+	if err != nil {
+		t.Error("teardown failed to clean test database")
+	}
+}
+
+func TestMain(m *testing.M) {
+	exitCode := m.Run()
+	os.Exit(exitCode)
+}