tests(db): add additional tests & comments
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Evan Reichard 2024-06-16 20:00:41 -04:00
parent 9809a09d2e
commit 3a633235ea
5 changed files with 383 additions and 134 deletions

View File

@ -869,10 +869,10 @@ func (api *API) updateUser(user string, rawPassword *string, isAdmin *bool) erro
updateParams.Admin = user.Admin
}
// Check Admins
// Check Admins - Disallow Demotion
if isLast, err := api.isLastAdmin(user); err != nil {
return err
} else if isLast {
} else if isLast && !updateParams.Admin {
return fmt.Errorf("unable to demote %s - last admin", user)
}

View File

@ -13,6 +13,7 @@ import (
"reichard.io/antholume/metadata"
)
// getTimeZones returns a string slice of IANA timezones.
func getTimeZones() []string {
return []string{
"Africa/Cairo",
@ -52,6 +53,8 @@ func getTimeZones() []string {
}
}
// niceSeconds takes in an int (in seconds) and returns a string readable
// representation. For example 1928371 -> "22d 7h 39m 31s".
func niceSeconds(input int64) (result string) {
if input == 0 {
return "N/A"
@ -80,6 +83,8 @@ func niceSeconds(input int64) (result string) {
return
}
// niceNumbers takes in an int and returns a string representation. For example
// 19823 -> "19.8k".
func niceNumbers(input int64) string {
if input == 0 {
return "0"
@ -98,7 +103,8 @@ func niceNumbers(input int64) string {
}
}
// Convert Database Array -> Int64 Array
// getSVGGraphData builds SVGGraphData from the provided stats, width and height.
// It is used exclusively in templates to generate the daily read stats graph.
func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
var intData []int64
for _, item := range inputData {
@ -108,6 +114,8 @@ func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, sv
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
}
// dict returns a map[string]any dict. Each pair of two is a key & value
// respectively. It's primarily utilized in templates.
func dict(values ...any) (map[string]any, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
@ -123,6 +131,8 @@ func dict(values ...any) (map[string]any, error) {
return dict, nil
}
// fields returns a map[string]any of the provided struct. It's primarily
// utilized in templates.
func fields(value any) (map[string]any, error) {
v := reflect.Indirect(reflect.ValueOf(value))
if v.Kind() != reflect.Struct {
@ -137,10 +147,13 @@ func fields(value any) (map[string]any, error) {
return m, nil
}
// slice returns a slice of the provided arguments. It's primarily utilized in
// templates.
func slice(elements ...any) []any {
return elements
}
// deriveBaseFileName builds the base filename for a given MetadataInfo object.
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
// Derive New FileName
var newFileName string
@ -160,6 +173,7 @@ func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
}
// importStatusPriority returns the order priority for import status in the UI.
func importStatusPriority(status importStatus) int {
switch status {
case importFailed:

View File

@ -43,7 +43,7 @@ func init() {
})
}
// Returns an initialized manager
// NewMgr Returns an initialized manager
func NewMgr(c *config.Config) *DBManager {
// Create Manager
dbm := &DBManager{
@ -58,7 +58,7 @@ func NewMgr(c *config.Config) *DBManager {
return dbm
}
// Init manager
// init loads the DB manager
func (dbm *DBManager) init() error {
// Build DB Location
var dbLocation string
@ -125,7 +125,7 @@ func (dbm *DBManager) init() error {
return nil
}
// Reload manager (close DB & reinit)
// Reload closes the DB & reinits
func (dbm *DBManager) Reload() error {
// Close handle
err := dbm.DB.Close()
@ -141,6 +141,7 @@ func (dbm *DBManager) Reload() error {
return nil
}
// CacheTempTables clears existing statistics and recalculates
func (dbm *DBManager) CacheTempTables() error {
start := time.Now()
user_streaks_sql := `
@ -165,6 +166,8 @@ func (dbm *DBManager) CacheTempTables() error {
return nil
}
// updateSettings ensures that we're enforcing foreign keys and enable journal
// mode.
func (dbm *DBManager) updateSettings() error {
// Set SQLite PRAGMA Settings
pragmaQuery := `
@ -188,6 +191,7 @@ func (dbm *DBManager) updateSettings() error {
return nil
}
// performMigrations runs all migrations
func (dbm *DBManager) performMigrations(isNew bool) error {
// Create context
ctx := context.WithValue(context.Background(), "isNew", isNew) // nolint
@ -204,7 +208,7 @@ func (dbm *DBManager) performMigrations(isNew bool) error {
return goose.UpContext(ctx, dbm.DB, "migrations")
}
// Determines whether the database is empty
// isEmpty determines whether the database is empty
func isEmpty(db *sql.DB) (bool, error) {
var tableCount int
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount)
@ -214,7 +218,7 @@ func isEmpty(db *sql.DB) (bool, error) {
return tableCount == 0, nil
}
// LOCAL_TIME custom SQL function
// localTime is a custom SQL function that is registered as LOCAL_TIME in the init function
func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
timeStr, ok := args[0].(string)
if !ok {

View File

@ -5,98 +5,71 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"reichard.io/antholume/config"
"reichard.io/antholume/utils"
)
type databaseTest struct {
*testing.T
var (
userID string = "testUser"
userPass string = "testPass"
deviceID string = "testDevice"
deviceName string = "testDeviceName"
documentID string = "testDocument"
documentTitle string = "testTitle"
documentAuthor string = "testAuthor"
documentWords int64 = 5000
)
type DatabaseTestSuite struct {
suite.Suite
dbm *DBManager
}
var userID string = "testUser"
var userPass string = "testPass"
var deviceID string = "testDevice"
var deviceName string = "testDeviceName"
var documentID string = "testDocument"
var documentTitle string = "testTitle"
var documentAuthor string = "testAuthor"
func TestDatabase(t *testing.T) {
suite.Run(t, new(DatabaseTestSuite))
}
func TestNewMgr(t *testing.T) {
// PROGRESS - TODO:
// - 󰊕 (q *Queries) GetProgress
// - 󰊕 (q *Queries) UpdateProgress
func (suite *DatabaseTestSuite) SetupTest() {
cfg := config.Config{
DBType: "memory",
}
dbm := NewMgr(&cfg)
assert.NotNil(t, dbm, "should not have nil dbm")
t.Run("Database", func(t *testing.T) {
dt := databaseTest{t, dbm}
dt.TestUser()
dt.TestDocument()
dt.TestDevice()
dt.TestActivity()
dt.TestDailyReadStats()
})
}
func (dt *databaseTest) TestUser() {
dt.Run("User", func(t *testing.T) {
// Generate Auth Hash
rawAuthHash, err := utils.GenerateToken(64)
assert.Nil(t, err, "should have nil err")
suite.dbm = NewMgr(&cfg)
// Create User
rawAuthHash, _ := utils.GenerateToken(64)
authHash := fmt.Sprintf("%x", rawAuthHash)
changed, err := dt.dbm.Queries.CreateUser(dt.dbm.Ctx, CreateUserParams{
_, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
ID: userID,
Pass: &userPass,
AuthHash: &authHash,
})
suite.NoError(err)
assert.Nil(t, err, "should have nil err")
assert.Equal(t, int64(1), changed)
user, err := dt.dbm.Queries.GetUser(dt.dbm.Ctx, userID)
assert.Nil(t, err, "should have nil err")
assert.Equal(t, userPass, *user.Pass)
})
}
func (dt *databaseTest) TestDocument() {
dt.Run("Document", func(t *testing.T) {
doc, err := dt.dbm.Queries.UpsertDocument(dt.dbm.Ctx, UpsertDocumentParams{
// Create Document
_, err = suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
ID: documentID,
Title: &documentTitle,
Author: &documentAuthor,
Words: &documentWords,
})
suite.NoError(err)
assert.Nil(t, err, "should have nil err")
assert.Equal(t, documentID, doc.ID, "should have document id")
assert.Equal(t, documentTitle, *doc.Title, "should have document title")
assert.Equal(t, documentAuthor, *doc.Author, "should have document author")
})
}
func (dt *databaseTest) TestDevice() {
dt.Run("Device", func(t *testing.T) {
device, err := dt.dbm.Queries.UpsertDevice(dt.dbm.Ctx, UpsertDeviceParams{
// Create Device
_, err = suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
ID: deviceID,
UserID: userID,
DeviceName: deviceName,
})
suite.NoError(err)
assert.Nil(t, err, "should have nil err")
assert.Equal(t, deviceID, device.ID, "should have device id")
assert.Equal(t, userID, device.UserID, "should have user id")
assert.Equal(t, deviceName, device.DeviceName, "should have device name")
})
}
func (dt *databaseTest) TestActivity() {
dt.Run("Progress", func(t *testing.T) {
// 10 Activities, 10 Days
// Create Activity
end := time.Now()
start := end.AddDate(0, 0, -9)
var counter int64 = 0
@ -105,7 +78,7 @@ func (dt *databaseTest) TestActivity() {
counter += 1
// Add Item
activity, err := dt.dbm.Queries.AddActivity(dt.dbm.Ctx, AddActivityParams{
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
DocumentID: documentID,
DeviceID: deviceID,
UserID: userID,
@ -115,25 +88,77 @@ func (dt *databaseTest) TestActivity() {
EndPercentage: float64(counter+1) / 100.0,
})
assert.Nil(t, err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
assert.Equal(t, counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
}
// Initiate Cache
dt.dbm.CacheTempTables()
err = suite.dbm.CacheTempTables()
suite.NoError(err)
}
// DOCUMENT - TODO:
// - 󰊕 (q *Queries) DeleteDocument
// - 󰊕 (q *Queries) GetDeletedDocuments
// - 󰊕 (q *Queries) GetDocument
// - 󰊕 (q *Queries) GetDocumentProgress
// - 󰊕 (q *Queries) GetDocumentWithStats
// - 󰊕 (q *Queries) GetDocuments
// - 󰊕 (q *Queries) GetDocumentsSize
// - 󰊕 (q *Queries) GetDocumentsWithStats
// - 󰊕 (q *Queries) GetMissingDocuments
// - 󰊕 (q *Queries) GetWantedDocuments
// - 󰊕 (q *Queries) UpsertDocument
func (suite *DatabaseTestSuite) TestDocument() {
testDocID := "docid1"
doc, err := suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
ID: testDocID,
Title: &documentTitle,
Author: &documentAuthor,
})
suite.Nil(err, "should have nil err")
suite.Equal(testDocID, doc.ID, "should have document id")
suite.Equal(documentTitle, *doc.Title, "should have document title")
suite.Equal(documentAuthor, *doc.Author, "should have document author")
}
// DEVICES - TODO:
// - 󰊕 (q *Queries) GetDevice
// - 󰊕 (q *Queries) GetDevices
// - 󰊕 (q *Queries) UpsertDevice
func (suite *DatabaseTestSuite) TestDevice() {
testDevice := "dev123"
device, err := suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
ID: testDevice,
UserID: userID,
DeviceName: deviceName,
})
suite.Nil(err, "should have nil err")
suite.Equal(testDevice, device.ID, "should have device id")
suite.Equal(userID, device.UserID, "should have user id")
suite.Equal(deviceName, device.DeviceName, "should have device name")
}
// ACTIVITY - TODO:
// - 󰊕 (q *Queries) AddActivity
// - 󰊕 (q *Queries) GetActivity
// - 󰊕 (q *Queries) GetLastActivity
func (suite *DatabaseTestSuite) TestActivity() {
// Validate Exists
existsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
existsRows, err := suite.dbm.Queries.GetActivity(suite.dbm.Ctx, GetActivityParams{
UserID: userID,
Offset: 0,
Limit: 50,
})
assert.Nil(t, err, "should have nil err for get activity")
assert.Len(t, existsRows, 10, "should have correct number of rows get activity")
suite.Nil(err, "should have nil err for get activity")
suite.Len(existsRows, 10, "should have correct number of rows get activity")
// Validate Doesn't Exist
doesntExistsRows, err := dt.dbm.Queries.GetActivity(dt.dbm.Ctx, GetActivityParams{
doesntExistsRows, err := suite.dbm.Queries.GetActivity(suite.dbm.Ctx, GetActivityParams{
UserID: userID,
DocumentID: "unknownDoc",
DocFilter: true,
@ -141,28 +166,30 @@ func (dt *databaseTest) TestActivity() {
Limit: 50,
})
assert.Nil(t, err, "should have nil err for get activity")
assert.Len(t, doesntExistsRows, 0, "should have no rows")
})
suite.Nil(err, "should have nil err for get activity")
suite.Len(doesntExistsRows, 0, "should have no rows")
}
func (dt *databaseTest) TestDailyReadStats() {
dt.Run("DailyReadStats", func(t *testing.T) {
readStats, err := dt.dbm.Queries.GetDailyReadStats(dt.dbm.Ctx, userID)
// MISC - TODO:
// - 󰊕 (q *Queries) AddMetadata
// - 󰊕 (q *Queries) GetDailyReadStats
// - 󰊕 (q *Queries) GetDatabaseInfo
// - 󰊕 (q *Queries) UpdateSettings
func (suite *DatabaseTestSuite) TestGetDailyReadStats() {
readStats, err := suite.dbm.Queries.GetDailyReadStats(suite.dbm.Ctx, userID)
assert.Nil(t, err, "should have nil err")
assert.Len(t, readStats, 30, "should have length of 30")
suite.Nil(err, "should have nil err")
suite.Len(readStats, 30, "should have length of 30")
// Validate 1 Minute / Day - Last 10 Days
for i := 0; i < 10; i++ {
stat := readStats[i]
assert.Equal(t, int64(1), stat.MinutesRead, "should have one minute read")
suite.Equal(int64(1), stat.MinutesRead, "should have one minute read")
}
// Validate 0 Minute / Day - Remaining 20 Days
for i := 10; i < 30; i++ {
stat := readStats[i]
assert.Equal(t, int64(0), stat.MinutesRead, "should have zero minutes read")
suite.Equal(int64(0), stat.MinutesRead, "should have zero minutes read")
}
})
}

204
database/users_test.go Normal file
View File

@ -0,0 +1,204 @@
package database
import (
"database/sql"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/suite"
"reichard.io/antholume/config"
"reichard.io/antholume/utils"
)
var (
testUserID string = "testUser"
testUserPass string = "testPass"
)
type UsersTestSuite struct {
suite.Suite
dbm *DBManager
}
func TestUsers(t *testing.T) {
suite.Run(t, new(UsersTestSuite))
}
func (suite *UsersTestSuite) SetupTest() {
cfg := config.Config{
DBType: "memory",
}
suite.dbm = NewMgr(&cfg)
// Create User
rawAuthHash, _ := utils.GenerateToken(64)
authHash := fmt.Sprintf("%x", rawAuthHash)
_, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
ID: testUserID,
Pass: &testUserPass,
AuthHash: &authHash,
})
suite.NoError(err)
// Create Document
_, err = suite.dbm.Queries.UpsertDocument(suite.dbm.Ctx, UpsertDocumentParams{
ID: documentID,
Title: &documentTitle,
Author: &documentAuthor,
Words: &documentWords,
})
suite.NoError(err)
// Create Device
_, err = suite.dbm.Queries.UpsertDevice(suite.dbm.Ctx, UpsertDeviceParams{
ID: deviceID,
UserID: testUserID,
DeviceName: deviceName,
})
suite.NoError(err)
}
func (suite *UsersTestSuite) TestGetUser() {
user, err := suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUserID)
suite.Nil(err, "should have nil err")
suite.Equal(testUserPass, *user.Pass)
}
func (suite *UsersTestSuite) TestCreateUser() {
testUser := "user1"
testPass := "pass1"
// Generate Auth Hash
rawAuthHash, err := utils.GenerateToken(64)
suite.Nil(err, "should have nil err")
authHash := fmt.Sprintf("%x", rawAuthHash)
changed, err := suite.dbm.Queries.CreateUser(suite.dbm.Ctx, CreateUserParams{
ID: testUser,
Pass: &testPass,
AuthHash: &authHash,
})
suite.Nil(err, "should have nil err")
suite.Equal(int64(1), changed)
user, err := suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUser)
suite.Nil(err, "should have nil err")
suite.Equal(testPass, *user.Pass)
}
func (suite *UsersTestSuite) TestDeleteUser() {
changed, err := suite.dbm.Queries.DeleteUser(suite.dbm.Ctx, testUserID)
suite.Nil(err, "should have nil err")
suite.Equal(int64(1), changed, "should have one changed row")
_, err = suite.dbm.Queries.GetUser(suite.dbm.Ctx, testUserID)
suite.ErrorIs(err, sql.ErrNoRows, "should have no rows error")
}
func (suite *UsersTestSuite) TestGetUsers() {
users, err := suite.dbm.Queries.GetUsers(suite.dbm.Ctx)
suite.Nil(err, "should have nil err")
suite.Len(users, 1, "should have single user")
}
func (suite *UsersTestSuite) TestUpdateUser() {
newPassword := "newPass123"
user, err := suite.dbm.Queries.UpdateUser(suite.dbm.Ctx, UpdateUserParams{
UserID: testUserID,
Password: &newPassword,
})
suite.Nil(err, "should have nil err")
suite.Equal(newPassword, *user.Pass, "should have new password")
}
func (suite *UsersTestSuite) TestGetUserStatistics() {
err := suite.dbm.CacheTempTables()
suite.NoError(err)
// Ensure Zero Items
userStats, err := suite.dbm.Queries.GetUserStatistics(suite.dbm.Ctx)
suite.Nil(err, "should have nil err")
suite.Empty(userStats, "should be empty")
// Create Activity
end := time.Now()
start := end.AddDate(0, 0, -9)
var counter int64 = 0
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
counter += 1
// Add Item
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
DocumentID: documentID,
DeviceID: deviceID,
UserID: testUserID,
StartTime: d.UTC().Format(time.RFC3339),
Duration: 60,
StartPercentage: float64(counter) / 100.0,
EndPercentage: float64(counter+1) / 100.0,
})
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
}
err = suite.dbm.CacheTempTables()
suite.NoError(err)
// Ensure One Item
userStats, err = suite.dbm.Queries.GetUserStatistics(suite.dbm.Ctx)
suite.Nil(err, "should have nil err")
suite.Len(userStats, 1, "should have length of one")
}
func (suite *UsersTestSuite) TestGetUsersStreaks() {
err := suite.dbm.CacheTempTables()
suite.NoError(err)
// Ensure Zero Items
userStats, err := suite.dbm.Queries.GetUserStreaks(suite.dbm.Ctx, testUserID)
suite.Nil(err, "should have nil err")
suite.Empty(userStats, "should be empty")
// Create Activity
end := time.Now()
start := end.AddDate(0, 0, -9)
var counter int64 = 0
for d := start; d.After(end) == false; d = d.AddDate(0, 0, 1) {
counter += 1
// Add Item
activity, err := suite.dbm.Queries.AddActivity(suite.dbm.Ctx, AddActivityParams{
DocumentID: documentID,
DeviceID: deviceID,
UserID: testUserID,
StartTime: d.UTC().Format(time.RFC3339),
Duration: 60,
StartPercentage: float64(counter) / 100.0,
EndPercentage: float64(counter+1) / 100.0,
})
suite.Nil(err, fmt.Sprintf("[%d] should have nil err for add activity", counter))
suite.Equal(counter, activity.ID, fmt.Sprintf("[%d] should have correct id for add activity", counter))
}
err = suite.dbm.CacheTempTables()
suite.NoError(err)
// Ensure Two Item
userStats, err = suite.dbm.Queries.GetUserStreaks(suite.dbm.Ctx, testUserID)
suite.Nil(err, "should have nil err")
suite.Len(userStats, 2, "should have length of two")
// Ensure Streak Stats
dayStats := userStats[0]
weekStats := userStats[1]
suite.Equal(int64(10), dayStats.CurrentStreak, "should be 10 days")
suite.Greater(weekStats.CurrentStreak, int64(1), "should be 2 or 3")
}