Files
relspecgo/pkg/ui/helpers_loadsave_test.go
T
2026-10-03 21:41:54 +02:00

309 lines
9.1 KiB
Go

package ui
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/rivo/tview"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
const uiFixtures = "../../tests/assets"
func newUIEditor() *SchemaEditor {
se := NewSchemaEditor(models.InitDatabase("start"))
se.db = newTestEditor().db
return se
}
func hasPage(se *SchemaEditor, name string) bool {
return se.pages.HasPage(name)
}
func TestSortedKeysAndColumnNames(t *testing.T) {
if got := sortedKeys(map[string]int{"b": 1, "a": 2, "c": 3}); strings.Join(got, ",") != "a,b,c" {
t.Errorf("sortedKeys: %v", got)
}
if got := sortedKeys[int](nil); len(got) != 0 {
t.Errorf("nil map: %v", got)
}
tbl := models.InitTable("t", "s")
tbl.Columns["z"] = models.InitColumn("z", "t", "s")
tbl.Columns["a"] = models.InitColumn("a", "t", "s")
if got := getColumnNames(tbl); strings.Join(got, ",") != "a,z" {
t.Errorf("getColumnNames: %v", got)
}
}
func TestLocations(t *testing.T) {
se := newTestEditor()
se.db.Schemas = append(se.db.Schemas, models.InitSchema("empty"))
sl := se.schemaLocations()
if len(sl) != 2 || sl[0].label != "public" || sl[1].schemaIndex != 1 || sl[0].tableIndex != -1 {
t.Errorf("schemaLocations: %+v", sl)
}
tl := se.tableLocations()
if len(tl) != 1 || tl[0].label != "public.users" || tl[0].schemaIndex != 0 || tl[0].tableIndex != 0 {
t.Errorf("tableLocations: %+v", tl)
}
}
func TestParseSkipTablesUI(t *testing.T) {
if got := parseSkipTablesUI(""); len(got) != 0 {
t.Errorf("empty: %v", got)
}
got := parseSkipTablesUI(" Users , ORDERS ,, ")
if len(got) != 2 || !got["users"] || !got["orders"] {
t.Errorf("got %v", got)
}
}
func TestHelpTexts(t *testing.T) {
for name, fn := range map[string]func() string{"load": getLoadHelpText, "save": getSaveHelpText, "import": getImportHelpText} {
if txt := fn(); !strings.Contains(txt, "dbml") && name != "save" || txt == "" {
t.Errorf("%s help text: %q", name, txt)
}
}
}
func TestObjectKinds(t *testing.T) {
se := newTestEditor()
if err := se.SaveIndex(0, 0, "", &models.Index{Name: "idx_e", Columns: []string{"email"}, Unique: true}); err != nil {
t.Fatal(err)
}
if err := se.SaveView(0, -1, &models.View{Name: "v1", Definition: "select 1"}); err != nil {
t.Fatal(err)
}
if err := se.SaveSequence(0, -1, &models.Sequence{Name: "s1", IncrementBy: 1, StartValue: 1}); err != nil {
t.Fatal(err)
}
if err := se.SaveScript(0, -1, &models.Script{Name: "sc1", SQL: "select 1"}); err != nil {
t.Fatal(err)
}
kinds := map[string]objectKind{
"indexes": se.indexKind(), "views": se.viewKind(), "sequences": se.sequenceKind(), "scripts": se.scriptKind(),
}
for page, k := range kinds {
t.Run(page, func(t *testing.T) {
if k.page != page || k.title == "" || k.singular == "" || len(k.headers) == 0 {
t.Fatalf("metadata: %+v", k)
}
rows := k.rows()
if len(rows) != 1 {
t.Fatalf("rows: %+v", rows)
}
for _, r := range rows {
if len(r.cells) != len(k.headers) {
t.Errorf("cells %v do not match headers %v", r.cells, k.headers)
}
}
if len(k.locations()) == 0 {
t.Error("no locations")
}
// Editing an existing row without changes keeps it valid.
form := tview.NewForm()
save := k.buildForm(form, &rows[0])
if form.GetFormItemCount() == 0 {
t.Error("no form fields")
}
loc := k.locations()[0]
loc.schemaIndex, loc.tableIndex = rows[0].schemaIndex, rows[0].tableIndex
if err := save(loc); err != nil {
t.Errorf("save unchanged: %v", err)
}
// A blank new form is rejected by validation.
blank := tview.NewForm()
saveBlank := k.buildForm(blank, nil)
if err := saveBlank(k.locations()[0]); err == nil {
t.Error("blank form accepted")
}
if !k.remove(rows[0]) || len(k.rows()) != 0 {
t.Error("remove failed")
}
})
}
}
func TestObjectKind_CreateIndexFromForm(t *testing.T) {
se := newTestEditor()
k := se.indexKind()
form := tview.NewForm()
save := k.buildForm(form, nil)
form.GetFormItemByLabel("Name").(*tview.InputField).SetText("idx_new")
form.GetFormItemByLabel("Columns (comma separated)").(*tview.InputField).SetText("id, email")
if err := save(k.locations()[0]); err != nil {
t.Fatal(err)
}
idx := se.db.Schemas[0].Tables[0].Indexes["idx_new"]
if idx == nil || len(idx.Columns) != 2 || idx.Type != "btree" {
t.Errorf("index: %+v", idx)
}
}
func TestLoadDatabase(t *testing.T) {
for _, tt := range []struct{ format, path string }{
{"dbml", "dbml/simple.dbml"},
{"json", "json/database.json"},
{"yaml", "yaml/database.yaml"},
{"drawdb", "drawdb/simple.json"},
{"dctx", "dctx/p1.dctx"},
{"graphql", "graphql/simple.graphql"},
{"prisma", "prisma/example.prisma"},
{"typeorm", "typeorm/example.ts"},
{"drizzle", "drizzle/schema.ts"},
{"gorm", "gorm/simple.go"},
{"bun", "bun/simple.go"},
} {
t.Run(tt.format, func(t *testing.T) {
se := newUIEditor()
se.loadDatabase(tt.format, filepath.Join(uiFixtures, tt.path), "")
if hasPage(se, "error-dialog") || !hasPage(se, "success-dialog") {
t.Fatalf("expected success dialog (pages: error=%v)", hasPage(se, "error-dialog"))
}
if se.loadConfig == nil || se.loadConfig.SourceType != tt.format || len(se.db.Schemas) == 0 {
t.Errorf("state: %+v db=%+v", se.loadConfig, se.db)
}
})
}
errCases := []struct {
name, format, path, conn string
}{
{"pgsql no conn", "pgsql", "", ""},
{"file required", "json", "", ""},
{"unsupported", "nope", "x", ""},
{"missing file", "json", filepath.Join(t.TempDir(), "missing.json"), ""},
}
for _, tt := range errCases {
t.Run(tt.name, func(t *testing.T) {
se := newUIEditor()
before := se.db
se.loadDatabase(tt.format, tt.path, tt.conn)
if !hasPage(se, "error-dialog") {
t.Error("expected error dialog")
}
if se.db != before || se.loadConfig != nil {
t.Error("state must be unchanged on error")
}
})
}
}
func TestCreateNewDatabase(t *testing.T) {
se := newUIEditor()
se.loadConfig = &LoadConfig{SourceType: "json"}
se.createNewDatabase()
if se.db.Name != "New Database" || len(se.db.Schemas) != 0 || se.loadConfig != nil || !hasPage(se, "success-dialog") {
t.Errorf("state: %+v", se.db)
}
}
func TestSaveDatabase(t *testing.T) {
for _, tt := range []struct{ format, file string }{
{"json", "o.json"},
{"yaml", "o.yaml"},
{"dbml", "o.dbml"},
{"drawdb", "o.drawdb.json"},
{"graphql", "o.graphql"},
{"prisma", "o.prisma"},
{"typeorm", "o.ts"},
{"drizzle", "d.ts"},
{"gorm", "g.go"},
{"bun", "b.go"},
} {
t.Run(tt.format, func(t *testing.T) {
se := newUIEditor()
out := filepath.Join(t.TempDir(), tt.file)
se.saveDatabase(tt.format, out)
if hasPage(se, "error-dialog") {
t.Fatal("unexpected error dialog")
}
if se.saveConfig == nil || se.saveConfig.FilePath != out || se.saveConfig.TargetType != tt.format {
t.Errorf("saveConfig: %+v", se.saveConfig)
}
if info, err := os.Stat(out); err != nil || info.Size() == 0 {
t.Errorf("output: %v", err)
}
})
}
for name, args := range map[string][2]string{
"pgsql unsupported": {"pgsql", "x.sql"},
"path required": {"json", ""},
"unknown format": {"nope", "x"},
} {
t.Run(name, func(t *testing.T) {
se := newUIEditor()
se.saveDatabase(args[0], args[1])
if !hasPage(se, "error-dialog") || se.saveConfig != nil {
t.Error("expected error dialog and no saveConfig")
}
})
}
}
func TestImportAndMerge(t *testing.T) {
se := newUIEditor()
se.importAndMergeDatabase("json", filepath.Join(uiFixtures, "json/database.json"), "", false, false, false, false, false, "")
if hasPage(se, "error-dialog") {
t.Fatal("unexpected error dialog")
}
for name, args := range map[string][3]string{
"pgsql no conn": {"pgsql", "", ""},
"file required": {"json", "", ""},
"unsupported": {"nope", "x", ""},
"missing file": {"json", filepath.Join(t.TempDir(), "missing.json"), ""},
} {
t.Run(name, func(t *testing.T) {
se := newUIEditor()
se.importAndMergeDatabase(args[0], args[1], args[2], false, false, false, false, false, "")
if !hasPage(se, "error-dialog") {
t.Error("expected error dialog")
}
})
}
}
func TestPerformMerge(t *testing.T) {
se := newUIEditor()
src := models.InitDatabase("src")
s := models.InitSchema("public")
tbl := models.InitTable("orders", "public")
tbl.Columns["id"] = models.InitColumn("id", "orders", "public")
skip := models.InitTable("skipme", "public")
s.Tables = append(s.Tables, tbl, skip)
src.Schemas = append(src.Schemas, s)
se.performMerge(src, false, false, false, false, false, "SkipMe")
if !hasPage(se, "success-dialog") {
t.Error("expected success dialog")
}
names := map[string]bool{}
for _, tb := range se.db.Schemas[0].Tables {
names[tb.Name] = true
}
if !names["users"] || !names["orders"] || names["skipme"] || len(names) != 2 {
t.Errorf("tables after merge: %v", names)
}
}
func TestEditorAccessors(t *testing.T) {
db := models.InitDatabase("d")
lc, sc := &LoadConfig{SourceType: "json"}, &SaveConfig{TargetType: "yaml"}
se := NewSchemaEditorWithConfigs(db, lc, sc)
if se.GetDatabase() != db || se.loadConfig != lc || se.saveConfig != sc || se.app == nil || se.pages == nil {
t.Errorf("%+v", se)
}
if se.createMainMenu() == nil {
t.Error("main menu")
}
}