moved from datasql to datastore, added unit test for users

This commit is contained in:
Brad Rydzewski 2014-09-26 00:10:48 -07:00
parent 557c730b52
commit 54caa88281
18 changed files with 551 additions and 83 deletions

View file

@ -1,4 +1,4 @@
package datasql
package database
import (
"github.com/drone/drone/shared/model"
@ -25,7 +25,7 @@ func (db *Commitstore) GetCommit(id int64) (*model.Commit, error) {
// datastore for the specified repo and sha
func (db *Commitstore) GetCommitSha(repo *model.Repo, branch, sha string) (*model.Commit, error) {
var commit = new(model.Commit)
var err = meddler.QueryRow(db, commit, commitShaQuery, repo.ID, branch, sha)
var err = meddler.QueryRow(db, commit, rebind(commitShaQuery), repo.ID, branch, sha)
return commit, err
}
@ -34,7 +34,7 @@ func (db *Commitstore) GetCommitSha(repo *model.Repo, branch, sha string) (*mode
// and branch.
func (db *Commitstore) GetCommitLast(repo *model.Repo, branch string) (*model.Commit, error) {
var commit = new(model.Commit)
var err = meddler.QueryRow(db, commit, commitLastQuery, repo.ID, branch)
var err = meddler.QueryRow(db, commit, rebind(commitLastQuery), repo.ID, branch)
return commit, err
}
@ -42,7 +42,7 @@ func (db *Commitstore) GetCommitLast(repo *model.Repo, branch string) (*model.Co
// from the datastore for the specified repository.
func (db *Commitstore) GetCommitList(repo *model.Repo) ([]*model.Commit, error) {
var commits []*model.Commit
var err = meddler.QueryAll(db, &commits, commitListQuery)
var err = meddler.QueryAll(db, &commits, rebind(commitListQuery))
return commits, err
}
@ -64,14 +64,14 @@ func (db *Commitstore) PutCommit(commit *model.Commit) error {
// DelCommit removes the commit from the datastore.
func (db *Commitstore) DelCommit(commit *model.Commit) error {
var _, err = db.Exec(commitDeleteStmt, commit.ID)
var _, err = db.Exec(rebind(commitDeleteStmt), commit.ID)
return err
}
// KillCommits updates all pending or started commits
// in the datastore settings the status to killed.
func (db *Commitstore) KillCommits() error {
var _, err = db.Exec(commitKillStmt)
var _, err = db.Exec(rebind(commitKillStmt))
return err
}

View file

@ -0,0 +1 @@
package database

View file

@ -0,0 +1,67 @@
package database
import (
"database/sql"
"github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/datastore/database/migrate"
"github.com/BurntSushi/migration"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"github.com/russross/meddler"
)
const (
driverPostgres = "postgres"
driverSqlite = "sqlite3"
driverMysql = "mysql"
)
// Connect is a helper function that establishes a new
// database connection and auto-generates the database
// schema. If the database already exists, it will perform
// and update as needed.
func Connect(driver, datasource string) (*sql.DB, error) {
switch driver {
case driverPostgres:
meddler.Default = meddler.PostgreSQL
case driverSqlite:
meddler.Default = meddler.SQLite
case driverMysql:
meddler.Default = meddler.MySQL
}
migration.DefaultGetVersion = migrate.GetVersion
migration.DefaultSetVersion = migrate.SetVersion
var migrations = []migration.Migrator{
migrate.Setup,
}
return migration.Open(driver, datasource, migrations)
}
// MustConnect is a helper function that create a
// new database commention and auto-generates the
// database schema. An error causes a panic.
func MustConnect(driver, datasource string) *sql.DB {
db, err := Connect(driver, datasource)
if err != nil {
panic(err)
}
return db
}
// New returns a new DataStore
func New(db *sql.DB) datastore.Datastore {
return struct {
*Userstore
*Permstore
*Repostore
*Commitstore
}{
NewUserstore(db),
NewPermstore(db),
NewRepostore(db),
NewCommitstore(db),
}
}

View file

@ -0,0 +1,46 @@
package migrate
import (
"strconv"
"strings"
"github.com/russross/meddler"
)
// transform is a helper function that transforms sql
// statements to work with multiple database types.
func transform(stmt string) string {
switch meddler.Default {
case meddler.MySQL:
stmt = strings.Replace(stmt, "AUTOINCREMENT", "AUTO_INCREMENT", -1)
case meddler.PostgreSQL:
stmt = strings.Replace(stmt, "INTEGER PRIMARY KEY AUTOINCREMENT", "SERIAL PRIMARY KEY", -1)
stmt = strings.Replace(stmt, "BLOB", "BYTEA", -1)
}
return stmt
}
// rebind is a helper function that changes the sql
// bind type from ? to $ for postgres queries.
func rebind(query string) string {
if meddler.Default != meddler.PostgreSQL {
return query
}
qb := []byte(query)
// Add space enough for 10 params before we have to allocate
rqb := make([]byte, 0, len(qb)+10)
j := 1
for _, b := range qb {
if b == '?' {
rqb = append(rqb, '$')
for _, b := range strconv.Itoa(j) {
rqb = append(rqb, byte(b))
}
j++
} else {
rqb = append(rqb, b)
}
}
return string(rqb)
}

View file

@ -0,0 +1,118 @@
package migrate
import (
"github.com/BurntSushi/migration"
)
// Setup is the database migration function that
// will setup the initial SQL database structure.
func Setup(tx migration.LimitedTx) error {
var stmts = []string{
blobTable,
userTable,
repoTable,
permTable,
commitTable,
}
for _, stmt := range stmts {
_, err := tx.Exec(transform(stmt))
if err != nil {
return err
}
}
return nil
}
var userTable = `
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_remote VARCHAR(255)
,user_login VARCHAR(255)
,user_access VARCHAR(255)
,user_secret VARCHAR(255)
,user_name VARCHAR(255)
,user_email VARCHAR(255)
,user_gravatar VARCHAR(255)
,user_token VARCHAR(255)
,user_admin BOOLEAN
,user_active BOOLEAN
,user_syncing BOOLEAN
,user_created INTEGER
,user_updated INTEGER
,user_synced INTEGER
,UNIQUE(user_token)
,UNIQUE(user_remote, user_login)
);
`
var permTable = `
CREATE TABLE IF NOT EXISTS perms (
perm_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_id INTEGER
,repo_id INTEGER
,perm_read BOOLEAN
,perm_write BOOLEAN
,perm_admin BOOLEAN
,perm_created INTEGER
,perm_updated INTEGER
,UNIQUE (repo_id, user_id)
);
`
var repoTable = `
CREATE TABLE IF NOT EXISTS repos (
repo_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_id INTEGER
,repo_remote VARCHAR(255)
,repo_host VARCHAR(255)
,repo_owner VARCHAR(255)
,repo_name VARCHAR(255)
,repo_url VARCHAR(1024)
,repo_clone_url VARCHAR(255)
,repo_git_url VARCHAR(255)
,repo_ssh_url VARCHAR(255)
,repo_active BOOLEAN
,repo_private BOOLEAN
,repo_privileged BOOLEAN
,repo_post_commit BOOLEAN
,repo_pull_request BOOLEAN
,repo_public_key BLOB
,repo_private_key BLOB
,repo_params BLOB
,repo_timeout INTEGER
,repo_created INTEGER
,repo_updated INTEGER
,UNIQUE(repo_host, repo_owner, repo_name)
);
`
var commitTable = `
CREATE TABLE IF NOT EXISTS commits (
commit_id INTEGER PRIMARY KEY AUTOINCREMENT
,repo_id INTEGER
,commit_status VARCHAR(255)
,commit_started INTEGER
,commit_finished INTEGER
,commit_duration INTEGER
,commit_sha VARCHAR(255)
,commit_branch VARCHAR(255)
,commit_pr VARCHAR(255)
,commit_author VARCHAR(255)
,commit_gravatar VARCHAR(255)
,commit_timestamp VARCHAR(255)
,commit_message VARCHAR(255)
,commit_yaml BLOB
,commit_created INTEGER
,commit_updated INTEGER
,UNIQUE(commit_sha, commit_branch, repo_id)
);
`
var blobTable = `
CREATE TABLE IF NOT EXISTS blobs (
blob_id INTEGER PRIMARY KEY AUTOINCREMENT
,blob_path VARCHAR(1024)
,blob_data BLOB
,UNIQUE(blob_path)
);
`

View file

@ -0,0 +1,57 @@
package migrate
import (
"github.com/BurntSushi/migration"
)
// GetVersion gets the migration version from the database,
// creating the migration table if it does not already exist.
func GetVersion(tx migration.LimitedTx) (int, error) {
v, err := getVersion(tx)
if err != nil {
if err := createVersionTable(tx); err != nil {
return 0, err
}
return getVersion(tx)
}
return v, nil
}
// SetVersion sets the migration version in the database,
// creating the migration table if it does not already exist.
func SetVersion(tx migration.LimitedTx, version int) error {
if err := setVersion(tx, version); err != nil {
if err := createVersionTable(tx); err != nil {
return err
}
return setVersion(tx, version)
}
return nil
}
// setVersion updates the migration version in the database.
func setVersion(tx migration.LimitedTx, version int) error {
_, err := tx.Exec(rebind("UPDATE migration_version SET version = ?"), version)
return err
}
// getVersion gets the migration version in the database.
func getVersion(tx migration.LimitedTx) (int, error) {
var version int
row := tx.QueryRow("SELECT version FROM migration_version")
if err := row.Scan(&version); err != nil {
return 0, err
}
return version, nil
}
// createVersionTable creates the version table and inserts the
// initial value (0) into the database.
func createVersionTable(tx migration.LimitedTx) error {
_, err := tx.Exec("CREATE TABLE migration_version ( version INTEGER )")
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO migration_version (version) VALUES (0)")
return err
}

View file

@ -1,4 +1,4 @@
package datasql
package database
import (
"github.com/drone/drone/shared/model"
@ -17,7 +17,7 @@ func NewPermstore(db meddler.DB) *Permstore {
// the datastore for the given repository.
func (db *Repostore) GetPerm(user *model.User, repo *model.Repo) (*model.Perm, error) {
var perm = new(model.Perm)
var err = meddler.QueryRow(db, perm, permQuery, user.ID, repo.ID)
var err = meddler.QueryRow(db, perm, rebind(permQuery), user.ID, repo.ID)
return perm, err
}
@ -33,7 +33,7 @@ func (db *Repostore) PutPerm(perm *model.Perm) error {
// DelPerm removes permission from the datastore.
func (db *Repostore) DelPerm(perm *model.Perm) error {
var _, err = db.Exec(permDeleteStmt, perm.ID)
var _, err = db.Exec(rebind(permDeleteStmt), perm.ID)
return err
}

View file

@ -0,0 +1 @@
package database

View file

@ -1,4 +1,4 @@
package datasql
package database
import (
"github.com/drone/drone/shared/model"
@ -25,7 +25,7 @@ func (db *Repostore) GetRepo(id int64) (*model.Repo, error) {
// for the specified remote, owner and name.
func (db *Repostore) GetRepoName(remote, owner, name string) (*model.Repo, error) {
var repo = new(model.Repo)
var err = meddler.QueryRow(db, repo, repoNameQuery, remote, owner, name)
var err = meddler.QueryRow(db, repo, rebind(repoNameQuery), remote, owner, name)
return repo, err
}
@ -33,7 +33,7 @@ func (db *Repostore) GetRepoName(remote, owner, name string) (*model.Repo, error
// the datastore accessible by the given user ID.
func (db *Repostore) GetRepoList(user *model.User) ([]*model.Repo, error) {
var repos []*model.Repo
var err = meddler.QueryAll(db, &repos, repoListQuery)
var err = meddler.QueryAll(db, &repos, rebind(repoListQuery))
return repos, err
}
@ -49,7 +49,7 @@ func (db *Repostore) PutRepo(repo *model.Repo) error {
// DelRepo removes the repo from the datastore.
func (db *Repostore) DelRepo(repo *model.Repo) error {
var _, err = db.Exec(repoDeleteStmt, repo.ID)
var _, err = db.Exec(rebind(repoDeleteStmt), repo.ID)
return err
}

View file

@ -0,0 +1 @@
package database

View file

@ -1,4 +1,4 @@
package datasql
package database
import (
"github.com/drone/drone/shared/model"
@ -15,7 +15,7 @@ func NewUserstore(db meddler.DB) *Userstore {
// GetUser retrieves a specific user from the
// datastore for the given ID.
func (db *Repostore) GetUser(id int64) (*model.User, error) {
func (db *Userstore) GetUser(id int64) (*model.User, error) {
var usr = new(model.User)
var err = meddler.Load(db, userTable, usr, id)
return usr, err
@ -23,41 +23,41 @@ func (db *Repostore) GetUser(id int64) (*model.User, error) {
// GetUserLogin retrieves a user from the datastore
// for the specified remote and login name.
func (db *Repostore) GetUserLogin(remote, login string) (*model.User, error) {
func (db *Userstore) GetUserLogin(remote, login string) (*model.User, error) {
var usr = new(model.User)
var err = meddler.QueryRow(db, usr, userLoginQuery)
var err = meddler.QueryRow(db, usr, rebind(userLoginQuery), remote, login)
return usr, err
}
// GetUserToken retrieves a user from the datastore
// with the specified token.
func (db *Repostore) GetUserToken(token string) (*model.User, error) {
func (db *Userstore) GetUserToken(token string) (*model.User, error) {
var usr = new(model.User)
var err = meddler.QueryRow(db, usr, userTokenQuery)
var err = meddler.QueryRow(db, usr, rebind(userTokenQuery), token)
return usr, err
}
// GetUserList retrieves a list of all users from
// the datastore that are registered in the system.
func (db *Repostore) GetUserList() ([]*model.User, error) {
func (db *Userstore) GetUserList() ([]*model.User, error) {
var users []*model.User
var err = meddler.QueryAll(db, &users, userListQuery)
var err = meddler.QueryAll(db, &users, rebind(userListQuery))
return users, err
}
// PostUser saves a User in the datastore.
func (db *Repostore) PostUser(user *model.User) error {
func (db *Userstore) PostUser(user *model.User) error {
return meddler.Save(db, userTable, user)
}
// PutUser saves a user in the datastore.
func (db *Repostore) PutUser(user *model.User) error {
func (db *Userstore) PutUser(user *model.User) error {
return meddler.Save(db, userTable, user)
}
// DelUser removes the user from the datastore.
func (db *Repostore) DelUser(user *model.User) error {
var _, err = db.Exec(userDeleteStmt, user.ID)
func (db *Userstore) DelUser(user *model.User) error {
var _, err = db.Exec(rebind(userDeleteStmt), user.ID)
return err
}

View file

@ -0,0 +1,203 @@
package database
import (
"testing"
"github.com/drone/drone/shared/model"
"github.com/franela/goblin"
)
func TestPackagestore(t *testing.T) {
db := MustConnect("sqlite3", ":memory:")
us := NewUserstore(db)
defer db.Close()
g := goblin.Goblin(t)
g.Describe("Userstore", func() {
// before each test be sure to purge the package
// table data from the database.
g.BeforeEach(func() {
db.Exec("DELETE FROM users")
})
g.It("Should Put a User", func() {
user := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
err := us.PostUser(&user)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID != 0).IsTrue()
})
g.It("Should Post a User", func() {
user := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
err := us.PostUser(&user)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID != 0).IsTrue()
})
g.It("Should Get a User", func() {
user := model.User{
Login: "joe",
Remote: "github.com",
Access: "f0b461ca586c27872b43a0685cbc2847",
Secret: "976f22a5eef7caacb7e678d6c52f49b1",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Gravatar: "b9015b0857e16ac4d94a0ffd9a0b79c8",
Token: "e42080dddf012c718e476da161d21ad5",
Active: true,
Admin: true,
Created: 1398065343,
Updated: 1398065344,
Synced: 1398065345,
}
us.PostUser(&user)
getuser, err := us.GetUser(user.ID)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID).Equal(getuser.ID)
g.Assert(user.Login).Equal(getuser.Login)
g.Assert(user.Remote).Equal(getuser.Remote)
g.Assert(user.Access).Equal(getuser.Access)
g.Assert(user.Secret).Equal(getuser.Secret)
g.Assert(user.Name).Equal(getuser.Name)
g.Assert(user.Email).Equal(getuser.Email)
g.Assert(user.Gravatar).Equal(getuser.Gravatar)
g.Assert(user.Token).Equal(getuser.Token)
g.Assert(user.Active).Equal(getuser.Active)
g.Assert(user.Admin).Equal(getuser.Admin)
g.Assert(user.Created).Equal(getuser.Created)
g.Assert(user.Updated).Equal(getuser.Updated)
g.Assert(user.Synced).Equal(getuser.Synced)
})
g.It("Should Get a User By Login", func() {
user := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
us.PostUser(&user)
getuser, err := us.GetUserLogin(user.Remote, user.Login)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID).Equal(getuser.ID)
g.Assert(user.Login).Equal(getuser.Login)
g.Assert(user.Remote).Equal(getuser.Remote)
})
g.It("Should Get a User By Token", func() {
user := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
us.PostUser(&user)
getuser, err := us.GetUserToken(user.Token)
g.Assert(err == nil).IsTrue()
g.Assert(user.ID).Equal(getuser.ID)
g.Assert(user.Token).Equal(getuser.Token)
})
g.It("Should Enforce Unique User Token", func() {
user1 := model.User{
Login: "jane",
Remote: "github.com",
Name: "Jane Doe",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
user2 := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
err1 := us.PostUser(&user1)
err2 := us.PostUser(&user2)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsFalse()
})
g.It("Should Enforce Unique User Remote and Login", func() {
user1 := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
user2 := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "ab20g0ddaf012c744e136da16aa21ad9",
}
err1 := us.PostUser(&user1)
err2 := us.PostUser(&user2)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsFalse()
})
g.It("Should Get a User List", func() {
user1 := model.User{
Login: "jane",
Remote: "github.com",
Name: "Jane Doe",
Email: "foo@bar.com",
Token: "ab20g0ddaf012c744e136da16aa21ad9",
}
user2 := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
us.PostUser(&user1)
us.PostUser(&user2)
users, err := us.GetUserList()
g.Assert(err == nil).IsTrue()
g.Assert(len(users)).Equal(2)
g.Assert(users[0].Login).Equal(user1.Login)
g.Assert(users[0].Remote).Equal(user1.Remote)
g.Assert(users[0].Name).Equal(user1.Name)
g.Assert(users[0].Email).Equal(user1.Email)
g.Assert(users[0].Token).Equal(user1.Token)
})
g.It("Should Del a User", func() {
user := model.User{
Login: "joe",
Remote: "github.com",
Name: "Joe Sixpack",
Email: "foo@bar.com",
Token: "e42080dddf012c718e476da161d21ad5",
}
us.PostUser(&user)
_, err1 := us.GetUser(user.ID)
err2 := us.DelUser(&user)
_, err3 := us.GetUser(user.ID)
g.Assert(err1 == nil).IsTrue()
g.Assert(err2 == nil).IsTrue()
g.Assert(err3 == nil).IsFalse()
})
})
}

View file

@ -0,0 +1,32 @@
package database
import (
"strconv"
"github.com/russross/meddler"
)
// rebind is a helper function that changes the sql
// bind type from ? to $ for postgres queries.
func rebind(query string) string {
if meddler.Default != meddler.PostgreSQL {
return query
}
qb := []byte(query)
// Add space enough for 10 params before we have to allocate
rqb := make([]byte, 0, len(qb)+10)
j := 1
for _, b := range qb {
if b == '?' {
rqb = append(rqb, '$')
for _, b := range strconv.Itoa(j) {
rqb = append(rqb, byte(b))
}
j++
} else {
rqb = append(rqb, b)
}
}
return string(rqb)
}

View file

@ -1 +0,0 @@
package datasql

View file

@ -1,54 +0,0 @@
package datasql
import (
"database/sql"
"github.com/drone/drone/server/blobstore/blobsql"
"github.com/drone/drone/server/datastore"
"github.com/drone/drone/shared/model"
"github.com/astaxie/beego/orm"
_ "github.com/mattn/go-sqlite3"
)
const (
driverPostgres = "postgres"
driverSqlite = "sqlite3"
driverMysql = "mysql"
databaseName = "default"
)
// Connect is a helper function that establishes a new
// database connection and auto-generates the database
// schema. If the database already exists, it will perform
// and update as needed.
func Connect(driver, datasource string) (*sql.DB, error) {
defer orm.ResetModelCache()
orm.RegisterDriver(driverSqlite, orm.DR_Sqlite)
orm.RegisterDataBase(databaseName, driver, datasource)
orm.RegisterModel(new(model.User))
orm.RegisterModel(new(model.Perm))
orm.RegisterModel(new(model.Repo))
orm.RegisterModel(new(model.Commit))
orm.RegisterModel(new(blobsql.Blob))
var err = orm.RunSyncdb(databaseName, true, true)
if err != nil {
return nil, err
}
return orm.GetDB(databaseName)
}
// New returns a new DataStore
func New(db *sql.DB) datastore.Datastore {
return struct {
*Userstore
*Permstore
*Repostore
*Commitstore
}{
NewUserstore(db),
NewPermstore(db),
NewRepostore(db),
NewCommitstore(db),
}
}

View file

@ -1 +0,0 @@
package datasql

View file

@ -1 +0,0 @@
package datasql

View file

@ -1 +0,0 @@
package datasql