feat(cli): add a json output and tests
This commit is contained in:
@@ -6,8 +6,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"goyco/cmd/goyco/commands"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func loadDotEnv() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -16,6 +17,23 @@ var ErrHelpRequested = errors.New("help requested")
|
||||
|
||||
type DBConnector func(cfg *config.Config) (*gorm.DB, func() error, error)
|
||||
|
||||
var (
|
||||
jsonOutputMu sync.RWMutex
|
||||
jsonOutput bool
|
||||
)
|
||||
|
||||
func SetJSONOutput(enabled bool) {
|
||||
jsonOutputMu.Lock()
|
||||
defer jsonOutputMu.Unlock()
|
||||
jsonOutput = enabled
|
||||
}
|
||||
|
||||
func IsJSONOutput() bool {
|
||||
jsonOutputMu.RLock()
|
||||
defer jsonOutputMu.RUnlock()
|
||||
return jsonOutput
|
||||
}
|
||||
|
||||
var (
|
||||
dbConnectorMu sync.RWMutex
|
||||
currentDBConnector = defaultDBConnector
|
||||
@@ -93,3 +111,39 @@ func truncate(in string, max int) string {
|
||||
}
|
||||
return in[:max-3] + "..."
|
||||
}
|
||||
|
||||
func outputJSON(v interface{}) error {
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(v)
|
||||
}
|
||||
|
||||
func outputMessage(message string, args ...interface{}) {
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"message": fmt.Sprintf(message, args...),
|
||||
})
|
||||
} else {
|
||||
fmt.Printf(message+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
func outputError(message string, args ...interface{}) {
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"error": fmt.Sprintf(message, args...),
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, message+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
func outputWarning(message string, args ...interface{}) {
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"warning": fmt.Sprintf(message, args...),
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Warning: "+message+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,3 +217,41 @@ func setInMemoryDBConnector(t *testing.T) {
|
||||
SetDBConnector(nil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetJSONOutput(t *testing.T) {
|
||||
t.Run("set and get JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
if !IsJSONOutput() {
|
||||
t.Error("expected JSON output to be enabled")
|
||||
}
|
||||
|
||||
SetJSONOutput(false)
|
||||
if IsJSONOutput() {
|
||||
t.Error("expected JSON output to be disabled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrent access", func(t *testing.T) {
|
||||
SetJSONOutput(false)
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
SetJSONOutput(true)
|
||||
_ = IsJSONOutput()
|
||||
SetJSONOutput(false)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
_ = IsJSONOutput()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
<-done
|
||||
<-done
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,23 +86,50 @@ func runStatusCommand(cfg *config.Config) error {
|
||||
pidFile := filepath.Join(pidDir, "goyco.pid")
|
||||
|
||||
if !isDaemonRunning(pidFile) {
|
||||
fmt.Println("Goyco is not running")
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "not_running",
|
||||
})
|
||||
} else {
|
||||
fmt.Println("Goyco is not running")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Goyco is running (PID file exists but cannot be read: %v)\n", err)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "running",
|
||||
"error": fmt.Sprintf("PID file exists but cannot be read: %v", err),
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Goyco is running (PID file exists but cannot be read: %v)\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(string(data))
|
||||
if err != nil {
|
||||
fmt.Printf("Goyco is running (PID file exists but contains invalid PID: %v)\n", err)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "running",
|
||||
"error": fmt.Sprintf("PID file exists but contains invalid PID: %v", err),
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Goyco is running (PID file exists but contains invalid PID: %v)\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Goyco is running (PID %d)\n", pid)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "running",
|
||||
"pid": pid,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Goyco is running (PID %d)\n", pid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -143,7 +170,14 @@ func stopDaemon(cfg *config.Config) error {
|
||||
|
||||
_ = os.Remove(pidFile)
|
||||
|
||||
fmt.Printf("Goyco stopped (PID %d)\n", pid)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "stopped",
|
||||
"pid": pid,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Goyco stopped (PID %d)\n", pid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -184,9 +218,18 @@ func runDaemon(cfg *config.Config) error {
|
||||
if err := writePIDFile(pidFile, pid); err != nil {
|
||||
return fmt.Errorf("cannot write PID file: %w", err)
|
||||
}
|
||||
fmt.Printf("Goyco started with PID %d\n", pid)
|
||||
fmt.Printf("PID file: %s\n", pidFile)
|
||||
fmt.Printf("Log file: %s\n", logFile)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "started",
|
||||
"pid": pid,
|
||||
"pid_file": pidFile,
|
||||
"log_file": logFile,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Goyco started with PID %d\n", pid)
|
||||
fmt.Printf("PID file: %s\n", pidFile)
|
||||
fmt.Printf("Log file: %s\n", logFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,20 @@ func TestRunStatusCommand(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("daemon not running with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
cfg.PIDDir = tempDir
|
||||
|
||||
err := runStatusCommand(cfg)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("daemon running with valid PID", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
cfg.PIDDir = tempDir
|
||||
@@ -118,6 +132,27 @@ func TestRunStatusCommand(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("daemon running with valid PID and JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
cfg.PIDDir = tempDir
|
||||
|
||||
pidFile := filepath.Join(tempDir, "goyco.pid")
|
||||
currentPID := os.Getpid()
|
||||
err := os.WriteFile(pidFile, []byte(strconv.Itoa(currentPID)), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PID file: %v", err)
|
||||
}
|
||||
|
||||
err = runStatusCommand(cfg)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("daemon running with invalid PID file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
cfg.PIDDir = tempDir
|
||||
|
||||
@@ -30,11 +30,20 @@ func HandleMigrateCommand(cfg *config.Config, name string, args []string) error
|
||||
}
|
||||
|
||||
func runMigrateCommand(db *gorm.DB) error {
|
||||
fmt.Println("Running database migrations...")
|
||||
if !IsJSONOutput() {
|
||||
fmt.Println("Running database migrations...")
|
||||
}
|
||||
if err := database.Migrate(db); err != nil {
|
||||
return fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
fmt.Println("Migrations applied successfully")
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "migrations_applied",
|
||||
"status": "success",
|
||||
})
|
||||
} else {
|
||||
fmt.Println("Migrations applied successfully")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,18 @@ func TestHandleMigrateCommand(t *testing.T) {
|
||||
t.Fatalf("unexpected error running migrations: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("runs migrations with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
cfg := testutils.NewTestConfig()
|
||||
setInMemoryDBConnector(t)
|
||||
|
||||
err := HandleMigrateCommand(cfg, "migrate", []string{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error running migrations: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,7 +90,14 @@ func postDelete(repo repositories.PostRepository, args []string) error {
|
||||
return fmt.Errorf("delete post: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Post deleted: ID=%d\n", id)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "post_deleted",
|
||||
"id": id,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Post deleted: ID=%d\n", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -126,6 +133,38 @@ func postList(postQueries *services.PostQueries, args []string) error {
|
||||
return fmt.Errorf("list posts: %w", err)
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
type postJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
AuthorID uint `json:"author_id"`
|
||||
Score int `json:"score"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
postsJSON := make([]postJSON, len(posts))
|
||||
for i, p := range posts {
|
||||
authorID := uint(0)
|
||||
if p.AuthorID != nil {
|
||||
authorID = *p.AuthorID
|
||||
}
|
||||
if p.Author.ID != 0 {
|
||||
authorID = p.Author.ID
|
||||
}
|
||||
postsJSON[i] = postJSON{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
AuthorID: authorID,
|
||||
Score: p.Score,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"posts": postsJSON,
|
||||
"count": len(postsJSON),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
fmt.Println("No posts found")
|
||||
return nil
|
||||
@@ -234,6 +273,39 @@ func postSearch(postQueries *services.PostQueries, args []string) error {
|
||||
return fmt.Errorf("search posts: %w", err)
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
type postJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
AuthorID uint `json:"author_id"`
|
||||
Score int `json:"score"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
postsJSON := make([]postJSON, len(posts))
|
||||
for i, p := range posts {
|
||||
authorID := uint(0)
|
||||
if p.AuthorID != nil {
|
||||
authorID = *p.AuthorID
|
||||
}
|
||||
if p.Author.ID != 0 {
|
||||
authorID = p.Author.ID
|
||||
}
|
||||
postsJSON[i] = postJSON{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
AuthorID: authorID,
|
||||
Score: p.Score,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"search_term": sanitizedTerm,
|
||||
"posts": postsJSON,
|
||||
"count": len(postsJSON),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
fmt.Println("No posts found matching your search")
|
||||
return nil
|
||||
|
||||
@@ -89,6 +89,26 @@ func TestPostDelete(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful delete with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
freshMockRepo := testutils.NewMockPostRepository()
|
||||
testPost := &database.Post{
|
||||
Title: "Test Post",
|
||||
Content: "Test Content",
|
||||
AuthorID: &[]uint{1}[0],
|
||||
Score: 0,
|
||||
}
|
||||
_ = freshMockRepo.Create(testPost)
|
||||
|
||||
err := postDelete(freshMockRepo, []string{"1"})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing id", func(t *testing.T) {
|
||||
err := postDelete(mockRepo, []string{})
|
||||
|
||||
@@ -174,6 +194,17 @@ func TestPostList(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list all posts with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
err := postList(postQueries, []string{})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list with limit", func(t *testing.T) {
|
||||
err := postList(postQueries, []string{"--limit", "1"})
|
||||
|
||||
@@ -271,6 +302,17 @@ func TestPostSearch(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("search with results and JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
err := postSearch(postQueries, []string{"Go"})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case insensitive search", func(t *testing.T) {
|
||||
mockRepo.SearchCalls = nil
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ func printPruneUsage() {
|
||||
func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
||||
fs := flag.NewFlagSet("prune posts", flag.ContinueOnError)
|
||||
dryRun := fs.Bool("dry-run", false, "preview what would be deleted without actually deleting")
|
||||
yes := fs.Bool("yes", false, "skip confirmation prompt")
|
||||
fs.SetOutput(os.Stderr)
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
@@ -73,6 +74,49 @@ func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
||||
return fmt.Errorf("get posts by deleted users: %w", err)
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
type postJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
postsJSON := make([]postJSON, len(posts))
|
||||
for i, post := range posts {
|
||||
authorName := "(deleted)"
|
||||
if post.Author.ID != 0 {
|
||||
authorName = post.Author.Username
|
||||
}
|
||||
postsJSON[i] = postJSON{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Author: authorName,
|
||||
URL: post.URL,
|
||||
}
|
||||
}
|
||||
if *dryRun {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_posts",
|
||||
"dry_run": true,
|
||||
"posts": postsJSON,
|
||||
"count": len(postsJSON),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if !*yes {
|
||||
return fmt.Errorf("confirmation required. Use --yes to skip prompt in JSON mode")
|
||||
}
|
||||
deletedCount, err := postRepo.HardDeletePostsByDeletedUsers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("hard delete posts: %w", err)
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_posts",
|
||||
"deleted_count": deletedCount,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(posts) == 0 {
|
||||
fmt.Println("No posts found for deleted users")
|
||||
return nil
|
||||
@@ -93,15 +137,17 @@ func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nAre you sure you want to permanently delete %d posts? (yes/no): ", len(posts))
|
||||
var confirmation string
|
||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||
return fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
if !*yes {
|
||||
fmt.Printf("\nAre you sure you want to permanently delete %d posts? (yes/no): ", len(posts))
|
||||
var confirmation string
|
||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||
return fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if confirmation != "yes" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
if confirmation != "yes" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deletedCount, err := postRepo.HardDeletePostsByDeletedUsers()
|
||||
@@ -117,6 +163,7 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
||||
fs := flag.NewFlagSet("prune users", flag.ContinueOnError)
|
||||
dryRun := fs.Bool("dry-run", false, "preview what would be deleted without actually deleting")
|
||||
deletePosts := fs.Bool("with-posts", false, "also delete all posts when deleting users")
|
||||
yes := fs.Bool("yes", false, "skip confirmation prompt")
|
||||
fs.SetOutput(os.Stderr)
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
@@ -130,7 +177,14 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
||||
|
||||
userCount := len(users)
|
||||
if userCount == 0 {
|
||||
fmt.Println("No users found to delete")
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_users",
|
||||
"count": 0,
|
||||
})
|
||||
} else {
|
||||
fmt.Println("No users found to delete")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -142,6 +196,61 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
||||
}
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
type userJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
usersJSON := make([]userJSON, len(users))
|
||||
for i, user := range users {
|
||||
usersJSON[i] = userJSON{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
}
|
||||
}
|
||||
if *dryRun {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_users",
|
||||
"dry_run": true,
|
||||
"users": usersJSON,
|
||||
"user_count": userCount,
|
||||
"post_count": postCount,
|
||||
"with_posts": *deletePosts,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if !*yes {
|
||||
return fmt.Errorf("confirmation required. Use --yes to skip prompt or --json for non-interactive mode")
|
||||
}
|
||||
if *deletePosts {
|
||||
totalDeleted, err := userRepo.HardDeleteAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("hard delete all users and posts: %w", err)
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_users",
|
||||
"deleted_count": totalDeleted,
|
||||
"with_posts": true,
|
||||
})
|
||||
} else {
|
||||
deletedCount := 0
|
||||
for _, user := range users {
|
||||
if err := userRepo.SoftDeleteWithPosts(user.ID); err != nil {
|
||||
return fmt.Errorf("soft delete user %d: %w", user.ID, err)
|
||||
}
|
||||
deletedCount++
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_users",
|
||||
"deleted_count": deletedCount,
|
||||
"with_posts": false,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users", userCount)
|
||||
if *deletePosts {
|
||||
fmt.Printf(" and %d posts", postCount)
|
||||
@@ -158,21 +267,23 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
||||
return nil
|
||||
}
|
||||
|
||||
confirmMsg := fmt.Sprintf("\nAre you sure you want to permanently delete %d users", userCount)
|
||||
if *deletePosts {
|
||||
confirmMsg += fmt.Sprintf(" and %d posts", postCount)
|
||||
}
|
||||
confirmMsg += "? (yes/no): "
|
||||
fmt.Print(confirmMsg)
|
||||
if !*yes {
|
||||
confirmMsg := fmt.Sprintf("\nAre you sure you want to permanently delete %d users", userCount)
|
||||
if *deletePosts {
|
||||
confirmMsg += fmt.Sprintf(" and %d posts", postCount)
|
||||
}
|
||||
confirmMsg += "? (yes/no): "
|
||||
fmt.Print(confirmMsg)
|
||||
|
||||
var confirmation string
|
||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||
return fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
var confirmation string
|
||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||
return fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if confirmation != "yes" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
if confirmation != "yes" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if *deletePosts {
|
||||
@@ -198,6 +309,7 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
||||
func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRepository, args []string) error {
|
||||
fs := flag.NewFlagSet("prune all", flag.ContinueOnError)
|
||||
dryRun := fs.Bool("dry-run", false, "preview what would be deleted without actually deleting")
|
||||
yes := fs.Bool("yes", false, "skip confirmation prompt")
|
||||
fs.SetOutput(os.Stderr)
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
@@ -214,6 +326,30 @@ func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRe
|
||||
return fmt.Errorf("get post count: %w", err)
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
if *dryRun {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_all",
|
||||
"dry_run": true,
|
||||
"user_count": len(userCount),
|
||||
"post_count": postCount,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if !*yes {
|
||||
return fmt.Errorf("confirmation required. Use --yes to skip prompt or --json for non-interactive mode")
|
||||
}
|
||||
totalDeleted, err := userRepo.HardDeleteAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("hard delete all: %w", err)
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "prune_all",
|
||||
"deleted_count": totalDeleted,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d users and %d posts to delete\n", len(userCount), postCount)
|
||||
|
||||
if *dryRun {
|
||||
@@ -221,15 +357,17 @@ func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRe
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("\nAre you sure you want to permanently delete ALL %d users and %d posts? (yes/no): ", len(userCount), postCount)
|
||||
var confirmation string
|
||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||
return fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
if !*yes {
|
||||
fmt.Printf("\nAre you sure you want to permanently delete ALL %d users and %d posts? (yes/no): ", len(userCount), postCount)
|
||||
var confirmation string
|
||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||
return fmt.Errorf("read confirmation: %w", err)
|
||||
}
|
||||
|
||||
if confirmation != "yes" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
if confirmation != "yes" {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
totalDeleted, err := userRepo.HardDeleteAll()
|
||||
|
||||
@@ -110,6 +110,16 @@ func TestPrunePosts(t *testing.T) {
|
||||
t.Errorf("prunePosts() with dry-run error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("prunePosts with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
err := prunePosts(postRepo, []string{"--dry-run"})
|
||||
if err != nil {
|
||||
t.Errorf("prunePosts() with dry-run and JSON output error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
post1 := database.Post{
|
||||
ID: 1,
|
||||
Title: "Post by deleted user 1",
|
||||
@@ -138,6 +148,16 @@ func TestPrunePosts(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("prunePosts() with dry-run error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("prunePosts with JSON output and mock data", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
err := prunePosts(postRepo, []string{"--dry-run"})
|
||||
if err != nil {
|
||||
t.Errorf("prunePosts() with dry-run and JSON output error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPruneAll(t *testing.T) {
|
||||
@@ -175,6 +195,16 @@ func TestPruneAll(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("pruneAll() with dry-run error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("pruneAll with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
err := pruneAll(userRepo, postRepo, []string{"--dry-run"})
|
||||
if err != nil {
|
||||
t.Errorf("pruneAll() with dry-run and JSON output error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrunePostsWithError(t *testing.T) {
|
||||
|
||||
@@ -69,54 +69,91 @@ func seedDatabase(userRepo repositories.UserRepository, postRepo repositories.Po
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Starting database seeding...")
|
||||
if !IsJSONOutput() {
|
||||
fmt.Println("Starting database seeding...")
|
||||
}
|
||||
|
||||
spinner := NewSpinner("Creating seed user")
|
||||
spinner.Spin()
|
||||
if !IsJSONOutput() {
|
||||
spinner.Spin()
|
||||
}
|
||||
|
||||
seedUser, err := ensureSeedUser(userRepo)
|
||||
if err != nil {
|
||||
spinner.Complete()
|
||||
if !IsJSONOutput() {
|
||||
spinner.Complete()
|
||||
}
|
||||
return fmt.Errorf("ensure seed user: %w", err)
|
||||
}
|
||||
spinner.Complete()
|
||||
|
||||
fmt.Printf("Seed user ready: ID=%d Username=%s\n", seedUser.ID, seedUser.Username)
|
||||
if !IsJSONOutput() {
|
||||
spinner.Complete()
|
||||
fmt.Printf("Seed user ready: ID=%d Username=%s\n", seedUser.ID, seedUser.Username)
|
||||
}
|
||||
|
||||
processor := NewParallelProcessor()
|
||||
|
||||
progress := NewProgressIndicator(*numUsers, "Creating users (parallel)")
|
||||
var progress *ProgressIndicator
|
||||
if !IsJSONOutput() {
|
||||
progress = NewProgressIndicator(*numUsers, "Creating users (parallel)")
|
||||
}
|
||||
users, err := processor.CreateUsersInParallel(userRepo, *numUsers, progress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create random users: %w", err)
|
||||
}
|
||||
progress.Complete()
|
||||
if !IsJSONOutput() && progress != nil {
|
||||
progress.Complete()
|
||||
}
|
||||
|
||||
allUsers := append([]database.User{*seedUser}, users...)
|
||||
|
||||
progress = NewProgressIndicator(*numPosts, "Creating posts (parallel)")
|
||||
if !IsJSONOutput() {
|
||||
progress = NewProgressIndicator(*numPosts, "Creating posts (parallel)")
|
||||
}
|
||||
posts, err := processor.CreatePostsInParallel(postRepo, seedUser.ID, *numPosts, progress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create random posts: %w", err)
|
||||
}
|
||||
progress.Complete()
|
||||
if !IsJSONOutput() && progress != nil {
|
||||
progress.Complete()
|
||||
}
|
||||
|
||||
progress = NewProgressIndicator(len(posts), "Creating votes (parallel)")
|
||||
if !IsJSONOutput() {
|
||||
progress = NewProgressIndicator(len(posts), "Creating votes (parallel)")
|
||||
}
|
||||
votes, err := processor.CreateVotesInParallel(voteRepo, allUsers, posts, *votesPerPost, progress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create random votes: %w", err)
|
||||
}
|
||||
progress.Complete()
|
||||
if !IsJSONOutput() && progress != nil {
|
||||
progress.Complete()
|
||||
}
|
||||
|
||||
progress = NewProgressIndicator(len(posts), "Updating scores (parallel)")
|
||||
if !IsJSONOutput() {
|
||||
progress = NewProgressIndicator(len(posts), "Updating scores (parallel)")
|
||||
}
|
||||
err = processor.UpdatePostScoresInParallel(postRepo, voteRepo, posts, progress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update post scores: %w", err)
|
||||
}
|
||||
progress.Complete()
|
||||
if !IsJSONOutput() && progress != nil {
|
||||
progress.Complete()
|
||||
}
|
||||
|
||||
fmt.Println("Database seeding completed successfully!")
|
||||
fmt.Printf("Created %d users, %d posts, and %d votes\n", len(allUsers), len(posts), votes)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "seed_completed",
|
||||
"users": len(allUsers),
|
||||
"posts": len(posts),
|
||||
"votes": votes,
|
||||
"seed_user": map[string]interface{}{
|
||||
"id": seedUser.ID,
|
||||
"username": seedUser.Username,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
fmt.Println("Database seeding completed successfully!")
|
||||
fmt.Printf("Created %d users, %d posts, and %d votes\n", len(allUsers), len(posts), votes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func userCreate(cfg *config.Config, repo repositories.UserRepository, args []str
|
||||
|
||||
auditLogger, err := NewAuditLogger(cfg.LogDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Could not initialize audit logging: %v\n", err)
|
||||
outputWarning("Could not initialize audit logging: %v", err)
|
||||
auditLogger = nil
|
||||
}
|
||||
|
||||
@@ -168,7 +168,16 @@ func userCreate(cfg *config.Config, repo repositories.UserRepository, args []str
|
||||
auditLogger.LogUserCreation(user.ID, user.Username, user.Email, true, nil)
|
||||
}
|
||||
|
||||
fmt.Printf("User created: %s (%s)\n", user.Username, user.Email)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_created",
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("User created: %s (%s)\n", user.Username, user.Email)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -286,7 +295,16 @@ func userUpdate(cfg *config.Config, repo repositories.UserRepository, refreshTok
|
||||
return handleDatabaseConstraintError(err)
|
||||
}
|
||||
|
||||
fmt.Printf("User updated: %s (%s)\n", user.Username, user.Email)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_updated",
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("User updated: %s (%s)\n", user.Username, user.Email)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -383,16 +401,13 @@ func userDelete(cfg *config.Config, repo repositories.UserRepository, args []str
|
||||
}
|
||||
|
||||
var deleteErr error
|
||||
var postsDeleted bool
|
||||
if *deletePosts {
|
||||
deleteErr = repo.HardDelete(uint(id))
|
||||
if deleteErr == nil {
|
||||
fmt.Printf("User deleted: ID=%d (posts also deleted)\n", id)
|
||||
}
|
||||
postsDeleted = true
|
||||
} else {
|
||||
deleteErr = repo.SoftDeleteWithPosts(uint(id))
|
||||
if deleteErr == nil {
|
||||
fmt.Printf("User deleted: ID=%d (posts kept)\n", id)
|
||||
}
|
||||
postsDeleted = false
|
||||
}
|
||||
|
||||
if deleteErr != nil {
|
||||
@@ -402,10 +417,30 @@ func userDelete(cfg *config.Config, repo repositories.UserRepository, args []str
|
||||
emailSender := services.NewSMTPSenderWithTimeout(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.From, cfg.SMTP.Timeout)
|
||||
subject, body := services.GenerateAdminAccountDeletionNotificationEmail(user.Username, cfg.App.AdminEmail, cfg.App.BaseURL, cfg.App.Title, *deletePosts)
|
||||
|
||||
emailSent := true
|
||||
if err := emailSender.Send(user.Email, subject, body); err != nil {
|
||||
fmt.Printf("Warning: Could not send notification email to %s: %v\n", user.Email, err)
|
||||
outputWarning("Could not send notification email to %s: %v", user.Email, err)
|
||||
emailSent = false
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_deleted",
|
||||
"id": id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"posts_deleted": postsDeleted,
|
||||
"email_sent": emailSent,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||
if postsDeleted {
|
||||
fmt.Printf("User deleted: ID=%d (posts also deleted)\n", id)
|
||||
} else {
|
||||
fmt.Printf("User deleted: ID=%d (posts kept)\n", id)
|
||||
}
|
||||
if emailSent {
|
||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -426,6 +461,31 @@ func userList(repo repositories.UserRepository, args []string) error {
|
||||
return fmt.Errorf("list users: %w", err)
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
type userJSON struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Locked bool `json:"locked"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
usersJSON := make([]userJSON, len(users))
|
||||
for i, u := range users {
|
||||
usersJSON[i] = userJSON{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Locked: u.Locked,
|
||||
CreatedAt: u.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
outputJSON(map[string]interface{}{
|
||||
"users": usersJSON,
|
||||
"count": len(usersJSON),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
fmt.Println("No users found")
|
||||
return nil
|
||||
@@ -517,7 +577,16 @@ func userLock(cfg *config.Config, repo repositories.UserRepository, args []strin
|
||||
}
|
||||
|
||||
if user.Locked {
|
||||
fmt.Printf("User is already locked: %s\n", user.Username)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_lock",
|
||||
"id": id,
|
||||
"username": user.Username,
|
||||
"status": "already_locked",
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("User is already locked: %s\n", user.Username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -525,15 +594,27 @@ func userLock(cfg *config.Config, repo repositories.UserRepository, args []strin
|
||||
return fmt.Errorf("lock user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User locked: %s\n", user.Username)
|
||||
|
||||
emailSender := services.NewSMTPSenderWithTimeout(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.From, cfg.SMTP.Timeout)
|
||||
subject, body := services.GenerateAccountLockNotificationEmail(user.Username, cfg.App.AdminEmail, cfg.App.Title)
|
||||
|
||||
emailSent := true
|
||||
if err := emailSender.Send(user.Email, subject, body); err != nil {
|
||||
fmt.Printf("Warning: Could not send notification email to %s: %v\n", user.Email, err)
|
||||
outputWarning("Could not send notification email to %s: %v", user.Email, err)
|
||||
emailSent = false
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_locked",
|
||||
"id": id,
|
||||
"username": user.Username,
|
||||
"email_sent": emailSent,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||
fmt.Printf("User locked: %s\n", user.Username)
|
||||
if emailSent {
|
||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -571,7 +652,16 @@ func userUnlock(cfg *config.Config, repo repositories.UserRepository, args []str
|
||||
}
|
||||
|
||||
if !user.Locked {
|
||||
fmt.Printf("User is already unlocked: %s\n", user.Username)
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_unlock",
|
||||
"id": id,
|
||||
"username": user.Username,
|
||||
"status": "already_unlocked",
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("User is already unlocked: %s\n", user.Username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -579,15 +669,27 @@ func userUnlock(cfg *config.Config, repo repositories.UserRepository, args []str
|
||||
return fmt.Errorf("unlock user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User unlocked: %s\n", user.Username)
|
||||
|
||||
emailSender := services.NewSMTPSenderWithTimeout(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.From, cfg.SMTP.Timeout)
|
||||
subject, body := services.GenerateAccountUnlockNotificationEmail(user.Username, cfg.App.AdminEmail, cfg.App.BaseURL, cfg.App.Title)
|
||||
|
||||
emailSent := true
|
||||
if err := emailSender.Send(user.Email, subject, body); err != nil {
|
||||
fmt.Printf("Warning: Could not send notification email to %s: %v\n", user.Email, err)
|
||||
outputWarning("Could not send notification email to %s: %v", user.Email, err)
|
||||
emailSent = false
|
||||
}
|
||||
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "user_unlocked",
|
||||
"id": id,
|
||||
"username": user.Username,
|
||||
"email_sent": emailSent,
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||
fmt.Printf("User unlocked: %s\n", user.Username)
|
||||
if emailSent {
|
||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -629,8 +731,18 @@ func resetUserPassword(cfg *config.Config, repo repositories.UserRepository, ses
|
||||
return fmt.Errorf("send password reset email: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Password reset for user %s: Temporary password sent to %s\n", user.Username, user.Email)
|
||||
fmt.Printf("⚠️ User must change this password on next login!\n")
|
||||
if IsJSONOutput() {
|
||||
outputJSON(map[string]interface{}{
|
||||
"action": "password_reset",
|
||||
"id": userID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"message": "Temporary password sent. User must change password on next login.",
|
||||
})
|
||||
} else {
|
||||
fmt.Printf("Password reset for user %s: Temporary password sent to %s\n", user.Username, user.Email)
|
||||
fmt.Printf("⚠️ User must change this password on next login!\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -81,6 +81,22 @@ func TestUserCreate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful creation with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
mockRepo := testutils.NewMockUserRepository()
|
||||
err := userCreate(cfg, mockRepo, []string{
|
||||
"--username", "testuser",
|
||||
"--email", "test@example.com",
|
||||
"--password", "StrongPass123!",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing username", func(t *testing.T) {
|
||||
mockRepo := testutils.NewMockUserRepository()
|
||||
err := userCreate(cfg, mockRepo, []string{
|
||||
@@ -239,6 +255,22 @@ func TestUserUpdate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful update username with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
cfg := &config.Config{}
|
||||
mockRefreshRepo := &mockRefreshTokenRepo{}
|
||||
err := userUpdate(cfg, mockRepo, mockRefreshRepo, []string{
|
||||
"1",
|
||||
"--username", "newusername",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful update email", func(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
mockRefreshRepo := &mockRefreshTokenRepo{}
|
||||
@@ -351,6 +383,25 @@ func TestUserDelete(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful delete with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
testUser := &database.User{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Password: "hashedpassword",
|
||||
}
|
||||
freshMockRepo := testutils.NewMockUserRepository()
|
||||
_ = freshMockRepo.Create(testUser)
|
||||
|
||||
err := userDelete(cfg, freshMockRepo, []string{"1"})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful delete with posts", func(t *testing.T) {
|
||||
testUser2 := &database.User{
|
||||
Username: "testuser2",
|
||||
@@ -459,6 +510,17 @@ func TestUserList(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list all users with JSON output", func(t *testing.T) {
|
||||
SetJSONOutput(true)
|
||||
defer SetJSONOutput(false)
|
||||
|
||||
err := userList(mockRepo, []string{})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list with limit", func(t *testing.T) {
|
||||
err := userList(mockRepo, []string{"--limit", "1"})
|
||||
|
||||
|
||||
@@ -172,11 +172,12 @@ func FuzzRunCommandHandler(f *testing.F) {
|
||||
|
||||
err := handleRunCommand(cfg, args)
|
||||
|
||||
if len(args) > 0 && args[0] == "--help" {
|
||||
switch {
|
||||
case len(args) > 0 && args[0] == "--help":
|
||||
if err != nil {
|
||||
t.Logf("Help flag should not error, got: %v", err)
|
||||
}
|
||||
} else if len(args) > 0 {
|
||||
case len(args) > 0:
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
@@ -190,7 +191,7 @@ func FuzzRunCommandHandler(f *testing.F) {
|
||||
if !strings.Contains(errMsg, "unexpected arguments") {
|
||||
t.Logf("Got error (may be acceptable for server setup): %v", err)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
if err != nil && strings.Contains(err.Error(), "unexpected arguments") {
|
||||
t.Fatalf("Empty args should not trigger 'unexpected arguments' error: %v", err)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ func run(args []string) error {
|
||||
rootFS.SetOutput(os.Stderr)
|
||||
rootFS.Usage = printRootUsage
|
||||
showHelp := rootFS.Bool("help", false, "show this help message")
|
||||
jsonOutput := rootFS.Bool("json", false, "output results in JSON format")
|
||||
|
||||
if err := rootFS.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
@@ -80,6 +81,8 @@ func run(args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
commands.SetJSONOutput(*jsonOutput)
|
||||
|
||||
remaining := rootFS.Args()
|
||||
if len(remaining) == 0 {
|
||||
printRootUsage()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
@@ -95,7 +96,12 @@ func TestServerConfigurationFromConfig(t *testing.T) {
|
||||
testServer := httptest.NewServer(srv.Handler)
|
||||
defer testServer.Close()
|
||||
|
||||
resp, err := http.Get(testServer.URL + "/health")
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, testServer.URL+"/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
@@ -157,8 +163,9 @@ func TestTLSWiringFromConfig(t *testing.T) {
|
||||
|
||||
expectedAddr := cfg.Server.Host + ":" + cfg.Server.Port
|
||||
srv := &http.Server{
|
||||
Addr: expectedAddr,
|
||||
Handler: router,
|
||||
Addr: expectedAddr,
|
||||
Handler: router,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
if srv.Addr != expectedAddr {
|
||||
@@ -201,7 +208,12 @@ func TestTLSWiringFromConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(testServer.URL + "/health")
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, testServer.URL+"/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make TLS request: %v", err)
|
||||
}
|
||||
@@ -215,10 +227,8 @@ func TestTLSWiringFromConfig(t *testing.T) {
|
||||
|
||||
if resp.TLS == nil {
|
||||
t.Error("Expected TLS connection info to be present in response")
|
||||
} else {
|
||||
if resp.TLS.Version < tls.VersionTLS12 {
|
||||
t.Errorf("Expected TLS version 1.2 or higher, got %x", resp.TLS.Version)
|
||||
}
|
||||
} else if resp.TLS.Version < tls.VersionTLS12 {
|
||||
t.Errorf("Expected TLS version 1.2 or higher, got %x", resp.TLS.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,7 +368,12 @@ func TestServerInitializationFlow(t *testing.T) {
|
||||
testServer := httptest.NewServer(srv.Handler)
|
||||
defer testServer.Close()
|
||||
|
||||
resp, err := http.Get(testServer.URL + "/health")
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, testServer.URL+"/health", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
@@ -370,7 +385,12 @@ func TestServerInitializationFlow(t *testing.T) {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
resp, err = http.Get(testServer.URL + "/api")
|
||||
req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, testServer.URL+"/api", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user