diff --git a/.gitignore b/.gitignore index 7947ac1..90a730f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ bin/* backend/config.json backend/embed/assets backend/.task +backend/coverage.html test/node_modules */node_modules docs/.vuepress/dist diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go index 8e5802f..2f0567b 100644 --- a/backend/internal/api/middleware/auth.go +++ b/backend/internal/api/middleware/auth.go @@ -49,7 +49,7 @@ func Enforce(permission string) func(http.Handler) http.Handler { } userID := uint(claims["uid"].(float64)) - _, enabled := user.IsEnabled(userID) + _, enabled, _ := user.IsEnabled(userID) if token == nil || !enabled { h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) return diff --git a/backend/internal/api/middleware/sse_auth.go b/backend/internal/api/middleware/sse_auth.go index 81721dc..e82dd2f 100644 --- a/backend/internal/api/middleware/sse_auth.go +++ b/backend/internal/api/middleware/sse_auth.go @@ -32,7 +32,7 @@ func SSEAuth(next http.Handler) http.Handler { } userID := uint(claims["uid"].(float64)) - _, enabled := user.IsEnabled(userID) + _, enabled, _ := user.IsEnabled(userID) if token == nil || !enabled { h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) return diff --git a/backend/internal/entity/accesslist/methods.go b/backend/internal/entity/accesslist/methods.go index 9485d51..b0c9ad8 100644 --- a/backend/internal/entity/accesslist/methods.go +++ b/backend/internal/entity/accesslist/methods.go @@ -30,8 +30,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/certificate/methods.go b/backend/internal/entity/certificate/methods.go index 28b0ca4..bf76976 100644 --- a/backend/internal/entity/certificate/methods.go +++ b/backend/internal/entity/certificate/methods.go @@ -46,8 +46,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ent } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/certificateauthority/methods.go b/backend/internal/entity/certificateauthority/methods.go index c77c370..d97f12f 100644 --- a/backend/internal/entity/certificateauthority/methods.go +++ b/backend/internal/entity/certificateauthority/methods.go @@ -30,8 +30,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/dnsprovider/methods.go b/backend/internal/entity/dnsprovider/methods.go index 036e498..450f15e 100644 --- a/backend/internal/entity/dnsprovider/methods.go +++ b/backend/internal/entity/dnsprovider/methods.go @@ -30,8 +30,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/host/methods.go b/backend/internal/entity/host/methods.go index 5220923..b73f6f0 100644 --- a/backend/internal/entity/host/methods.go +++ b/backend/internal/entity/host/methods.go @@ -32,8 +32,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ent } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/lists.go b/backend/internal/entity/lists.go index 8507913..0ce5fae 100644 --- a/backend/internal/entity/lists.go +++ b/backend/internal/entity/lists.go @@ -7,11 +7,6 @@ import ( "gorm.io/gorm" ) -// ListableModel is a interface for common use -type ListableModel interface { - TableName() string -} - // ListResponse is the JSON response for users list type ListResponse struct { Total int64 `json:"total"` @@ -29,7 +24,6 @@ func ListQueryBuilder( filterMap map[string]model.FilterMapValue, ) *gorm.DB { scopes := make([]func(*gorm.DB) *gorm.DB, 0) - scopes = append(scopes, ScopeOffsetLimit(pageInfo)) scopes = append(scopes, ScopeFilters(filters, filterMap)) return database.GetDB().Scopes(scopes...) } @@ -38,8 +32,16 @@ func ListQueryBuilder( // Postgres in particular doesn't like count(*) when ordering at the same time func AddOrderToList( dbo *gorm.DB, - pageInfo *model.PageInfo, + sort []model.Sort, defaultSort model.Sort, ) *gorm.DB { - return dbo.Scopes(ScopeOrderBy(pageInfo, defaultSort)) + return dbo.Scopes(ScopeOrderBy(sort, defaultSort)) +} + +// AddOffsetLimitToList is used after query above is used for pagination +func AddOffsetLimitToList( + dbo *gorm.DB, + pageInfo *model.PageInfo, +) *gorm.DB { + return dbo.Scopes(ScopeOffsetLimit(pageInfo)) } diff --git a/backend/internal/entity/nginxtemplate/methods.go b/backend/internal/entity/nginxtemplate/methods.go index aaa320b..67644ae 100644 --- a/backend/internal/entity/nginxtemplate/methods.go +++ b/backend/internal/entity/nginxtemplate/methods.go @@ -30,8 +30,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/scopes.go b/backend/internal/entity/scopes.go index 6bdcc07..7406eb4 100644 --- a/backend/internal/entity/scopes.go +++ b/backend/internal/entity/scopes.go @@ -19,11 +19,11 @@ func ScopeOffsetLimit(pageInfo *model.PageInfo) func(db *gorm.DB) *gorm.DB { } } -func ScopeOrderBy(pageInfo *model.PageInfo, defaultSort model.Sort) func(db *gorm.DB) *gorm.DB { +func ScopeOrderBy(sort []model.Sort, defaultSort model.Sort) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { - if pageInfo.Sort != nil { + if sort != nil { // Sort by items in slice - return db.Order(sortToOrderString(pageInfo.Sort)) + return db.Order(sortToOrderString(sort)) } else if defaultSort.Field != "" { // Default to this sort str := defaultSort.Field diff --git a/backend/internal/entity/setting/methods.go b/backend/internal/entity/setting/methods.go index 3aa971f..4fa03a0 100644 --- a/backend/internal/entity/setting/methods.go +++ b/backend/internal/entity/setting/methods.go @@ -37,8 +37,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/stream/methods.go b/backend/internal/entity/stream/methods.go index bbd38e2..d23cc8e 100644 --- a/backend/internal/entity/stream/methods.go +++ b/backend/internal/entity/stream/methods.go @@ -30,8 +30,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/upstream/methods.go b/backend/internal/entity/upstream/methods.go index 2c09065..45f3887 100644 --- a/backend/internal/entity/upstream/methods.go +++ b/backend/internal/entity/upstream/methods.go @@ -30,8 +30,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ent } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/upstreamserver/methods.go b/backend/internal/entity/upstreamserver/methods.go index e14b848..2aa2074 100644 --- a/backend/internal/entity/upstreamserver/methods.go +++ b/backend/internal/entity/upstreamserver/methods.go @@ -39,8 +39,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/user/entity_test.go b/backend/internal/entity/user/entity_test.go new file mode 100644 index 0000000..82dead7 --- /dev/null +++ b/backend/internal/entity/user/entity_test.go @@ -0,0 +1,421 @@ +package user + +import ( + goerrors "errors" + "regexp" + "testing" + + "npm/internal/errors" + "npm/internal/model" + "npm/internal/test" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + singleRow *sqlmock.Rows + capabilitiesRows *sqlmock.Rows + listCountRows *sqlmock.Rows + listRows *sqlmock.Rows +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.singleRow = sqlmock.NewRows([]string{ + "id", + "name", + "nickname", + "email", + "is_disabled", + "is_system", + }).AddRow( + 10, + "John Doe", + "Jonny", + "jon@example.com", + false, + false, + ) + + s.capabilitiesRows = sqlmock.NewRows([]string{ + "user_id", + "capability_name", + }).AddRow( + 10, + "hosts.view", + ).AddRow( + 10, + "hosts.manage", + ) + + s.listCountRows = sqlmock.NewRows([]string{ + "count(*)", + }).AddRow( + 2, + ) + + s.listRows = sqlmock.NewRows([]string{ + "id", + "name", + "nickname", + "email", + "is_disabled", + "is_system", + }).AddRow( + 10, + "John Doe", + "Jonny", + "jon@example.com", + false, + false, + ).AddRow( + 11, + "Jane Doe", + "Jane", + "jane@example.com", + true, + false, + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +func assertModel(t *testing.T, m Model) { + assert.Equal(t, uint(10), m.ID) + assert.Equal(t, "John Doe", m.Name) + assert.Equal(t, "Jonny", m.Nickname) + assert.Equal(t, "jon@example.com", m.Email) + assert.Equal(t, false, m.IsDisabled) + assert.Equal(t, false, m.IsSystem) +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestGetByID() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT 1`)). + WithArgs(10, 0). + WillReturnRows(s.singleRow) + + m, err := GetByID(10) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestLoadByEmail() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE email = $1 AND is_system = $2 AND "user"."is_deleted" = $3 ORDER BY "user"."id" LIMIT 1`)). + WithArgs("jon@example.com", false, 0). + WillReturnRows(s.singleRow) + + m, err := GetByEmail("jon@example.com") + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestIsEnabled() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT 1`)). + WithArgs(10, 0). + WillReturnRows(s.singleRow) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT 1`)). + WithArgs(999, 0). + WillReturnError(goerrors.New("record not found")) + + // user that exists + exists, enabled, err := IsEnabled(10) + require.NoError(s.T(), err) + assert.Equal(s.T(), true, exists) + assert.Equal(s.T(), true, enabled) + // that that doesn't exist + exists, enabled, err = IsEnabled(999) + assert.Equal(s.T(), "record not found", err.Error()) + assert.Equal(s.T(), false, exists) + assert.Equal(s.T(), false, enabled) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSave() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE email = $1 AND is_system = $2 AND "user"."is_deleted" = $3 ORDER BY "user"."id" LIMIT 1`)). + WithArgs("jon@example.com", false, 0). + WillReturnRows(s.singleRow) + + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","nickname","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + "John Doe", + "Jonny", + "sarah@example.com", + false, + false, + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) + s.mock.ExpectCommit() + + // New model, as system + m := Model{ + Name: "John Doe", + Nickname: "Jonny", + Email: "JON@example.com", // mixed case on purpose + IsSystem: true, + } + err := m.Save() + assert.Equal(s.T(), errors.ErrSystemUserReadonly.Error(), err.Error()) + + // Remove system and try again. Expect error due to duplicate email + m.IsSystem = false + err = m.Save() + assert.Equal(s.T(), errors.ErrDuplicateEmailUser.Error(), err.Error()) + + // Change email and try again. Expect success + m.Email = "sarah@example.com" + err = m.Save() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestDelete() { + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`UPDATE "user" SET "is_deleted"=$1 WHERE "user"."id" = $2 AND "user"."is_deleted" = $3`)). + WithArgs(1, 10, 0). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + m := Model{} + err := m.Delete() + assert.Equal(s.T(), "Unable to delete a new object", err.Error()) + + m2 := Model{ + ModelBase: model.ModelBase{ + ID: 10, + }, + Name: "John Doe", + } + err2 := m2.Delete() + require.NoError(s.T(), err2) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGenerateGravatar() { + m := Model{Email: "jon@example.com"} + m.generateGravatar() + assert.Equal(s.T(), "https://www.gravatar.com/avatar/dc36565cc2376197358fa27ed4c47253?d=mm&r=pg&s=128", m.GravatarURL) +} + +func (s *testsuite) TestDeleteAll() { + s.mock. + ExpectExec(regexp.QuoteMeta("DELETE FROM `user` WHERE is_system = $1")). + WithArgs(false). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := DeleteAll() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGetCapabilities() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnRows(s.capabilitiesRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(999). + WillReturnRows(sqlmock.NewRows([]string{})) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(1000). + WillReturnError(goerrors.New("some other error")) + + // user that exists + caps, err := GetCapabilities(10) + require.NoError(s.T(), err) + assert.Equal(s.T(), 2, len(caps)) + // user that doesn't exist + caps, err = GetCapabilities(999) + require.NoError(s.T(), err) + assert.Equal(s.T(), 0, len(caps)) + // some other error + caps, err = GetCapabilities(1000) + assert.Equal(s.T(), "some other error", err.Error()) + assert.Equal(s.T(), 0, len(caps)) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestList() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "user" WHERE name LIKE $1 AND "user"."is_deleted" = $2`)). + WithArgs("%jon%", 0). + WillReturnRows(s.listCountRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE name LIKE $1 AND "user"."is_deleted" = $2 ORDER BY name asc LIMIT 8`)). + WithArgs("%jon%", 0). + WillReturnRows(s.listRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnRows(s.capabilitiesRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(11). + WillReturnRows(sqlmock.NewRows([]string{})) + + p := model.PageInfo{ + Offset: 0, + Limit: 8, + Sort: []model.Sort{ + { + Field: "name", + Direction: "asc", + }, + }, + } + + f := []model.Filter{ + { + Field: "name", + Modifier: "contains", + Value: []string{"jon"}, + }, + } + + e := []string{"capabilities"} + + resp, err := List(p, f, e) + require.NoError(s.T(), err) + assert.Equal(s.T(), int64(2), resp.Total) + assert.Equal(s.T(), p.Offset, resp.Offset) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Sort, resp.Sort) + assert.Equal(s.T(), f, resp.Filter) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSetPermissions() { + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`DELETE FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`INSERT INTO "user_has_capability" ("user_id","capability_name") VALUES ($1,$2),($3,$4)`)). + WithArgs(10, "hosts.view", 10, "hosts.manage"). + WillReturnResult(sqlmock.NewResult(88, 0)) + s.mock.ExpectCommit() + + // Empty model returns error + m := Model{} + err := m.SetPermissions([]string{"hosts.view", "hosts.manage"}) + assert.Equal(s.T(), "Cannot set permissions without first saving the User", err.Error()) + + // Defined user + m.ID = 10 + err = m.SetPermissions([]string{"hosts.view", "hosts.manage"}) + require.NoError(s.T(), err) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSaveCapabilities() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "capability"`)). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow("full-admin"). + AddRow("hosts.view"). + AddRow("hosts.manage")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`DELETE FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`INSERT INTO "user_has_capability" ("user_id","capability_name") VALUES ($1,$2),($3,$4)`)). + WithArgs(10, "hosts.view", 10, "hosts.manage"). + WillReturnResult(sqlmock.NewResult(88, 0)) + s.mock.ExpectCommit() + + // Empty model returns error + m := Model{} + err := m.SaveCapabilities() + assert.Equal(s.T(), "Cannot save capabilities on unsaved user", err.Error()) + + // Empty model returns error + m.ID = 10 + err = m.SaveCapabilities() + assert.Equal(s.T(), "At least 1 capability required for a user", err.Error()) + + // With some caps + m.Capabilities = []string{"hosts.view", "hosts.manage"} + err = m.SaveCapabilities() + require.NoError(s.T(), err) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSaveCapabilitiesInvalid() { + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "capability"`)). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow("full-admin"). + AddRow("hosts.view"). + AddRow("hosts.manage")) + + // Empty model returns error + m := Model{ + ModelBase: model.ModelBase{ + ID: 10, + }, + Capabilities: []string{"doesnotexist", "hosts.manage"}, + } + err := m.SaveCapabilities() + assert.Equal(s.T(), "Capability `doesnotexist` is not valid", err.Error()) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} diff --git a/backend/internal/entity/user/methods.go b/backend/internal/entity/user/methods.go index 6ae1435..d37b923 100644 --- a/backend/internal/entity/user/methods.go +++ b/backend/internal/entity/user/methods.go @@ -23,14 +23,14 @@ func GetByEmail(email string) (Model, error) { } // IsEnabled is used by middleware to ensure the user is still enabled -// returns (userExist, isEnabled) -func IsEnabled(userID uint) (bool, bool) { +// returns (userExist, isEnabled, error) +func IsEnabled(userID uint) (bool, bool, error) { var user Model db := database.GetDB() if result := db.First(&user, userID); result.Error != nil { - return false, false + return false, false, result.Error } - return true, !user.IsDisabled + return true, !user.IsDisabled, nil } // List will return a list of users @@ -51,8 +51,10 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ent } // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) items := make([]Model, 0) - if res := entity.AddOrderToList(dbo, &pageInfo, defaultSort).Find(&items); res.Error != nil { + if res := dbo.Find(&items); res.Error != nil { return result, res.Error } diff --git a/backend/internal/entity/user/model_test.go b/backend/internal/entity/user/model_test.go deleted file mode 100644 index 045e1ab..0000000 --- a/backend/internal/entity/user/model_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package user - -import ( - "regexp" - "testing" - - "npm/internal/errors" - "npm/internal/model" - "npm/internal/test" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -// +------------+ -// | Setup | -// +------------+ - -type testsuite struct { - suite.Suite - mock sqlmock.Sqlmock - singleRow *sqlmock.Rows -} - -// SetupTest is executed before each test -func (s *testsuite) SetupTest() { - var err error - s.mock, err = test.Setup() - require.NoError(s.T(), err) - - s.singleRow = sqlmock.NewRows([]string{ - "id", - "name", - "nickname", - "email", - "is_disabled", - "is_system", - }).AddRow( - 10, - "John Doe", - "Jonny", - "jon@example.com", - false, - false, - ) -} - -// In order for 'go test' to run this suite, we need to create -// a normal test function and pass our suite to suite.Run -func TestExampleTestSuite(t *testing.T) { - suite.Run(t, new(testsuite)) -} - -// +------------+ -// | Tests | -// +------------+ - -func (s *testsuite) TestLoadByID() { - s.mock. - ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT 1`)). - WithArgs(10, 0). - WillReturnRows(s.singleRow) - - m := Model{} - err := m.LoadByID(10) - require.NoError(s.T(), err) - require.NoError(s.T(), s.mock.ExpectationsWereMet()) -} - -func (s *testsuite) TestLoadByEmail() { - s.mock. - ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE email = $1 AND is_system = $2 AND "user"."is_deleted" = $3 ORDER BY "user"."id" LIMIT 1`)). - WithArgs("jon@example.com", false, 0). - WillReturnRows(s.singleRow) - - m := Model{} - err := m.LoadByEmail("jon@example.com") - require.NoError(s.T(), err) - require.NoError(s.T(), s.mock.ExpectationsWereMet()) -} - -func (s *testsuite) TestSave() { - s.mock. - ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE email = $1 AND is_system = $2 AND "user"."is_deleted" = $3 ORDER BY "user"."id" LIMIT 1`)). - WithArgs("jon@example.com", false, 0). - WillReturnRows(s.singleRow) - - s.mock.ExpectBegin() - s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","nickname","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`)). - WithArgs( - sqlmock.AnyArg(), - sqlmock.AnyArg(), - 0, - "John Doe", - "Jonny", - "sarah@example.com", - false, - false, - ). - WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) - s.mock.ExpectCommit() - - // New model, as system - m := Model{ - Name: "John Doe", - Nickname: "Jonny", - Email: "JON@example.com", // mixed case on purpose - IsSystem: true, - } - err := m.Save() - assert.Equal(s.T(), errors.ErrSystemUserReadonly.Error(), err.Error()) - - // Remove system and try again. Expect error due to duplicate email - m.IsSystem = false - err = m.Save() - assert.Equal(s.T(), errors.ErrDuplicateEmailUser.Error(), err.Error()) - - // Change email and try again. Expect success - m.Email = "sarah@example.com" - err = m.Save() - require.NoError(s.T(), err) - require.NoError(s.T(), s.mock.ExpectationsWereMet()) -} - -func (s *testsuite) TestDelete() { - s.mock.ExpectBegin() - s.mock. - ExpectExec(regexp.QuoteMeta(`UPDATE "user" SET "is_deleted"=$1 WHERE "user"."id" = $2 AND "user"."is_deleted" = $3`)). - WithArgs(1, 10, 0). - WillReturnResult(sqlmock.NewResult(0, 1)) - s.mock.ExpectCommit() - - m := Model{} - err := m.Delete() - assert.Equal(s.T(), "Unable to delete a new object", err.Error()) - - m2 := Model{ - ModelBase: model.ModelBase{ - ID: 10, - }, - Name: "John Doe", - } - err2 := m2.Delete() - require.NoError(s.T(), err2) - require.NoError(s.T(), s.mock.ExpectationsWereMet()) -} - -func (s *testsuite) TestGenerateGravatar() { - m := Model{Email: "jon@example.com"} - m.generateGravatar() - assert.Equal(s.T(), "https://www.gravatar.com/avatar/dc36565cc2376197358fa27ed4c47253?d=mm&r=pg&s=128", m.GravatarURL) -} diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh index dcfb5c5..5b7ea8c 100755 --- a/backend/scripts/test.sh +++ b/backend/scripts/test.sh @@ -1,3 +1,7 @@ #!/bin/bash -e -go test -json -cover ./internal/... | tparse +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +go test -json -cover -coverprofile="$DIR/../coverage.out" ./internal/... | tparse +go tool cover -html="$DIR/../coverage.out" -o "$DIR/../coverage.html" +rm -f "$DIR/../coverage.out"