package app import ( "database/sql" "path/filepath" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" ) func TestExtractRequiredDatabaseDSN(t *testing.T) { dsn, args, err := extractRequiredDatabaseDSN([]string{"list", "--dsn", "db", "extra"}) if err != nil { t.Fatal(err) } if dsn != "db" || strings.Join(args, ",") != "list,extra" { t.Fatalf("dsn=%q args=%#v", dsn, args) } dsn, args, err = extractRequiredDatabaseDSN([]string{"--database-url=mysql", "view", "alice"}) if err != nil || dsn != "mysql" || len(args) != 2 || args[0] != "view" { t.Fatalf("dsn=%q args=%#v err=%v", dsn, args, err) } if _, _, err := extractRequiredDatabaseDSN([]string{"list"}); err == nil || !strings.Contains(err.Error(), "database URL") { t.Fatalf("expected missing dsn error, got %v", err) } if _, _, err := extractRequiredDatabaseDSN([]string{"--dsn"}); err == nil || !strings.Contains(err.Error(), "requires") { t.Fatalf("expected missing dsn value error, got %v", err) } } func TestParseServerUsersArgs(t *testing.T) { email, username, password, admin, err := parseServerUsersAddArgs([]string{"--email", "a@example.com", "--username", "alice", "--password", "password123", "--admin"}) if err != nil { t.Fatal(err) } if email != "a@example.com" || username != "alice" || password != "password123" || !admin { t.Fatalf("unexpected add parse: %q %q %q %v", email, username, password, admin) } ref, edit, err := parseServerUsersEditArgs([]string{"alice", "--email", "new@example.com", "--username", "newalice", "--password", "password456", "--admin", "false"}) if err != nil { t.Fatal(err) } if ref != "alice" || edit.Email == nil || *edit.Email != "new@example.com" || edit.Admin == nil || *edit.Admin { t.Fatalf("unexpected edit parse: ref=%q edit=%#v", ref, edit) } if _, _, err := parseServerUsersEditArgs([]string{"alice"}); err == nil || !strings.Contains(err.Error(), "no user changes") { t.Fatalf("expected no changes error, got %v", err) } ref, err = parseServerUsersRemoveArgs([]string{"alice", "--force"}) if err != nil || ref != "alice" { t.Fatalf("remove parse ref=%q err=%v", ref, err) } if _, err := parseServerUsersRemoveArgs([]string{"alice"}); err == nil || !strings.Contains(err.Error(), "--force") { t.Fatalf("expected force error, got %v", err) } } func TestParseFlexibleBool(t *testing.T) { for _, v := range []string{"true", "1", "yes", "y"} { got, err := parseFlexibleBool(v) if err != nil || !got { t.Fatalf("%q => %v %v", v, got, err) } } for _, v := range []string{"false", "0", "no", "n"} { got, err := parseFlexibleBool(v) if err != nil || got { t.Fatalf("%q => %v %v", v, got, err) } } if _, err := parseFlexibleBool("wat"); err == nil { t.Fatal("expected invalid bool") } } func TestServerUserWhere(t *testing.T) { where, args := serverUserWhere("42") if where != "id = ?" || args[0] != int64(42) { t.Fatalf("id where=%q args=%#v", where, args) } where, args = serverUserWhere("Alice@Example.com") if where != "email = ?" || args[0] != "alice@example.com" { t.Fatalf("email where=%q args=%#v", where, args) } where, args = serverUserWhere("Alice") if where != "username = ?" || args[0] != "alice" { t.Fatalf("username where=%q args=%#v", where, args) } } func TestServerUsersListViewAddEditRemove(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() now := time.Date(2026, 1, 2, 3, 4, 0, 0, time.UTC) mock.ExpectQuery("SELECT id, email, username, is_admin, created_at FROM users ORDER BY username").WillReturnRows(sqlmock.NewRows([]string{"id", "email", "username", "is_admin", "created_at"}).AddRow(int64(1), "alice@example.com", "alice", true, now)) out := captureStdout(t, func() { if err := serverUsersList(db); err != nil { t.Fatal(err) } }) if !strings.Contains(out, "alice") || !strings.Contains(out, "ADMIN") { t.Fatalf("unexpected list output: %s", out) } mock.ExpectQuery("SELECT id, email, username, is_admin, created_at FROM users WHERE username = ").WithArgs("alice").WillReturnRows(sqlmock.NewRows([]string{"id", "email", "username", "is_admin", "created_at"}).AddRow(int64(1), "alice@example.com", "alice", true, now)) out = captureStdout(t, func() { if err := serverUsersView(db, "Alice"); err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Username: alice") || !strings.Contains(out, "Admin: true") { t.Fatalf("unexpected view output: %s", out) } mock.ExpectExec("INSERT INTO users").WithArgs("bob@example.com", "bob", sqlmock.AnyArg(), false).WillReturnResult(sqlmock.NewResult(2, 1)) out = captureStdout(t, func() { if err := serverUsersAdd(db, "Bob@Example.com", "Bob", "password123", false); err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Created user bob") { t.Fatalf("unexpected add output: %s", out) } admin := false email := "new@example.com" edit := serverUserEdit{Email: &email, Admin: &admin} mock.ExpectExec("UPDATE users SET email = \\?, is_admin = \\? WHERE username = ").WithArgs("new@example.com", false, "alice").WillReturnResult(sqlmock.NewResult(0, 1)) out = captureStdout(t, func() { if err := serverUsersEditUser(db, "alice", edit); err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Updated user alice") { t.Fatalf("unexpected edit output: %s", out) } mock.ExpectExec("DELETE FROM users WHERE username = ").WithArgs("alice").WillReturnResult(sqlmock.NewResult(0, 1)) out = captureStdout(t, func() { if err := serverUsersRemove(db, "alice"); err != nil { t.Fatal(err) } }) if !strings.Contains(out, "Removed user alice") { t.Fatalf("unexpected remove output: %s", out) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestServerUsersEditValidationAndNotFound(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() badEmail := "bad" if err := serverUsersEditUser(db, "alice", serverUserEdit{Email: &badEmail}); err == nil || !strings.Contains(err.Error(), "invalid email") { t.Fatalf("expected invalid email, got %v", err) } shortPassword := "short" if err := serverUsersEditUser(db, "alice", serverUserEdit{Password: &shortPassword}); err == nil || !strings.Contains(err.Error(), "password") { t.Fatalf("expected password error, got %v", err) } admin := true mock.ExpectExec("UPDATE users SET is_admin = ").WithArgs(true, "missing").WillReturnResult(sqlmock.NewResult(0, 0)) if err := serverUsersEditUser(db, "missing", serverUserEdit{Admin: &admin}); err != sql.ErrNoRows { t.Fatalf("expected sql.ErrNoRows, got %v", err) } mock.ExpectExec("DELETE FROM users WHERE username = ").WithArgs("missing").WillReturnResult(sqlmock.NewResult(0, 0)) if err := serverUsersRemove(db, "missing"); err != sql.ErrNoRows { t.Fatalf("expected sql.ErrNoRows remove, got %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestCliServerUsersRequiresDSN(t *testing.T) { if err := cliServerUsers([]string{"list"}); err == nil || !strings.Contains(err.Error(), "database URL") { t.Fatalf("expected required dsn, got %v", err) } if err := runServer([]string{"users", "list"}); err == nil || !strings.Contains(err.Error(), "database URL") { t.Fatalf("expected runServer users dsn error, got %v", err) } if err := runServer([]string{"--config", filepath.Join(t.TempDir(), "missing.json")}); err == nil || !strings.Contains(err.Error(), "mysql_dsn") { t.Fatalf("expected normal server dsn error, got %v", err) } }