feat(cli): add a json output and tests
This commit is contained in:
23
README.md
23
README.md
@@ -251,6 +251,29 @@ Goyco includes a comprehensive CLI for administration:
|
|||||||
./bin/goyco prune all # Hard delete all users and posts
|
./bin/goyco prune all # Hard delete all users and posts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JSON Output
|
||||||
|
|
||||||
|
All CLI commands support JSON output for easier parsing and integration with scripts. Use the `--json` flag to enable structured JSON output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get JSON output
|
||||||
|
./bin/goyco --json user list
|
||||||
|
./bin/goyco --json post list
|
||||||
|
./bin/goyco --json status
|
||||||
|
|
||||||
|
# Example: Parse JSON output with jq
|
||||||
|
./bin/goyco --json user list | jq '.users[0].username'
|
||||||
|
./bin/goyco --json status | jq '.status'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note for destructive operations**: When using `--json` with `prune` commands, you must also use the `--yes` flag to skip interactive confirmation prompts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/goyco --json prune posts --yes
|
||||||
|
./bin/goyco --json prune users --yes --with-posts
|
||||||
|
./bin/goyco --json prune all --yes
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Get the sources
|
### Get the sources
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"goyco/cmd/goyco/commands"
|
"goyco/cmd/goyco/commands"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadDotEnv() {
|
func loadDotEnv() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -16,6 +17,23 @@ var ErrHelpRequested = errors.New("help requested")
|
|||||||
|
|
||||||
type DBConnector func(cfg *config.Config) (*gorm.DB, func() error, error)
|
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 (
|
var (
|
||||||
dbConnectorMu sync.RWMutex
|
dbConnectorMu sync.RWMutex
|
||||||
currentDBConnector = defaultDBConnector
|
currentDBConnector = defaultDBConnector
|
||||||
@@ -93,3 +111,39 @@ func truncate(in string, max int) string {
|
|||||||
}
|
}
|
||||||
return in[:max-3] + "..."
|
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)
|
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")
|
pidFile := filepath.Join(pidDir, "goyco.pid")
|
||||||
|
|
||||||
if !isDaemonRunning(pidFile) {
|
if !isDaemonRunning(pidFile) {
|
||||||
|
if IsJSONOutput() {
|
||||||
|
outputJSON(map[string]interface{}{
|
||||||
|
"status": "not_running",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
fmt.Println("Goyco is not running")
|
fmt.Println("Goyco is not running")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(pidFile)
|
data, err := os.ReadFile(pidFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
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)
|
fmt.Printf("Goyco is running (PID file exists but cannot be read: %v)\n", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pid, err := strconv.Atoi(string(data))
|
pid, err := strconv.Atoi(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
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)
|
fmt.Printf("Goyco is running (PID file exists but contains invalid PID: %v)\n", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if IsJSONOutput() {
|
||||||
|
outputJSON(map[string]interface{}{
|
||||||
|
"status": "running",
|
||||||
|
"pid": pid,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
fmt.Printf("Goyco is running (PID %d)\n", pid)
|
fmt.Printf("Goyco is running (PID %d)\n", pid)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +170,14 @@ func stopDaemon(cfg *config.Config) error {
|
|||||||
|
|
||||||
_ = os.Remove(pidFile)
|
_ = os.Remove(pidFile)
|
||||||
|
|
||||||
|
if IsJSONOutput() {
|
||||||
|
outputJSON(map[string]interface{}{
|
||||||
|
"action": "stopped",
|
||||||
|
"pid": pid,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
fmt.Printf("Goyco stopped (PID %d)\n", pid)
|
fmt.Printf("Goyco stopped (PID %d)\n", pid)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,9 +218,18 @@ func runDaemon(cfg *config.Config) error {
|
|||||||
if err := writePIDFile(pidFile, pid); err != nil {
|
if err := writePIDFile(pidFile, pid); err != nil {
|
||||||
return fmt.Errorf("cannot write PID file: %w", err)
|
return fmt.Errorf("cannot write PID file: %w", err)
|
||||||
}
|
}
|
||||||
|
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("Goyco started with PID %d\n", pid)
|
||||||
fmt.Printf("PID file: %s\n", pidFile)
|
fmt.Printf("PID file: %s\n", pidFile)
|
||||||
fmt.Printf("Log file: %s\n", logFile)
|
fmt.Printf("Log file: %s\n", logFile)
|
||||||
|
}
|
||||||
return nil
|
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) {
|
t.Run("daemon running with valid PID", func(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
cfg.PIDDir = 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) {
|
t.Run("daemon running with invalid PID file", func(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
cfg.PIDDir = tempDir
|
cfg.PIDDir = tempDir
|
||||||
|
|||||||
@@ -30,11 +30,20 @@ func HandleMigrateCommand(cfg *config.Config, name string, args []string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runMigrateCommand(db *gorm.DB) error {
|
func runMigrateCommand(db *gorm.DB) error {
|
||||||
|
if !IsJSONOutput() {
|
||||||
fmt.Println("Running database migrations...")
|
fmt.Println("Running database migrations...")
|
||||||
|
}
|
||||||
if err := database.Migrate(db); err != nil {
|
if err := database.Migrate(db); err != nil {
|
||||||
return fmt.Errorf("run migrations: %w", err)
|
return fmt.Errorf("run migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
if IsJSONOutput() {
|
||||||
|
outputJSON(map[string]interface{}{
|
||||||
|
"action": "migrations_applied",
|
||||||
|
"status": "success",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
fmt.Println("Migrations applied successfully")
|
fmt.Println("Migrations applied successfully")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,18 @@ func TestHandleMigrateCommand(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error running migrations: %v", err)
|
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)
|
return fmt.Errorf("delete post: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if IsJSONOutput() {
|
||||||
|
outputJSON(map[string]interface{}{
|
||||||
|
"action": "post_deleted",
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
fmt.Printf("Post deleted: ID=%d\n", id)
|
fmt.Printf("Post deleted: ID=%d\n", id)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +133,38 @@ func postList(postQueries *services.PostQueries, args []string) error {
|
|||||||
return fmt.Errorf("list posts: %w", err)
|
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 {
|
if len(posts) == 0 {
|
||||||
fmt.Println("No posts found")
|
fmt.Println("No posts found")
|
||||||
return nil
|
return nil
|
||||||
@@ -234,6 +273,39 @@ func postSearch(postQueries *services.PostQueries, args []string) error {
|
|||||||
return fmt.Errorf("search posts: %w", err)
|
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 {
|
if len(posts) == 0 {
|
||||||
fmt.Println("No posts found matching your search")
|
fmt.Println("No posts found matching your search")
|
||||||
return nil
|
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) {
|
t.Run("missing id", func(t *testing.T) {
|
||||||
err := postDelete(mockRepo, []string{})
|
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) {
|
t.Run("list with limit", func(t *testing.T) {
|
||||||
err := postList(postQueries, []string{"--limit", "1"})
|
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) {
|
t.Run("case insensitive search", func(t *testing.T) {
|
||||||
mockRepo.SearchCalls = nil
|
mockRepo.SearchCalls = nil
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func printPruneUsage() {
|
|||||||
func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
||||||
fs := flag.NewFlagSet("prune posts", flag.ContinueOnError)
|
fs := flag.NewFlagSet("prune posts", flag.ContinueOnError)
|
||||||
dryRun := fs.Bool("dry-run", false, "preview what would be deleted without actually deleting")
|
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)
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
if err := fs.Parse(args); err != nil {
|
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)
|
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 {
|
if len(posts) == 0 {
|
||||||
fmt.Println("No posts found for deleted users")
|
fmt.Println("No posts found for deleted users")
|
||||||
return nil
|
return nil
|
||||||
@@ -93,6 +137,7 @@ func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !*yes {
|
||||||
fmt.Printf("\nAre you sure you want to permanently delete %d posts? (yes/no): ", len(posts))
|
fmt.Printf("\nAre you sure you want to permanently delete %d posts? (yes/no): ", len(posts))
|
||||||
var confirmation string
|
var confirmation string
|
||||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||||
@@ -103,6 +148,7 @@ func prunePosts(postRepo repositories.PostRepository, args []string) error {
|
|||||||
fmt.Println("Operation cancelled")
|
fmt.Println("Operation cancelled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deletedCount, err := postRepo.HardDeletePostsByDeletedUsers()
|
deletedCount, err := postRepo.HardDeletePostsByDeletedUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,6 +163,7 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
|||||||
fs := flag.NewFlagSet("prune users", flag.ContinueOnError)
|
fs := flag.NewFlagSet("prune users", flag.ContinueOnError)
|
||||||
dryRun := fs.Bool("dry-run", false, "preview what would be deleted without actually deleting")
|
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")
|
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)
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
@@ -130,7 +177,14 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
|||||||
|
|
||||||
userCount := len(users)
|
userCount := len(users)
|
||||||
if userCount == 0 {
|
if userCount == 0 {
|
||||||
|
if IsJSONOutput() {
|
||||||
|
outputJSON(map[string]interface{}{
|
||||||
|
"action": "prune_users",
|
||||||
|
"count": 0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
fmt.Println("No users found to delete")
|
fmt.Println("No users found to delete")
|
||||||
|
}
|
||||||
return nil
|
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)
|
fmt.Printf("Found %d users", userCount)
|
||||||
if *deletePosts {
|
if *deletePosts {
|
||||||
fmt.Printf(" and %d posts", postCount)
|
fmt.Printf(" and %d posts", postCount)
|
||||||
@@ -158,6 +267,7 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !*yes {
|
||||||
confirmMsg := fmt.Sprintf("\nAre you sure you want to permanently delete %d users", userCount)
|
confirmMsg := fmt.Sprintf("\nAre you sure you want to permanently delete %d users", userCount)
|
||||||
if *deletePosts {
|
if *deletePosts {
|
||||||
confirmMsg += fmt.Sprintf(" and %d posts", postCount)
|
confirmMsg += fmt.Sprintf(" and %d posts", postCount)
|
||||||
@@ -174,6 +284,7 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
|||||||
fmt.Println("Operation cancelled")
|
fmt.Println("Operation cancelled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if *deletePosts {
|
if *deletePosts {
|
||||||
totalDeleted, err := userRepo.HardDeleteAll()
|
totalDeleted, err := userRepo.HardDeleteAll()
|
||||||
@@ -198,6 +309,7 @@ func pruneUsers(userRepo repositories.UserRepository, postRepo repositories.Post
|
|||||||
func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRepository, args []string) error {
|
func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRepository, args []string) error {
|
||||||
fs := flag.NewFlagSet("prune all", flag.ContinueOnError)
|
fs := flag.NewFlagSet("prune all", flag.ContinueOnError)
|
||||||
dryRun := fs.Bool("dry-run", false, "preview what would be deleted without actually deleting")
|
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)
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
if err := fs.Parse(args); err != nil {
|
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)
|
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)
|
fmt.Printf("Found %d users and %d posts to delete\n", len(userCount), postCount)
|
||||||
|
|
||||||
if *dryRun {
|
if *dryRun {
|
||||||
@@ -221,6 +357,7 @@ func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !*yes {
|
||||||
fmt.Printf("\nAre you sure you want to permanently delete ALL %d users and %d posts? (yes/no): ", len(userCount), postCount)
|
fmt.Printf("\nAre you sure you want to permanently delete ALL %d users and %d posts? (yes/no): ", len(userCount), postCount)
|
||||||
var confirmation string
|
var confirmation string
|
||||||
if _, err := fmt.Scanln(&confirmation); err != nil {
|
if _, err := fmt.Scanln(&confirmation); err != nil {
|
||||||
@@ -231,6 +368,7 @@ func pruneAll(userRepo repositories.UserRepository, postRepo repositories.PostRe
|
|||||||
fmt.Println("Operation cancelled")
|
fmt.Println("Operation cancelled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
totalDeleted, err := userRepo.HardDeleteAll()
|
totalDeleted, err := userRepo.HardDeleteAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ func TestPrunePosts(t *testing.T) {
|
|||||||
t.Errorf("prunePosts() with dry-run error = %v", err)
|
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{
|
post1 := database.Post{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Title: "Post by deleted user 1",
|
Title: "Post by deleted user 1",
|
||||||
@@ -138,6 +148,16 @@ func TestPrunePosts(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("prunePosts() with dry-run error = %v", err)
|
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) {
|
func TestPruneAll(t *testing.T) {
|
||||||
@@ -175,6 +195,16 @@ func TestPruneAll(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("pruneAll() with dry-run error = %v", err)
|
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) {
|
func TestPrunePostsWithError(t *testing.T) {
|
||||||
|
|||||||
@@ -69,54 +69,91 @@ func seedDatabase(userRepo repositories.UserRepository, postRepo repositories.Po
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !IsJSONOutput() {
|
||||||
fmt.Println("Starting database seeding...")
|
fmt.Println("Starting database seeding...")
|
||||||
|
}
|
||||||
|
|
||||||
spinner := NewSpinner("Creating seed user")
|
spinner := NewSpinner("Creating seed user")
|
||||||
|
if !IsJSONOutput() {
|
||||||
spinner.Spin()
|
spinner.Spin()
|
||||||
|
}
|
||||||
|
|
||||||
seedUser, err := ensureSeedUser(userRepo)
|
seedUser, err := ensureSeedUser(userRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !IsJSONOutput() {
|
||||||
spinner.Complete()
|
spinner.Complete()
|
||||||
|
}
|
||||||
return fmt.Errorf("ensure seed user: %w", err)
|
return fmt.Errorf("ensure seed user: %w", err)
|
||||||
}
|
}
|
||||||
|
if !IsJSONOutput() {
|
||||||
spinner.Complete()
|
spinner.Complete()
|
||||||
|
|
||||||
fmt.Printf("Seed user ready: ID=%d Username=%s\n", seedUser.ID, seedUser.Username)
|
fmt.Printf("Seed user ready: ID=%d Username=%s\n", seedUser.ID, seedUser.Username)
|
||||||
|
}
|
||||||
|
|
||||||
processor := NewParallelProcessor()
|
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)
|
users, err := processor.CreateUsersInParallel(userRepo, *numUsers, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create random users: %w", err)
|
return fmt.Errorf("create random users: %w", err)
|
||||||
}
|
}
|
||||||
|
if !IsJSONOutput() && progress != nil {
|
||||||
progress.Complete()
|
progress.Complete()
|
||||||
|
}
|
||||||
|
|
||||||
allUsers := append([]database.User{*seedUser}, users...)
|
allUsers := append([]database.User{*seedUser}, users...)
|
||||||
|
|
||||||
|
if !IsJSONOutput() {
|
||||||
progress = NewProgressIndicator(*numPosts, "Creating posts (parallel)")
|
progress = NewProgressIndicator(*numPosts, "Creating posts (parallel)")
|
||||||
|
}
|
||||||
posts, err := processor.CreatePostsInParallel(postRepo, seedUser.ID, *numPosts, progress)
|
posts, err := processor.CreatePostsInParallel(postRepo, seedUser.ID, *numPosts, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create random posts: %w", err)
|
return fmt.Errorf("create random posts: %w", err)
|
||||||
}
|
}
|
||||||
|
if !IsJSONOutput() && progress != nil {
|
||||||
progress.Complete()
|
progress.Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsJSONOutput() {
|
||||||
progress = NewProgressIndicator(len(posts), "Creating votes (parallel)")
|
progress = NewProgressIndicator(len(posts), "Creating votes (parallel)")
|
||||||
|
}
|
||||||
votes, err := processor.CreateVotesInParallel(voteRepo, allUsers, posts, *votesPerPost, progress)
|
votes, err := processor.CreateVotesInParallel(voteRepo, allUsers, posts, *votesPerPost, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create random votes: %w", err)
|
return fmt.Errorf("create random votes: %w", err)
|
||||||
}
|
}
|
||||||
|
if !IsJSONOutput() && progress != nil {
|
||||||
progress.Complete()
|
progress.Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsJSONOutput() {
|
||||||
progress = NewProgressIndicator(len(posts), "Updating scores (parallel)")
|
progress = NewProgressIndicator(len(posts), "Updating scores (parallel)")
|
||||||
|
}
|
||||||
err = processor.UpdatePostScoresInParallel(postRepo, voteRepo, posts, progress)
|
err = processor.UpdatePostScoresInParallel(postRepo, voteRepo, posts, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update post scores: %w", err)
|
return fmt.Errorf("update post scores: %w", err)
|
||||||
}
|
}
|
||||||
|
if !IsJSONOutput() && progress != nil {
|
||||||
progress.Complete()
|
progress.Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Println("Database seeding completed successfully!")
|
||||||
fmt.Printf("Created %d users, %d posts, and %d votes\n", len(allUsers), len(posts), votes)
|
fmt.Printf("Created %d users, %d posts, and %d votes\n", len(allUsers), len(posts), votes)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func userCreate(cfg *config.Config, repo repositories.UserRepository, args []str
|
|||||||
|
|
||||||
auditLogger, err := NewAuditLogger(cfg.LogDir)
|
auditLogger, err := NewAuditLogger(cfg.LogDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: Could not initialize audit logging: %v\n", err)
|
outputWarning("Could not initialize audit logging: %v", err)
|
||||||
auditLogger = nil
|
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)
|
auditLogger.LogUserCreation(user.ID, user.Username, user.Email, true, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
fmt.Printf("User created: %s (%s)\n", user.Username, user.Email)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +295,16 @@ func userUpdate(cfg *config.Config, repo repositories.UserRepository, refreshTok
|
|||||||
return handleDatabaseConstraintError(err)
|
return handleDatabaseConstraintError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
fmt.Printf("User updated: %s (%s)\n", user.Username, user.Email)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,16 +401,13 @@ func userDelete(cfg *config.Config, repo repositories.UserRepository, args []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
var deleteErr error
|
var deleteErr error
|
||||||
|
var postsDeleted bool
|
||||||
if *deletePosts {
|
if *deletePosts {
|
||||||
deleteErr = repo.HardDelete(uint(id))
|
deleteErr = repo.HardDelete(uint(id))
|
||||||
if deleteErr == nil {
|
postsDeleted = true
|
||||||
fmt.Printf("User deleted: ID=%d (posts also deleted)\n", id)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
deleteErr = repo.SoftDeleteWithPosts(uint(id))
|
deleteErr = repo.SoftDeleteWithPosts(uint(id))
|
||||||
if deleteErr == nil {
|
postsDeleted = false
|
||||||
fmt.Printf("User deleted: ID=%d (posts kept)\n", id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if deleteErr != nil {
|
if deleteErr != nil {
|
||||||
@@ -402,11 +417,31 @@ 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)
|
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)
|
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 {
|
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 {
|
} else {
|
||||||
|
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)
|
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -426,6 +461,31 @@ func userList(repo repositories.UserRepository, args []string) error {
|
|||||||
return fmt.Errorf("list users: %w", err)
|
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 {
|
if len(users) == 0 {
|
||||||
fmt.Println("No users found")
|
fmt.Println("No users found")
|
||||||
return nil
|
return nil
|
||||||
@@ -517,7 +577,16 @@ func userLock(cfg *config.Config, repo repositories.UserRepository, args []strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user.Locked {
|
if user.Locked {
|
||||||
|
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)
|
fmt.Printf("User is already locked: %s\n", user.Username)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,16 +594,28 @@ func userLock(cfg *config.Config, repo repositories.UserRepository, args []strin
|
|||||||
return fmt.Errorf("lock user: %w", err)
|
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)
|
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)
|
subject, body := services.GenerateAccountLockNotificationEmail(user.Username, cfg.App.AdminEmail, cfg.App.Title)
|
||||||
|
|
||||||
|
emailSent := true
|
||||||
if err := emailSender.Send(user.Email, subject, body); err != nil {
|
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 {
|
} else {
|
||||||
|
fmt.Printf("User locked: %s\n", user.Username)
|
||||||
|
if emailSent {
|
||||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -571,7 +652,16 @@ func userUnlock(cfg *config.Config, repo repositories.UserRepository, args []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !user.Locked {
|
if !user.Locked {
|
||||||
|
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)
|
fmt.Printf("User is already unlocked: %s\n", user.Username)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,16 +669,28 @@ func userUnlock(cfg *config.Config, repo repositories.UserRepository, args []str
|
|||||||
return fmt.Errorf("unlock user: %w", err)
|
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)
|
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)
|
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 {
|
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 {
|
} else {
|
||||||
|
fmt.Printf("User unlocked: %s\n", user.Username)
|
||||||
|
if emailSent {
|
||||||
fmt.Printf("Notification email sent to %s\n", user.Email)
|
fmt.Printf("Notification email sent to %s\n", user.Email)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -629,8 +731,18 @@ func resetUserPassword(cfg *config.Config, repo repositories.UserRepository, ses
|
|||||||
return fmt.Errorf("send password reset email: %w", err)
|
return fmt.Errorf("send password reset email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("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")
|
fmt.Printf("⚠️ User must change this password on next login!\n")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
t.Run("missing username", func(t *testing.T) {
|
||||||
mockRepo := testutils.NewMockUserRepository()
|
mockRepo := testutils.NewMockUserRepository()
|
||||||
err := userCreate(cfg, mockRepo, []string{
|
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) {
|
t.Run("successful update email", func(t *testing.T) {
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
mockRefreshRepo := &mockRefreshTokenRepo{}
|
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) {
|
t.Run("successful delete with posts", func(t *testing.T) {
|
||||||
testUser2 := &database.User{
|
testUser2 := &database.User{
|
||||||
Username: "testuser2",
|
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) {
|
t.Run("list with limit", func(t *testing.T) {
|
||||||
err := userList(mockRepo, []string{"--limit", "1"})
|
err := userList(mockRepo, []string{"--limit", "1"})
|
||||||
|
|
||||||
|
|||||||
@@ -172,11 +172,12 @@ func FuzzRunCommandHandler(f *testing.F) {
|
|||||||
|
|
||||||
err := handleRunCommand(cfg, args)
|
err := handleRunCommand(cfg, args)
|
||||||
|
|
||||||
if len(args) > 0 && args[0] == "--help" {
|
switch {
|
||||||
|
case len(args) > 0 && args[0] == "--help":
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Help flag should not error, got: %v", err)
|
t.Logf("Help flag should not error, got: %v", err)
|
||||||
}
|
}
|
||||||
} else if len(args) > 0 {
|
case len(args) > 0:
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -190,7 +191,7 @@ func FuzzRunCommandHandler(f *testing.F) {
|
|||||||
if !strings.Contains(errMsg, "unexpected arguments") {
|
if !strings.Contains(errMsg, "unexpected arguments") {
|
||||||
t.Logf("Got error (may be acceptable for server setup): %v", err)
|
t.Logf("Got error (may be acceptable for server setup): %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
if err != nil && strings.Contains(err.Error(), "unexpected arguments") {
|
if err != nil && strings.Contains(err.Error(), "unexpected arguments") {
|
||||||
t.Fatalf("Empty args should not trigger 'unexpected arguments' error: %v", err)
|
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.SetOutput(os.Stderr)
|
||||||
rootFS.Usage = printRootUsage
|
rootFS.Usage = printRootUsage
|
||||||
showHelp := rootFS.Bool("help", false, "show this help message")
|
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 err := rootFS.Parse(args); err != nil {
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
@@ -80,6 +81,8 @@ func run(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commands.SetJSONOutput(*jsonOutput)
|
||||||
|
|
||||||
remaining := rootFS.Args()
|
remaining := rootFS.Args()
|
||||||
if len(remaining) == 0 {
|
if len(remaining) == 0 {
|
||||||
printRootUsage()
|
printRootUsage()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@@ -95,7 +96,12 @@ func TestServerConfigurationFromConfig(t *testing.T) {
|
|||||||
testServer := httptest.NewServer(srv.Handler)
|
testServer := httptest.NewServer(srv.Handler)
|
||||||
defer testServer.Close()
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to make request: %v", err)
|
t.Fatalf("Failed to make request: %v", err)
|
||||||
}
|
}
|
||||||
@@ -159,6 +165,7 @@ func TestTLSWiringFromConfig(t *testing.T) {
|
|||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: expectedAddr,
|
Addr: expectedAddr,
|
||||||
Handler: router,
|
Handler: router,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
if srv.Addr != expectedAddr {
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to make TLS request: %v", err)
|
t.Fatalf("Failed to make TLS request: %v", err)
|
||||||
}
|
}
|
||||||
@@ -215,13 +227,11 @@ func TestTLSWiringFromConfig(t *testing.T) {
|
|||||||
|
|
||||||
if resp.TLS == nil {
|
if resp.TLS == nil {
|
||||||
t.Error("Expected TLS connection info to be present in response")
|
t.Error("Expected TLS connection info to be present in response")
|
||||||
} else {
|
} else if resp.TLS.Version < tls.VersionTLS12 {
|
||||||
if resp.TLS.Version < tls.VersionTLS12 {
|
|
||||||
t.Errorf("Expected TLS version 1.2 or higher, got %x", resp.TLS.Version)
|
t.Errorf("Expected TLS version 1.2 or higher, got %x", resp.TLS.Version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigLoadingInCLI(t *testing.T) {
|
func TestConfigLoadingInCLI(t *testing.T) {
|
||||||
originalEnv := os.Environ()
|
originalEnv := os.Environ()
|
||||||
@@ -358,7 +368,12 @@ func TestServerInitializationFlow(t *testing.T) {
|
|||||||
testServer := httptest.NewServer(srv.Handler)
|
testServer := httptest.NewServer(srv.Handler)
|
||||||
defer testServer.Close()
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to make request: %v", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to make request: %v", err)
|
t.Fatalf("Failed to make request: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user