diff --git a/.gitignore b/.gitignore index 4303775..116f2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ go.work # End of https://www.toptal.com/developers/gitignore/api/go pman +coverage.out +.idea/* diff --git a/cmd/alias.go b/cmd/alias.go index e2c8b4c..e85ff66 100644 --- a/cmd/alias.go +++ b/cmd/alias.go @@ -9,6 +9,10 @@ import ( "github.com/theredditbandit/pman/pkg/db" ) +var ( + ErrBadUsageAliasCmd = errors.New("bad usage of alias command") +) + // aliasCmd represents the alias command var aliasCmd = &cobra.Command{ Use: "alias", @@ -17,22 +21,24 @@ var aliasCmd = &cobra.Command{ avlpn or something smaller and use that to query pman`, RunE: func(_ *cobra.Command, args []string) error { if len(args) != 2 { - return errors.New("Please provide a project name and an alias") + fmt.Println("Usage: pman alias ") + return ErrBadUsageAliasCmd } pname := args[0] alias := args[1] - _, err := db.GetRecord(pname, StatusBucket) + _, err := db.GetRecord(db.DBName, pname, StatusBucket) if err != nil { - return fmt.Errorf("%s project does not exist in db", pname) + fmt.Printf("%s project does not exist in db", pname) + return err } fmt.Printf("Aliasing %s to %s \n", pname, alias) data := map[string]string{alias: pname} revData := map[string]string{pname: alias} - err = db.WriteToDB(data, ProjectAliasBucket) + err = db.WriteToDB(db.DBName, data, ProjectAliasBucket) if err != nil { return err } - err = db.WriteToDB(revData, ProjectAliasBucket) + err = db.WriteToDB(db.DBName, revData, ProjectAliasBucket) if err != nil { return err } diff --git a/cmd/delete.go b/cmd/delete.go index 5b37753..b759380 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -9,38 +9,41 @@ import ( "github.com/theredditbandit/pman/pkg/db" ) +var ( + ErrBadUsageDelCmd = errors.New("bad usage of delete command") +) + var delCmd = &cobra.Command{ Use: "delete", Short: "Deletes a project from the index database. This does not delete the project from the filesystem", Aliases: []string{"del", "d"}, RunE: func(_ *cobra.Command, args []string) error { if len(args) != 1 { - return errors.New("Please provide a project name") + fmt.Println("Usage : pman delete ") + return ErrBadUsageDelCmd } projName := args[0] - _, err := db.GetRecord(projName, StatusBucket) + _, err := db.GetRecord(db.DBName, projName, StatusBucket) if err != nil { fmt.Printf("%s is not a valid project to be deleted\n", projName) fmt.Println("Note : projects cannot be deleted using their alias") - - // not a real error here - return nil + return err } - err = db.DeleteFromDb(projName, ProjectPathBucket) + err = db.DeleteFromDb(db.DBName, projName, ProjectPathBucket) if err != nil { return err } - err = db.DeleteFromDb(projName, StatusBucket) + err = db.DeleteFromDb(db.DBName, projName, StatusBucket) if err != nil { return err } - alias, err := db.GetRecord(projName, ProjectAliasBucket) + alias, err := db.GetRecord(db.DBName, projName, ProjectAliasBucket) if err == nil { - err = db.DeleteFromDb(alias, ProjectAliasBucket) + err = db.DeleteFromDb(db.DBName, alias, ProjectAliasBucket) if err != nil { return err } - err = db.DeleteFromDb(projName, ProjectAliasBucket) + err = db.DeleteFromDb(db.DBName, projName, ProjectAliasBucket) if err != nil { return err } @@ -50,7 +53,6 @@ var delCmd = &cobra.Command{ return err } fmt.Printf("Successfully deleted %s from the db \n", projName) - return nil }, } diff --git a/cmd/info.go b/cmd/info.go index 3fa0da7..3c73fa6 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -9,13 +9,18 @@ import ( "github.com/theredditbandit/pman/pkg/utils" ) +var ( + ErrBadUsageInfoCmd = errors.New("bad usage of info command") +) + var infoCmd = &cobra.Command{ Use: "info", Short: "The info command pretty prints the README.md file present at the root of the specified project.", Aliases: []string{"ifo", "ifno", "ino"}, RunE: func(_ *cobra.Command, args []string) error { if len(args) != 1 { - return errors.New("Please provide a project name") + fmt.Println("Please provide a project name") + return ErrBadUsageInfoCmd } projectName := args[0] infoData, err := utils.ReadREADME(projectName) @@ -27,7 +32,6 @@ var infoCmd = &cobra.Command{ return err } fmt.Print(md) - return nil }, } diff --git a/cmd/ls.go b/cmd/ls.go index d7ac692..d61634d 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -19,7 +19,7 @@ var lsCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, _ []string) error { filterFlag, _ := cmd.Flags().GetString("f") oldUI, _ := cmd.Flags().GetBool("o") - data, err := db.GetAllRecords(StatusBucket) + data, err := db.GetAllRecords(db.DBName, StatusBucket) if err != nil { return err } diff --git a/cmd/reset.go b/cmd/reset.go index a86897e..f4cd8af 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -12,13 +12,13 @@ var resetCmd = &cobra.Command{ Use: "reset", Short: "Deletes the current indexed projects, run pman init to reindex the projects", RunE: func(_ *cobra.Command, _ []string) error { - err := db.DeleteDb() + err := db.DeleteDb(db.DBName) if err != nil { + fmt.Println(err) return err } fmt.Println("Successfully reset the database, run pman init to reindex the projects") - return nil }, } diff --git a/cmd/root.go b/cmd/root.go index 523b3a2..4889b96 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,13 +13,18 @@ const ( version = "1.0" ) +var ( + ErrNoArgs = errors.New("this command has no argument") +) + var rootCmd = &cobra.Command{ - Use: "pman", - Short: "A cli project manager", - Version: version, + Use: "pman", + Short: "A cli project manager", + Version: version, + SilenceUsage: true, RunE: func(_ *cobra.Command, args []string) error { if len(args) == 0 { - return errors.New("this command has no argument") + return ErrNoArgs } return nil }, diff --git a/cmd/set.go b/cmd/set.go index 06c66b0..e72ad48 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -9,6 +9,11 @@ import ( "github.com/theredditbandit/pman/pkg/db" ) +var ( + ErrFlagNotImplemented = errors.New("flag not implemented yet") + ErrBadUsageSetCmd = errors.New("bad usage of set command") +) + var setCmd = &cobra.Command{ Use: "set", Short: "Set the status of a project", @@ -22,26 +27,27 @@ var setCmd = &cobra.Command{ interactiveFlag, _ := cmd.Flags().GetBool("i") // TODO: Implement this if interactiveFlag { cmd.SilenceUsage = true - return errors.New("Not implemented yet") + return ErrFlagNotImplemented } if len(args) != 2 { - return errors.New("Please provide a directory name") + fmt.Println("Please provide a directory name") + return ErrBadUsageSetCmd } var pname string alias := args[0] status := args[1] - project, err := db.GetRecord(alias, ProjectAliasBucket) + project, err := db.GetRecord(db.DBName, alias, ProjectAliasBucket) if err == nil { pname = project } else { pname = alias } - err = db.UpdateRec(pname, status, StatusBucket) + err = db.UpdateRec(db.DBName, pname, status, StatusBucket) if err != nil { - return fmt.Errorf("Error updating record : %w", err) + fmt.Println("Error updating record : ", err) + return err } fmt.Printf("Project %s set to status %s\n", pname, status) - return nil }, } diff --git a/cmd/status.go b/cmd/status.go index 3eb4e0e..ba4d671 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -10,6 +10,10 @@ import ( "github.com/theredditbandit/pman/pkg/utils" ) +var ( + ErrBadUsageStatusCmd = errors.New("bad usage of status command") +) + // statusCmd represents the status command var statusCmd = &cobra.Command{ Use: "status", @@ -18,24 +22,25 @@ var statusCmd = &cobra.Command{ RunE: func(_ *cobra.Command, args []string) error { var alias string if len(args) != 1 { - return errors.New("Please provide a project name") + fmt.Println("Please provide a project name") + return ErrBadUsageStatusCmd } projName := args[0] - actualName, err := db.GetRecord(projName, ProjectAliasBucket) + actualName, err := db.GetRecord(db.DBName, projName, ProjectAliasBucket) if err == nil { // check if user has supplied an alias instead of actual project name alias = projName projName = actualName } - status, err := db.GetRecord(projName, StatusBucket) + status, err := db.GetRecord(db.DBName, projName, StatusBucket) if err != nil { - return fmt.Errorf("%s is not a valid project name : Err -> %w", projName, err) + fmt.Printf("%s is not a valid project name : Err -> %s", projName, err) + return err } if alias != "" { fmt.Printf("Status of %s (%s) : %s\n", projName, alias, utils.TitleCase(status)) } else { fmt.Printf("Status of %s : %s\n", projName, utils.TitleCase(status)) } - return nil }, } diff --git a/go.mod b/go.mod index e0e8f82..cb0e9c2 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/lipgloss v0.10.0 github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/thoas/go-funk v0.9.3 go.etcd.io/bbolt v1.3.10 golang.org/x/text v0.15.0 @@ -18,6 +19,7 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.1 // indirect @@ -32,6 +34,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -41,4 +44,5 @@ require ( golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 99e3518..96a1f5f 100644 --- a/go.sum +++ b/go.sum @@ -72,7 +72,11 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= @@ -96,7 +100,9 @@ golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/db/db.go b/pkg/db/db.go index 9b4d199..2698ae4 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -1,7 +1,9 @@ package db import ( + "errors" "fmt" + "log" "os" "os/user" "path/filepath" @@ -9,28 +11,40 @@ import ( bolt "go.etcd.io/bbolt" ) -const DBName = "projects.db" +const DBName string = "projects" +const DBTestName string = "projects_test" + +var ( + ErrOpenDB = errors.New("error opening database") + ErrCreateBucket = errors.New("error creating bucket") + ErrWriteToDB = errors.New("error writing to database") + ErrBucketNotFound = errors.New("bucket not found") + ErrProjectNotFound = errors.New("project not found") + ErrDeleteFromDB = errors.New("error deleting from database") + ErrKeyNotFound = errors.New("key not found in db") + ErrListAllRecords = errors.New("error listing all records") + ErrClearDB = errors.New("error clearing database") + ErrDBNameEmpty = errors.New("dbname cannot be empty") +) // WriteToDB writes the data to the specified bucket in the database -func WriteToDB(data map[string]string, bucketName string) error { - dbLoc, err := getDBLoc(DBName) - if err != nil { - return err - } - db, err := bolt.Open(dbLoc, 0o600, nil) // create the database if it doesn't exist then open it +func WriteToDB(dbname string, data map[string]string, bucketName string) error { + dbLoc, err := GetDBLoc(dbname) if err != nil { - return err + log.Printf("%v : %v \n", ErrOpenDB, err) + return errors.Join(ErrOpenDB, err) } + db, _ := bolt.Open(dbLoc, 0o600, nil) // create the database if it doesn't exist then open it defer db.Close() err = db.Update(func(tx *bolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName)) if err != nil { - return err + return errors.Join(ErrCreateBucket, err) } for k, v := range data { err = bucket.Put([]byte(k), []byte(v)) if err != nil { - return err + return errors.Join(ErrWriteToDB, err) } } return nil @@ -38,36 +52,34 @@ func WriteToDB(data map[string]string, bucketName string) error { return err } -func DeleteFromDb(key string, bucketName string) error { - dbLoc, err := getDBLoc(DBName) - if err != nil { - return err - } - db, err := bolt.Open(dbLoc, 0o600, nil) // create the database if it doesn't exist then open it +func DeleteFromDb(dbname, key, bucketName string) error { + dbLoc, err := GetDBLoc(dbname) if err != nil { - return err + log.Printf("%v : %v \n", ErrOpenDB, err) + return errors.Join(ErrOpenDB, err) } + db, _ := bolt.Open(dbLoc, 0o600, nil) // create the database if it doesn't exist then open it defer db.Close() err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(bucketName)) if bucket == nil { - return fmt.Errorf("bucket %s not found", bucketName) - } - err := bucket.Delete([]byte(key)) - if err != nil { - return err + return ErrBucketNotFound } - return nil + return bucket.Delete([]byte(key)) }) return err } // getDBLoc returns the path to the database file, creating the directory if it doesn't exist -func getDBLoc(dbname string) (string, error) { +func GetDBLoc(dbname string) (string, error) { usr, err := user.Current() if err != nil { return "", err } + if dbname == "" { + return "", ErrDBNameEmpty + } + dbname = dbname + ".db" dbPath := filepath.Join(usr.HomeDir, ".local", "share", "pman", dbname) if _, err := os.Stat(filepath.Dir(dbPath)); os.IsNotExist(err) { err = os.MkdirAll(filepath.Dir(dbPath), 0o755) @@ -79,25 +91,24 @@ func getDBLoc(dbname string) (string, error) { } // GetRecord returns the value of the key from the specified bucket, and error if it does not exist -func GetRecord(key string, bucketName string) (string, error) { - dbLoc, err := getDBLoc(DBName) - if err != nil { - return "", err - } +func GetRecord(dbname, key, bucketName string) (string, error) { var rec string - db, err := bolt.Open(dbLoc, 0o600, nil) + dbLoc, err := GetDBLoc(dbname) if err != nil { + log.Printf("%v : %v \n", ErrOpenDB, err) return "", err } + db, _ := bolt.Open(dbLoc, 0o600, nil) + defer db.Close() err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(bucketName)) if bucket == nil { - return fmt.Errorf("Bucket not found") + return ErrBucketNotFound } v := bucket.Get([]byte(key)) if v == nil { - return fmt.Errorf("key not found in db") + return ErrKeyNotFound } rec = string(v) return nil @@ -109,27 +120,30 @@ func GetRecord(key string, bucketName string) (string, error) { } // GetAllRecords returns all the records from the specified bucket as a dictionary -func GetAllRecords(bucketName string) (map[string]string, error) { - dbLoc, err := getDBLoc(DBName) - if err != nil { - return nil, err - } - db, err := bolt.Open(dbLoc, 0o600, nil) +func GetAllRecords(dbname, bucketName string) (map[string]string, error) { + dbLoc, err := GetDBLoc(dbname) if err != nil { - return nil, err + log.Printf("%v : %v \n", ErrOpenDB, err) + + return map[string]string{}, err } + db, _ := bolt.Open(dbLoc, 0o600, nil) defer db.Close() records := make(map[string]string) err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(bucketName)) if bucket == nil { - return fmt.Errorf("database not found. \nThis could be because no project dir has been initialized yet") + fmt.Print("Database not found \nThis could be because no project dir has been initialized yet \n") + return ErrBucketNotFound } err := bucket.ForEach(func(k, v []byte) error { records[string(k)] = string(v) return nil }) - return err + if err != nil { + return errors.Join(ErrListAllRecords, err) + } + return nil }) if err != nil { return nil, err @@ -138,24 +152,23 @@ func GetAllRecords(bucketName string) (map[string]string, error) { } // UpdateRec updates the value of the key in the specified bucket, usually used to update the status of a project -func UpdateRec(key, val, bucketName string) error { - dbLoc, err := getDBLoc(DBName) - if err != nil { - return err - } - db, err := bolt.Open(dbLoc, 0o600, nil) +func UpdateRec(dbname, key, val, bucketName string) error { + dbLoc, err := GetDBLoc(dbname) if err != nil { + log.Print(err) return err } + db, _ := bolt.Open(dbLoc, 0o600, nil) + defer db.Close() err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(bucketName)) if bucket == nil { - return fmt.Errorf("Bucket not found") + return ErrBucketNotFound } v := bucket.Get([]byte(key)) if v == nil { - return fmt.Errorf("Project not found") + return ErrProjectNotFound } err := bucket.Put([]byte(key), []byte(val)) return err @@ -163,8 +176,8 @@ func UpdateRec(key, val, bucketName string) error { return err } -func DeleteDb() error { - dbLoc, err := getDBLoc(DBName) +func DeleteDb(dbname string) error { + dbLoc, err := GetDBLoc(dbname) if err != nil { return err } diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go new file mode 100644 index 0000000..11cbb73 --- /dev/null +++ b/pkg/db/db_test.go @@ -0,0 +1,436 @@ +package db_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" + + "github.com/theredditbandit/pman/pkg/db" +) + +const dbname = db.DBTestName +const bucketName = "testBucket" +const key = "testKey" + +func Test_GetDBLoc(t *testing.T) { + t.Run("Test getDBLoc", func(t *testing.T) { + expectedWords := []string{".local", "share", "pman"} + + actualPath, err := db.GetDBLoc(dbname) + + t.Cleanup(func() { + _ = os.Remove(actualPath) + }) + + require.NoError(t, err) + assert.Contains(t, actualPath, expectedWords[0]) + assert.Contains(t, actualPath, expectedWords[1]) + assert.Contains(t, actualPath, expectedWords[2]) + assert.Contains(t, actualPath, db.DBTestName) + }) + + t.Run("Test GetDBLoc with empty dbname", func(t *testing.T) { + dbname := "" + expectedErr := db.ErrDBNameEmpty + + actualPath, err := db.GetDBLoc(dbname) + + require.ErrorIs(t, err, expectedErr) + assert.Empty(t, actualPath) + }) +} + +func Test_GetRecord(t *testing.T) { + t.Run("Test GetRecord", func(t *testing.T) { + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + _ = os.Remove(actualPath) + }) + + expectedValue := "testValue" + + err = db.WriteToDB(dbname, map[string]string{key: expectedValue}, bucketName) + require.NoError(t, err) + + actualValue, err := db.GetRecord(dbname, key, bucketName) + require.NoError(t, err) + assert.Equal(t, expectedValue, actualValue) + }) + + t.Run("Test GetRecord with empty dbname", func(t *testing.T) { + dbname := "" + key := "testKey" + bucketName := "testBucket" + expectedErr := db.ErrDBNameEmpty + + actualValue, err := db.GetRecord(dbname, key, bucketName) + + require.ErrorIs(t, err, expectedErr) + assert.Empty(t, actualValue) + }) + + t.Run("Test GetRecord with key not found", func(t *testing.T) { + expectedErr := db.ErrKeyNotFound + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + _ = os.Remove(actualPath) + }) + + err = db.WriteToDB(dbname, map[string]string{}, bucketName) + require.NoError(t, err) + + actualValue, err := db.GetRecord(dbname, key, bucketName) + + require.ErrorIs(t, err, expectedErr) + assert.Empty(t, actualValue) + }) + + t.Run("Test GetRecord with bucket not found", func(t *testing.T) { + expectedErr := db.ErrBucketNotFound + + actualValue, err := db.GetRecord(dbname, key, bucketName) + + require.ErrorIs(t, err, expectedErr) + assert.Empty(t, actualValue) + }) +} +func Test_WriteToDB(t *testing.T) { + t.Run("Test WriteToDB", func(t *testing.T) { + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + _ = os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + bucketName := "testBucket" + + err = db.WriteToDB(dbname, data, bucketName) + require.NoError(t, err) + + // Verify that the data was written correctly + db, err := bolt.Open(actualPath, 0o600, nil) + require.NoError(t, err) + defer db.Close() + + err = db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(bucketName)) + assert.NotNil(t, bucket) + + for k, v := range data { + value := bucket.Get([]byte(k)) + assert.Equal(t, []byte(v), value) + } + + return nil + }) + require.NoError(t, err) + }) + + t.Run("Test WriteToDB with empty bucket name", func(t *testing.T) { + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + _ = os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + bucketName := "" + + err = db.WriteToDB(dbname, data, bucketName) + + require.ErrorIs(t, err, db.ErrCreateBucket) + }) + + t.Run("Test WriteToDB with empty map key", func(t *testing.T) { + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + data := map[string]string{ + "": "value1", + } + + err = db.WriteToDB(dbname, data, bucketName) + + require.ErrorIs(t, err, db.ErrWriteToDB) + }) + + t.Run("Test WriteToDB with empty dbname value", func(t *testing.T) { + dbname := "" + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + bucketName := "testBucket" + + err := db.WriteToDB(dbname, data, bucketName) + + require.ErrorIs(t, err, db.ErrOpenDB) + }) +} + +func Test_DeleteFromDb(t *testing.T) { + t.Run("Test DeleteFromDb", func(t *testing.T) { + + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + key := "key1" + + err = db.WriteToDB(dbname, data, bucketName) + require.NoError(t, err) + + err = db.DeleteFromDb(dbname, key, bucketName) + require.NoError(t, err) + + // Verify that the key was deleted + db, err := bolt.Open(actualPath, 0o600, nil) + require.NoError(t, err) + defer db.Close() + + err = db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(bucketName)) + assert.NotNil(t, bucket) + + value := bucket.Get([]byte(key)) + assert.Nil(t, value) + + return nil + }) + require.NoError(t, err) + }) + + t.Run("Test DeleteFromDb with empty dbname", func(t *testing.T) { + dbname := "" + key := "key1" + expectedErr := db.ErrDBNameEmpty + + err := db.DeleteFromDb(dbname, key, bucketName) + + require.ErrorIs(t, err, expectedErr) + }) + + t.Run("Test DeleteFromDb with key not found", func(t *testing.T) { + + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + key := "key4" + + err = db.WriteToDB(dbname, data, bucketName) + require.NoError(t, err) + + err = db.DeleteFromDb(dbname, key, bucketName) + + require.NoError(t, err) + }) + + t.Run("Test DeleteFromDb with bucket not found", func(t *testing.T) { + + key := "key1" + expectedErr := db.ErrBucketNotFound + + err := db.DeleteFromDb(dbname, key, bucketName) + + require.ErrorIs(t, err, expectedErr) + }) +} + +func Test_ListAllRecords(t *testing.T) { + t.Run("Test ListAllRecords", func(t *testing.T) { + + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + + err = db.WriteToDB(dbname, data, bucketName) + require.NoError(t, err) + + records, err := db.GetAllRecords(dbname, bucketName) + + require.NoError(t, err) + assert.Equal(t, data, records) + }) + + t.Run("Test ListAllRecords with empty dbname", func(t *testing.T) { + dbname := "" + expectedErr := db.ErrDBNameEmpty + expectedValue := map[string]string{} + + records, err := db.GetAllRecords(dbname, bucketName) + + require.ErrorIs(t, err, expectedErr) + assert.Equal(t, expectedValue, records) + }) + + t.Run("Test ListAllRecords with bucket not found", func(t *testing.T) { + expectedErr := db.ErrBucketNotFound + + records, err := db.GetAllRecords(dbname, bucketName) + + require.ErrorIs(t, err, expectedErr) + assert.Nil(t, records) + }) +} +func Test_UpdateRec(t *testing.T) { + t.Run("Test UpdateRec", func(t *testing.T) { + + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + bucketName := "testBucket" + key := "key1" + newValue := "updatedValue" + + err = db.WriteToDB(dbname, data, bucketName) + require.NoError(t, err) + + err = db.UpdateRec(dbname, key, newValue, bucketName) + require.NoError(t, err) + + // Verify that the value was updated + db, err := bolt.Open(actualPath, 0o600, nil) + require.NoError(t, err) + defer db.Close() + + err = db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(bucketName)) + assert.NotNil(t, bucket) + + value := bucket.Get([]byte(key)) + assert.Equal(t, []byte(newValue), value) + return nil + }) + require.NoError(t, err) + }) + + t.Run("Test UpdateRec with empty dbname", func(t *testing.T) { + dbname := "" + key := "key1" + newValue := "updatedValue" + err := db.UpdateRec(dbname, key, newValue, bucketName) + + require.ErrorIs(t, err, db.ErrDBNameEmpty) + }) + + t.Run("Test UpdateRec with key not found", func(t *testing.T) { + + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + data := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + key := "key4" + newValue := "updatedValue" + + err = db.WriteToDB(dbname, data, bucketName) + require.NoError(t, err) + + err = db.UpdateRec(dbname, key, newValue, bucketName) + + require.ErrorIs(t, err, db.ErrProjectNotFound) + }) + + t.Run("Test UpdateRec with bucket not found", func(t *testing.T) { + + key := "key1" + newValue := "updatedValue" + expectedErr := db.ErrBucketNotFound + + err := db.UpdateRec(dbname, key, newValue, bucketName) + + require.ErrorIs(t, err, expectedErr) + }) +} + +func Test_DeleteDb(t *testing.T) { + t.Run("Test DeleteDb", func(t *testing.T) { + actualPath, err := db.GetDBLoc(dbname) + require.NoError(t, err) + + t.Cleanup(func() { + os.Remove(actualPath) + }) + + err = db.DeleteDb(dbname) + require.NoError(t, err) + + // Verify that the database file is deleted + _, err = os.Stat(actualPath) + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("Test DeleteDb with empty dbname", func(t *testing.T) { + dbname := "" + expectedErr := db.ErrDBNameEmpty + + err := db.DeleteDb(dbname) + + require.ErrorIs(t, err, expectedErr) + }) +} diff --git a/pkg/indexer.go b/pkg/indexer.go index d29eb37..dff5575 100644 --- a/pkg/indexer.go +++ b/pkg/indexer.go @@ -2,7 +2,7 @@ package pkg import ( "errors" - "fmt" + "log" "os" "path/filepath" @@ -15,39 +15,51 @@ const ( ProjectAliasBucket = "projectAliases" ) +var ( + ErrDirname = errors.New("error providing a directory name") + ErrDirInvalid = errors.New("error providing a valid directory name") + ErrIsNotDir = errors.New("error providing a file instead of a directory") + ErrIndexDir = errors.New("error indexing directory") +) + // InitDirs indexes a directory for project directories and writes the data to the DB func InitDirs(args []string) error { // the file which identifies a project directory projIdentifier := "README.md" if len(args) != 1 { - return errors.New("Please provide a directory name") + log.Print("Please provide a directory name") + return ErrDirname } dirname := args[0] if stat, err := os.Stat(dirname); os.IsNotExist(err) { - return fmt.Errorf("%s is not a directory", dirname) + log.Printf("%s is not a directory \n", dirname) + return ErrDirInvalid } else if !stat.IsDir() { - return fmt.Errorf("%s is a file and not a directory", dirname) + log.Printf("%s is a file and not a directory \n", dirname) + return ErrIsNotDir } projDirs, err := indexDir(dirname, projIdentifier) if err != nil { - return err + log.Print(err) + return ErrIndexDir } - fmt.Printf("Indexed %d project directories . . .\n", len(projDirs)) + log.Printf("Indexed %d project directories . . .\n", len(projDirs)) projectStatusMap := make(map[string]string) projectPathMap := make(map[string]string) for k, v := range projDirs { // k : full project path, v : project status , projectStatusMap[filepath.Base(k)] = v // filepath.Base(k) : project name projectPathMap[filepath.Base(k)] = k } - err = db.WriteToDB(projectStatusMap, StatusBucket) + err = db.WriteToDB(db.DBName, projectStatusMap, StatusBucket) if err != nil { + log.Print(err) return err } - err = db.WriteToDB(projectPathMap, ProjectPaths) + err = db.WriteToDB(db.DBName, projectPathMap, ProjectPaths) if err != nil { + log.Print(err) return err } - return nil } diff --git a/pkg/ui/interactiveTable.go b/pkg/ui/interactiveTable.go index 437dfa3..86fc82c 100644 --- a/pkg/ui/interactiveTable.go +++ b/pkg/ui/interactiveTable.go @@ -60,7 +60,7 @@ func RenderInteractiveTable(data map[string]string) error { } var rows []table.Row for proj, status := range data { - alias, err := db.GetRecord(proj, pkg.ProjectAliasBucket) + alias, err := db.GetRecord(db.DBName, proj, pkg.ProjectAliasBucket) lastEdited := utils.GetLastModifiedTime(proj) if err == nil { pname := fmt.Sprintf("%s (%s)", proj, alias) diff --git a/pkg/ui/statusTable.go b/pkg/ui/statusTable.go index 4838447..fc3338f 100644 --- a/pkg/ui/statusTable.go +++ b/pkg/ui/statusTable.go @@ -17,7 +17,7 @@ import ( func RenderTable(data map[string]string) error { var tableData [][]string for p, status := range data { - alias, err := db.GetRecord(p, pkg.ProjectAliasBucket) + alias, err := db.GetRecord(db.DBName, p, pkg.ProjectAliasBucket) lastEdited := utils.GetLastModifiedTime(p) if err == nil { pname := fmt.Sprintf("%s (%s)", p, alias) diff --git a/pkg/ui/tui.go b/pkg/ui/tui.go index a7f8035..94dc92d 100644 --- a/pkg/ui/tui.go +++ b/pkg/ui/tui.go @@ -79,7 +79,7 @@ func newModel() (model, error) { delegateKeys = newDelegateKeyMap() ) - data, err := db.GetAllRecords(pkg.StatusBucket) + data, err := db.GetAllRecords(db.DBName, pkg.StatusBucket) if err != nil { return model{}, err } @@ -185,7 +185,7 @@ func Tui() error { } if _, err := tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil { - return fmt.Errorf("Error running program: %w", err) + return fmt.Errorf("error running program: %w", err) } return nil diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index be44b61..ddca814 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,7 +1,9 @@ package utils import ( + "errors" "fmt" + "log" "os" "path/filepath" "time" @@ -14,6 +16,11 @@ import ( "github.com/theredditbandit/pman/pkg/db" ) +var ( + ErrBeautifyMD = errors.New("error beautifying markdown") + ErrReadREADME = errors.New("error reading README") +) + func TitleCase(s string) string { c := cases.Title(language.English) return c.String(s) @@ -32,11 +39,11 @@ func FilterByStatus(data map[string]string, status string) map[string]string { // Deprecated: Use ui.RenderTable instead func PrintData(data map[string]string) { for k, v := range data { - alias, err := db.GetRecord(k, pkg.ProjectAliasBucket) + alias, err := db.GetRecord(db.DBName, k, pkg.ProjectAliasBucket) if err == nil { - fmt.Printf("%s : %s (%s) \n", TitleCase(v), k, alias) + log.Printf("%s : %s (%s) \n", TitleCase(v), k, alias) } else { - fmt.Printf("%s : %s \n", TitleCase(v), k) + log.Printf("%s : %s \n", TitleCase(v), k) } } } @@ -46,7 +53,7 @@ func GetLastModifiedTime(pname string) string { var lastModFile string today := time.Now() _ = lastModFile - pPath, err := db.GetRecord(pname, pkg.ProjectPaths) + pPath, err := db.GetRecord(db.DBName, pname, pkg.ProjectPaths) if err != nil { return "Something went wrong" } @@ -80,29 +87,28 @@ func BeautifyMD(data []byte) (string, error) { glamour.WithAutoStyle(), ) if err != nil { - return "", fmt.Errorf("something went wrong while creating renderer: %w", err) - } - out, err := r.Render(string(data)) - if err != nil { - return "", err + log.Print("something went wrong while creating renderer: ", err) + return "", errors.Join(ErrBeautifyMD, err) } + out, _ := r.Render(string(data)) return out, nil } // ReadREADME: returns the byte array of README.md of a project func ReadREADME(projectName string) ([]byte, error) { - actualName, err := db.GetRecord(projectName, pkg.ProjectAliasBucket) + actualName, err := db.GetRecord(db.DBName, projectName, pkg.ProjectAliasBucket) if err == nil { projectName = actualName } - path, err := db.GetRecord(projectName, pkg.ProjectPaths) + path, err := db.GetRecord(db.DBName, projectName, pkg.ProjectPaths) if err != nil { - return nil, fmt.Errorf("project: %v not a valid project", projectName) + log.Printf("project: %v not a valid project\n", projectName) + return nil, errors.Join(ErrReadREADME, err) } pPath := filepath.Join(path, "README.md") data, err := os.ReadFile(pPath) if err != nil { - return nil, fmt.Errorf("Something went wrong while reading README for %s: %w", projectName, err) + return nil, fmt.Errorf("something went wrong while reading README for %s: %w", projectName, err) } return data, nil }