From ff5f6b64ffc4b1b5ff30997e929e6f4aa2426b33 Mon Sep 17 00:00:00 2001 From: Erhan Yakut Date: Sun, 28 Jun 2020 23:15:48 +0300 Subject: [PATCH] add some tests --- cmd/passwall-server/main.go | 6 +- go.mod | 2 + go.sum | 2 + internal/api/health_test.go | 8 +- internal/api/login.go | 21 +- internal/api/login_test.go | 255 ++++++++++++++++++ internal/router/router.go | 1 + internal/storage/database.go | 10 +- .../storage/login/login_repository_test.go | 76 ++++++ 9 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 internal/api/login_test.go create mode 100644 internal/storage/login/login_repository_test.go diff --git a/cmd/passwall-server/main.go b/cmd/passwall-server/main.go index 90f3622..68a6c3a 100644 --- a/cmd/passwall-server/main.go +++ b/cmd/passwall-server/main.go @@ -21,11 +21,13 @@ func main() { log.Fatal(err) } - s, err := storage.New(&cfg.Database) + db, err := storage.DBConn(&cfg.Database) if err != nil { - logger.Fatalf("failed to open storage: %s\n", err) + log.Fatal(err) } + s := storage.New(db) + // Migrate database tables // TODO: Migrate should be in storege.New functions of categories app.MigrateSystemTables(s) diff --git a/go.mod b/go.mod index debfcc1..5a8a5fb 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/didip/tollbooth v4.0.2+incompatible github.com/gin-gonic/gin v1.6.2 github.com/go-playground/validator/v10 v10.2.0 + github.com/go-test/deep v1.0.6 github.com/golang/protobuf v1.4.0 // indirect github.com/gorilla/mux v1.7.4 github.com/heroku/x v0.0.22 @@ -22,6 +23,7 @@ require ( github.com/satori/go.uuid v1.2.0 github.com/sethvargo/go-password v0.1.3 github.com/spf13/viper v1.6.2 + github.com/stretchr/testify v1.5.1 github.com/ulule/limiter/v3 v3.5.0 github.com/urfave/negroni v1.0.0 golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd diff --git a/go.sum b/go.sum index 042aed7..35a399e 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRf github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8= +github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= diff --git a/internal/api/health_test.go b/internal/api/health_test.go index 9682792..1da0661 100644 --- a/internal/api/health_test.go +++ b/internal/api/health_test.go @@ -1,6 +1,7 @@ package api import ( + "log" "net/http" "net/http/httptest" "testing" @@ -22,7 +23,12 @@ func TestHealthCheck(t *testing.T) { LogMode: false, } - db, err := storage.New(mockDBConfig) + mockDB, err := storage.DBConn(mockDBConfig) + if err != nil { + log.Fatal(err) + } + + db := storage.New(mockDB) req, err := http.NewRequest("GET", "/health", nil) if err != nil { diff --git a/internal/api/login.go b/internal/api/login.go index d967cdc..e33f7e1 100644 --- a/internal/api/login.go +++ b/internal/api/login.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "net/http" "strconv" @@ -16,7 +17,7 @@ const ( LoginDeleteSuccess = "Login deleted successfully!" ) -// FindAll ... +// FindAllLogins ... func FindAllLogins(s storage.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var err error @@ -38,7 +39,7 @@ func FindAllLogins(s storage.Store) http.HandlerFunc { } } -// FindByID ... +// FindLoginsByID ... func FindLoginsByID(s storage.Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -153,3 +154,19 @@ func DeleteLogin(s storage.Store) http.HandlerFunc { RespondWithJSON(w, http.StatusOK, response) } } + +// Test endpoint ... +func TestLogin(s storage.Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + schema := r.Context().Value("schema").(string) + fmt.Println(schema) + + response := model.Response{ + Code: http.StatusOK, + Status: Success, + Message: "Test success!", + } + RespondWithJSON(w, http.StatusOK, response) + } +} diff --git a/internal/api/login_test.go b/internal/api/login_test.go new file mode 100644 index 0000000..59f53cd --- /dev/null +++ b/internal/api/login_test.go @@ -0,0 +1,255 @@ +package api + +import ( + "context" + "database/sql/driver" + "encoding/json" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-test/deep" + "github.com/gorilla/mux" + "github.com/jinzhu/gorm" + "github.com/pass-wall/passwall-server/internal/storage" + "github.com/pass-wall/passwall-server/model" + "github.com/stretchr/testify/assert" +) + +func TestFindAllLogins(t *testing.T) { + w := httptest.NewRecorder() + + // Create mock db + mockDB, mock := dbSetup() + + // Initialize router + r := routersSetup(mockDB) + + // Generate dummy login + var logins []model.Login + login := model.Login{ + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeletedAt: nil, + Title: "Dummy Title", + URL: "http://dummy.com", + Username: "dummyuser", + Password: "GRr4f5bWKolEVw8EjXSryNPvVLEorL3VILyYhMUkZiize6FlBvP4C1I=", // Encrypted "dummypassword" + } + logins = append(logins, login) + + // Add dummy login to dummy db table + rows := sqlmock. + NewRows([]string{"id", "created_at", "updated_at", "deleted_at", "title", "url", "username", "password"}). + AddRow(login.ID, login.CreatedAt, login.UpdatedAt, login.DeletedAt, login.Title, login.URL, login.Username, login.Password) + + // Define expected query + const sqlSelectOne = `SELECT * FROM "user-test"."logins"` + mock.ExpectQuery(regexp.QuoteMeta(sqlSelectOne)). + WillReturnRows(rows) + + // Make request + r.ServeHTTP(w, httptest.NewRequest("GET", "/api/logins", nil)) + + // Check status code + assert.Equal(t, http.StatusOK, w.Code, "Did not get expected HTTP status code, got") + + // Unmarshall response + var resultLogins []model.Login + decoder := json.NewDecoder(w.Body) + if err := decoder.Decode(&resultLogins); err != nil { + t.Error(err) + } + + // Compare response and table data + assert.Nil(t, deep.Equal(logins, resultLogins)) + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + +} + +func TestFindLoginsByID(t *testing.T) { + w := httptest.NewRecorder() + + // Create mock db + mockDB, mock := dbSetup() + + // Initialize router + r := routersSetup(mockDB) + + // Generate dummy login + loginDTO := &model.LoginDTO{ + ID: 1, + Title: "Dummy Title", + URL: "http://dummy.com", + Username: "dummyuser", + Password: "dummypassword", + } + + encryptedPassword := "GRr4f5bWKolEVw8EjXSryNPvVLEorL3VILyYhMUkZiize6FlBvP4C1I=" + + // Add dummy login to dummy db table + rows := sqlmock. + NewRows([]string{"id", "created_at", "updated_at", "deleted_at", "title", "url", "username", "password"}). + AddRow(loginDTO.ID, time.Now(), time.Now(), nil, loginDTO.Title, loginDTO.URL, loginDTO.Username, encryptedPassword) + + // Define expected query + const sqlSelectOne = `SELECT * FROM "user-test"."logins" WHERE "user-test"."logins"."deleted_at" IS NULL AND ((id = $1))` + mock.ExpectQuery(regexp.QuoteMeta(sqlSelectOne)). + WithArgs(loginDTO.ID). + WillReturnRows(rows) + + // Make request + r.ServeHTTP(w, httptest.NewRequest("GET", "/api/logins/1", nil)) + + // Check status code + assert.Equal(t, http.StatusOK, w.Code, "Did not get expected HTTP status code, got") + + // Unmarshall response + resultLogin := new(model.LoginDTO) + decoder := json.NewDecoder(w.Body) + if err := decoder.Decode(&resultLogin); err != nil { + t.Error(err) + } + + // Compare response and table data + assert.Nil(t, deep.Equal(loginDTO, resultLogin)) + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func dbSetup() (*gorm.DB, sqlmock.Sqlmock) { + db, mock, _ := sqlmock.New() + DB, _ := gorm.Open("postgres", db) + // DB.LogMode(true) + + return DB, mock +} + +func routersSetup(db *gorm.DB) *mux.Router { + + // Create storage with mock db + store := storage.New(db) + + // Initialize router + apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter() + + // Login endpoints + apiRouter.Handle("/logins", contextMiddleware(FindAllLogins(store))).Methods(http.MethodGet) + apiRouter.Handle("/logins", contextMiddleware(CreateLogin(store))).Methods(http.MethodPost) + apiRouter.Handle("/logins/{id:[0-9]+}", contextMiddleware(FindLoginsByID(store))).Methods(http.MethodGet) + apiRouter.Handle("/logins/{id:[0-9]+}", contextMiddleware(UpdateLogin(store))).Methods(http.MethodPut) + apiRouter.Handle("/logins/{id:[0-9]+}", contextMiddleware(DeleteLogin(store))).Methods(http.MethodDelete) + + return apiRouter +} + +func contextMiddleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + ctxWithID := context.WithValue(ctx, "id", 1) + ctxWithAuthorized := context.WithValue(ctxWithID, "authorized", true) + ctxWithSchema := context.WithValue(ctxWithAuthorized, "schema", "user-test") + + h.ServeHTTP(w, r.WithContext(ctxWithSchema)) + }) +} + +// func TestDeleteLogin(t *testing.T) { +// w := httptest.NewRecorder() + +// // Create mock db +// mockDB, mock := dbSetup() + +// // Initialize router +// r := routersSetup(mockDB) + +// // Generate dummy login +// loginDTO := &model.LoginDTO{ +// ID: 1, +// Title: "Dummy Title", +// URL: "http://dummy.com", +// Username: "dummyuser", +// Password: "dummypassword", +// } + +// encryptedPassword := "GRr4f5bWKolEVw8EjXSryNPvVLEorL3VILyYhMUkZiize6FlBvP4C1I=" + +// // Add dummy login to dummy db table +// rows := sqlmock. +// NewRows([]string{"id", "created_at", "updated_at", "deleted_at", "title", "url", "username", "password"}). +// AddRow(loginDTO.ID, time.Now(), time.Now(), nil, loginDTO.Title, loginDTO.URL, loginDTO.Username, encryptedPassword) + +// // Define expected query +// const sqlSelectOne = `SELECT * FROM "user-test"."logins" WHERE "user-test"."logins"."deleted_at" IS NULL AND ((id = $1))` +// mock.ExpectQuery(regexp.QuoteMeta(sqlSelectOne)). +// WithArgs(loginDTO.ID). +// WillReturnRows(rows) + +// // Define expected query +// const sqlDeleteOne = `DELETE FROM "user-test"."logins" WHERE "user-test"."logins"."deleted_at" IS NULL AND ((id = $1))` +// mock.ExpectBegin() // start transaction +// mock.ExpectQuery(regexp.QuoteMeta(sqlDeleteOne)). +// WithArgs(loginDTO.ID). +// WillReturnRows(rows) +// mock.ExpectCommit() // commit transaction + +// // Make request +// r.ServeHTTP(w, httptest.NewRequest("DELETE", "/api/logins/1", nil)) + +// // Check status code +// assert.Equal(t, http.StatusOK, w.Code, "Did not get expected HTTP status code, got") + +// fmt.Println(w.Body.String()) +// } + +/* func TestCreateLogin(t *testing.T) { + w := httptest.NewRecorder() + + // Create mock db + mockDB, mock := dbSetup() + mockDB.LogMode(true) + + // Initialize router + r := routersSetup(mockDB) + + // Generate dummy login + loginDTO := &model.LoginDTO{ + ID: 1, + Title: "Dummy Title", + URL: "http://dummy.com", + Username: "dummyuser", + Password: "dummypassword", + } + + const sqlInsert = `INSERT INTO "user-test"."logins" ("created_at","updated_at","deleted_at","title","url","username","password") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "user-test"."logins"."id"` + + mock.ExpectBegin() // start transaction + mock.ExpectQuery(regexp.QuoteMeta(sqlInsert)). + WithArgs(AnyTime{}, AnyTime{}, nil, loginDTO.Title, loginDTO.URL, loginDTO.Username, loginDTO.Password). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(loginDTO.ID)) + mock.ExpectCommit() // commit transaction + + // Make request + data, _ := json.Marshal(loginDTO) + req, _ := http.NewRequest("POST", "/api/logins", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + r.ServeHTTP(w, req) +} */ + +type AnyTime struct{} + +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} diff --git a/internal/router/router.go b/internal/router/router.go index fa65b9b..36f8826 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -36,6 +36,7 @@ func (r *Router) initRoutes() { apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter() // Login endpoints + apiRouter.HandleFunc("/login-test", api.TestLogin(r.store)).Methods(http.MethodGet) apiRouter.HandleFunc("/logins", api.FindAllLogins(r.store)).Methods(http.MethodGet) apiRouter.HandleFunc("/logins", api.CreateLogin(r.store)).Methods(http.MethodPost) apiRouter.HandleFunc("/logins/{id:[0-9]+}", api.FindLoginsByID(r.store)).Methods(http.MethodGet) diff --git a/internal/storage/database.go b/internal/storage/database.go index 235d69b..0fd4b37 100644 --- a/internal/storage/database.go +++ b/internal/storage/database.go @@ -29,8 +29,7 @@ type Database struct { servers ServerRepository } -// New opens a database according to configuration. -func New(cfg *config.DatabaseConfiguration) (*Database, error) { +func DBConn(cfg *config.DatabaseConfiguration) (*gorm.DB, error) { var db *gorm.DB var err error @@ -41,6 +40,11 @@ func New(cfg *config.DatabaseConfiguration) (*Database, error) { db.LogMode(cfg.LogMode) + return db, err +} + +// New opens a database according to configuration. +func New(db *gorm.DB) *Database { return &Database{ db: db, logins: login.NewRepository(db), @@ -51,7 +55,7 @@ func New(cfg *config.DatabaseConfiguration) (*Database, error) { tokens: token.NewRepository(db), users: user.NewRepository(db), servers: server.NewRepository(db), - }, nil + } } // Create inserts the value into database. diff --git a/internal/storage/login/login_repository_test.go b/internal/storage/login/login_repository_test.go new file mode 100644 index 0000000..5841ac6 --- /dev/null +++ b/internal/storage/login/login_repository_test.go @@ -0,0 +1,76 @@ +package login + +import ( + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-test/deep" + "github.com/jinzhu/gorm" + "github.com/pass-wall/passwall-server/model" + "github.com/stretchr/testify/assert" +) + +func dbSetup() (*gorm.DB, sqlmock.Sqlmock) { + db, mock, _ := sqlmock.New() + DB, _ := gorm.Open("postgres", db) + + DB.LogMode(false) + + return DB, mock +} + +func TestAll(t *testing.T) { + + // Create mock db + mockDB, mock := dbSetup() + + // Initialize repository + loginRepository := NewRepository(mockDB) + + const sqlSelectAll = `SELECT * FROM "user-test"."logins" WHERE "user-test"."logins"."deleted_at" IS NULL` + mock.ExpectQuery(regexp.QuoteMeta(sqlSelectAll)). + WillReturnRows(sqlmock.NewRows(nil)) + + expected := []model.Login{} + loginList, err := loginRepository.All("user-test") + assert.Nil(t, err) + + assert.Nil(t, deep.Equal(expected, loginList)) +} + +func TestFindByID(t *testing.T) { + + // Create mock db + mockDB, mock := dbSetup() + + // Initialize repository + loginRepository := NewRepository(mockDB) + + login := &model.Login{ + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeletedAt: nil, + Title: "Dummy Title", + URL: "http://dummy.com", + Username: "dummyuser", + Password: "dummypassword", + } + + rows := sqlmock. + NewRows([]string{"id", "created_at", "updated_at", "deleted_at", "title", "url", "username", "password"}). + AddRow(login.ID, login.CreatedAt, login.UpdatedAt, login.DeletedAt, login.Title, login.URL, login.Username, login.Password) + + const sqlSelectOne = `SELECT * FROM "user-test"."logins" WHERE "user-test"."logins"."deleted_at" IS NULL AND ((id = $1))` + + mock.ExpectQuery(regexp.QuoteMeta(sqlSelectOne)). + WithArgs(login.ID). + WillReturnRows(rows) + + resultLogin, err := loginRepository.FindByID(login.ID, "user-test") + assert.Nil(t, err) + + assert.Nil(t, deep.Equal(login, resultLogin)) +}