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 updateParams.Admin = user.Admin
} }
// Check Admins // Check Admins - Disallow Demotion
if isLast, err := api.isLastAdmin(user); err != nil { if isLast, err := api.isLastAdmin(user); err != nil {
return err return err
} else if isLast { } else if isLast && !updateParams.Admin {
return fmt.Errorf("unable to demote %s - last admin", user) return fmt.Errorf("unable to demote %s - last admin", user)
} }

View File

@ -13,6 +13,7 @@ import (
"reichard.io/antholume/metadata" "reichard.io/antholume/metadata"
) )
// getTimeZones returns a string slice of IANA timezones.
func getTimeZones() []string { func getTimeZones() []string {
return []string{ return []string{
"Africa/Cairo", "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) { func niceSeconds(input int64) (result string) {
if input == 0 { if input == 0 {
return "N/A" return "N/A"
@ -80,6 +83,8 @@ func niceSeconds(input int64) (result string) {
return return
} }
// niceNumbers takes in an int and returns a string representation. For example
// 19823 -> "19.8k".
func niceNumbers(input int64) string { func niceNumbers(input int64) string {
if input == 0 { if input == 0 {
return "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 { func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
var intData []int64 var intData []int64
for _, item := range inputData { for _, item := range inputData {
@ -108,6 +114,8 @@ func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, sv
return graph.GetSVGGraphData(intData, svgWidth, svgHeight) 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) { func dict(values ...any) (map[string]any, error) {
if len(values)%2 != 0 { if len(values)%2 != 0 {
return nil, errors.New("invalid dict call") return nil, errors.New("invalid dict call")
@ -123,6 +131,8 @@ func dict(values ...any) (map[string]any, error) {
return dict, nil 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) { func fields(value any) (map[string]any, error) {
v := reflect.Indirect(reflect.ValueOf(value)) v := reflect.Indirect(reflect.ValueOf(value))
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
@ -137,10 +147,13 @@ func fields(value any) (map[string]any, error) {
return m, nil return m, nil
} }
// slice returns a slice of the provided arguments. It's primarily utilized in
// templates.
func slice(elements ...any) []any { func slice(elements ...any) []any {
return elements return elements
} }
// deriveBaseFileName builds the base filename for a given MetadataInfo object.
func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string { func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
// Derive New FileName // Derive New FileName
var newFileName string 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)) 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 { func importStatusPriority(status importStatus) int {
switch status { switch status {
case importFailed: 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 { func NewMgr(c *config.Config) *DBManager {
// Create Manager // Create Manager
dbm := &DBManager{ dbm := &DBManager{
@ -58,7 +58,7 @@ func NewMgr(c *config.Config) *DBManager {
return dbm return dbm
} }
// Init manager // init loads the DB manager
func (dbm *DBManager) init() error { func (dbm *DBManager) init() error {
// Build DB Location // Build DB Location
var dbLocation string var dbLocation string
@ -125,7 +125,7 @@ func (dbm *DBManager) init() error {
return nil return nil
} }
// Reload manager (close DB & reinit) // Reload closes the DB & reinits
func (dbm *DBManager) Reload() error { func (dbm *DBManager) Reload() error {
// Close handle // Close handle
err := dbm.DB.Close() err := dbm.DB.Close()
@ -141,6 +141,7 @@ func (dbm *DBManager) Reload() error {
return nil return nil
} }
// CacheTempTables clears existing statistics and recalculates
func (dbm *DBManager) CacheTempTables() error { func (dbm *DBManager) CacheTempTables() error {
start := time.Now() start := time.Now()
user_streaks_sql := ` user_streaks_sql := `
@ -165,6 +166,8 @@ func (dbm *DBManager) CacheTempTables() error {
return nil return nil
} }
// updateSettings ensures that we're enforcing foreign keys and enable journal
// mode.
func (dbm *DBManager) updateSettings() error { func (dbm *DBManager) updateSettings() error {
// Set SQLite PRAGMA Settings // Set SQLite PRAGMA Settings
pragmaQuery := ` pragmaQuery := `
@ -188,6 +191,7 @@ func (dbm *DBManager) updateSettings() error {
return nil return nil
} }
// performMigrations runs all migrations
func (dbm *DBManager) performMigrations(isNew bool) error { func (dbm *DBManager) performMigrations(isNew bool) error {
// Create context // Create context
ctx := context.WithValue(context.Background(), "isNew", isNew) // nolint 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") 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) { func isEmpty(db *sql.DB) (bool, error) {
var tableCount int var tableCount int
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount) 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 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) { func localTime(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
timeStr, ok := args[0].(string) timeStr, ok := args[0].(string)
if !ok { if !ok {

View File

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