Add user list command

This commit is contained in:
Sung 2025-11-01 19:39:49 -07:00
commit 8934db27db
6 changed files with 176 additions and 5 deletions

View file

@ -331,3 +331,23 @@ func TestServerUserCreateHelp(t *testing.T) {
assert.Equal(t, strings.Contains(outputStr, "--password"), true, "help should show --password (double dash)")
assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "help should show --dbPath (double dash)")
}
func TestServerUserList(t *testing.T) {
tmpDB := t.TempDir() + "/test.db"
// Create two users
exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "alice@example.com", "--password", "password123").CombinedOutput()
exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "bob@example.com", "--password", "password123").CombinedOutput()
// List users
listCmd := exec.Command(testServerBinary, "user", "list", "--dbPath", tmpDB)
output, err := listCmd.CombinedOutput()
if err != nil {
t.Fatalf("user list failed: %v\nOutput: %s", err, output)
}
outputStr := string(output)
assert.Equal(t, strings.Contains(outputStr, "alice@example.com"), true, "output should have alice")
assert.Equal(t, strings.Contains(outputStr, "bob@example.com"), true, "output should have bob")
}

View file

@ -116,6 +116,17 @@ func (a *App) GetUserByEmail(email string) (*database.User, error) {
return &user, nil
}
// GetAllUsers retrieves all users from the database
func (a *App) GetAllUsers() ([]database.User, error) {
var users []database.User
err := a.DB.Find(&users).Error
if err != nil {
return nil, pkgErrors.Wrap(err, "finding users")
}
return users, nil
}
// Authenticate authenticates a user
func (a *App) Authenticate(email, password string) (*database.User, error) {
user, err := a.GetUserByEmail(email)

View file

@ -108,6 +108,72 @@ func TestGetUserByEmail(t *testing.T) {
})
}
func TestGetAllUsers(t *testing.T) {
t.Run("success with multiple users", func(t *testing.T) {
db := testutils.InitMemoryDB(t)
user1 := testutils.SetupUserData(db, "alice@example.com", "password123")
user2 := testutils.SetupUserData(db, "bob@example.com", "password123")
user3 := testutils.SetupUserData(db, "charlie@example.com", "password123")
a := NewTest()
a.DB = db
users, err := a.GetAllUsers()
assert.Equal(t, err, nil, "should not error")
assert.Equal(t, len(users), 3, "should return 3 users")
// Verify all users are returned
emails := make(map[string]bool)
for _, user := range users {
emails[user.Email.String] = true
}
assert.Equal(t, emails["alice@example.com"], true, "alice should be in results")
assert.Equal(t, emails["bob@example.com"], true, "bob should be in results")
assert.Equal(t, emails["charlie@example.com"], true, "charlie should be in results")
// Verify user details match
for _, user := range users {
if user.Email.String == "alice@example.com" {
assert.Equal(t, user.ID, user1.ID, "alice ID mismatch")
} else if user.Email.String == "bob@example.com" {
assert.Equal(t, user.ID, user2.ID, "bob ID mismatch")
} else if user.Email.String == "charlie@example.com" {
assert.Equal(t, user.ID, user3.ID, "charlie ID mismatch")
}
}
})
t.Run("empty database", func(t *testing.T) {
db := testutils.InitMemoryDB(t)
a := NewTest()
a.DB = db
users, err := a.GetAllUsers()
assert.Equal(t, err, nil, "should not error")
assert.Equal(t, len(users), 0, "should return 0 users")
})
t.Run("single user", func(t *testing.T) {
db := testutils.InitMemoryDB(t)
user := testutils.SetupUserData(db, "alice@example.com", "password123")
a := NewTest()
a.DB = db
users, err := a.GetAllUsers()
assert.Equal(t, err, nil, "should not error")
assert.Equal(t, len(users), 1, "should return 1 user")
assert.Equal(t, users[0].Email.String, "alice@example.com", "email mismatch")
assert.Equal(t, users[0].ID, user.ID, "user ID mismatch")
})
}
func TestCreateUser(t *testing.T) {
t.Run("success", func(t *testing.T) {
db := testutils.InitMemoryDB(t)

View file

@ -111,8 +111,8 @@ func requireString(fs *flag.FlagSet, value, fieldName string) {
}
}
// setupAppWithDB creates config, initializes app, and returns cleanup function
func setupAppWithDB(fs *flag.FlagSet, dbPath string) (*app.App, func()) {
// createApp creates config, initializes app, and returns cleanup function
func createApp(fs *flag.FlagSet, dbPath string) (*app.App, func()) {
cfg, err := config.New(config.Params{
DBPath: dbPath,
})

View file

@ -51,7 +51,7 @@ func userCreateCmd(args []string) {
requireString(fs, *email, "email")
requireString(fs, *password, "password")
a, cleanup := setupAppWithDB(fs, *dbPath)
a, cleanup := createApp(fs, *dbPath)
defer cleanup()
_, err := a.CreateUser(*email, *password, *password)
@ -74,7 +74,7 @@ func userRemoveCmd(args []string, stdin io.Reader) {
requireString(fs, *email, "email")
a, cleanup := setupAppWithDB(fs, *dbPath)
a, cleanup := createApp(fs, *dbPath)
defer cleanup()
// Check if user exists first
@ -127,7 +127,7 @@ func userResetPasswordCmd(args []string) {
requireString(fs, *email, "email")
requireString(fs, *password, "password")
a, cleanup := setupAppWithDB(fs, *dbPath)
a, cleanup := createApp(fs, *dbPath)
defer cleanup()
// Find the user
@ -151,6 +151,27 @@ func userResetPasswordCmd(args []string) {
fmt.Printf("Email: %s\n", *email)
}
func userListCmd(args []string, output io.Writer) {
fs := setupFlagSet("list", "dnote-server user list")
dbPath := fs.String("dbPath", "", "Path to SQLite database file (env: DBPath, default: $XDG_DATA_HOME/dnote/server.db)")
fs.Parse(args)
a, cleanup := createApp(fs, *dbPath)
defer cleanup()
users, err := a.GetAllUsers()
if err != nil {
log.ErrorWrap(err, "listing users")
os.Exit(1)
}
for _, user := range users {
fmt.Fprintf(output, "%s,%s,%s\n", user.UUID, user.Email.String, user.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
}
}
func userCmd(args []string) {
if len(args) < 1 {
fmt.Println(`Usage:
@ -158,6 +179,7 @@ func userCmd(args []string) {
Available commands:
create: Create a new user
list: List all users
remove: Remove a user
reset-password: Reset a user's password`)
os.Exit(1)
@ -172,6 +194,8 @@ Available commands:
switch subcommand {
case "create":
userCreateCmd(subArgs)
case "list":
userListCmd(subArgs, os.Stdout)
case "remove":
userRemoveCmd(subArgs, os.Stdin)
case "reset-password":
@ -180,6 +204,7 @@ Available commands:
fmt.Printf("Unknown subcommand: %s\n\n", subcommand)
fmt.Println(`Available commands:
create: Create a new user
list: List all users
remove: Remove a user (only if they have no notes or books)
reset-password: Reset a user's password`)
os.Exit(1)

View file

@ -16,6 +16,8 @@
package cmd
import (
"bytes"
"fmt"
"strings"
"testing"
@ -107,3 +109,50 @@ func TestUserResetPasswordCmd(t *testing.T) {
err = bcrypt.CompareHashAndPassword([]byte(updatedUser.Password.String), []byte("oldpassword123"))
assert.Equal(t, err != nil, true, "old password should not match")
}
func TestUserListCmd(t *testing.T) {
t.Run("multiple users", func(t *testing.T) {
tmpDB := t.TempDir() + "/test.db"
// Create multiple users
db := testutils.InitDB(tmpDB)
user1 := testutils.SetupUserData(db, "alice@example.com", "password123")
user2 := testutils.SetupUserData(db, "bob@example.com", "password123")
user3 := testutils.SetupUserData(db, "charlie@example.com", "password123")
sqlDB, _ := db.DB()
sqlDB.Close()
// Capture output
var buf bytes.Buffer
userListCmd([]string{"--dbPath", tmpDB}, &buf)
// Verify output matches expected format
output := strings.TrimSpace(buf.String())
lines := strings.Split(output, "\n")
expectedLine1 := fmt.Sprintf("%s,alice@example.com,%s", user1.UUID, user1.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
expectedLine2 := fmt.Sprintf("%s,bob@example.com,%s", user2.UUID, user2.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
expectedLine3 := fmt.Sprintf("%s,charlie@example.com,%s", user3.UUID, user3.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"))
assert.Equal(t, lines[0], expectedLine1, "line 1 should match")
assert.Equal(t, lines[1], expectedLine2, "line 2 should match")
assert.Equal(t, lines[2], expectedLine3, "line 3 should match")
})
t.Run("empty database", func(t *testing.T) {
tmpDB := t.TempDir() + "/test.db"
// Initialize empty database
db := testutils.InitDB(tmpDB)
sqlDB, _ := db.DB()
sqlDB.Close()
// Capture output
var buf bytes.Buffer
userListCmd([]string{"--dbPath", tmpDB}, &buf)
// Verify no output
output := buf.String()
assert.Equal(t, output, "", "should have no output for empty database")
})
}