package main import ( "os" "path/filepath" "strings" "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" ) const fixturesDir = "../../tests/assets" // readableFormats maps each file-based reader format to an existing fixture. var readableFormats = []struct { format string path string }{ {"dbml", "dbml/simple.dbml"}, {"json", "json/database.json"}, {"yaml", "yaml/database.yaml"}, {"yml", "yaml/database.yaml"}, {"drawdb", "drawdb/simple.json"}, {"dctx", "dctx/p1.dctx"}, {"graphql", "graphql/simple.graphql"}, {"gql", "graphql/simple.graphql"}, {"prisma", "prisma/example.prisma"}, {"typeorm", "typeorm/example.ts"}, {"drizzle", "drizzle/schema.ts"}, {"gorm", "gorm/simple.go"}, {"bun", "bun/simple.go"}, } func TestReadDatabaseForConvert_FileFormats(t *testing.T) { for _, tt := range readableFormats { t.Run(tt.format, func(t *testing.T) { db, err := readDatabaseForConvert(tt.format, filepath.Join(fixturesDir, tt.path), "") if err != nil { t.Fatalf("read: %v", err) } if db == nil || len(db.Schemas) == 0 { t.Fatalf("no schemas read: %+v", db) } // Uppercase format names are accepted. if _, err := readDatabaseForConvert(strings.ToUpper(tt.format), filepath.Join(fixturesDir, tt.path), ""); err != nil { t.Errorf("uppercase format: %v", err) } }) } } func TestReadDatabaseForConvert_Errors(t *testing.T) { filePathFormats := []string{"dbml", "dctx", "drawdb", "json", "yaml", "gorm", "bun", "drizzle", "prisma", "typeorm", "graphql"} for _, f := range filePathFormats { t.Run("missing path "+f, func(t *testing.T) { _, err := readDatabaseForConvert(f, "", "") if err == nil || !strings.Contains(err.Error(), "file path is required") { t.Errorf("got %v", err) } }) } connFormats := []string{"pgsql", "postgres", "postgresql", "mssql", "sqlserver", "mysql", "mariadb"} for _, f := range connFormats { t.Run("missing conn "+f, func(t *testing.T) { _, err := readDatabaseForConvert(f, "", "") if err == nil || !strings.Contains(err.Error(), "connection string is required") { t.Errorf("got %v", err) } }) } if _, err := readDatabaseForConvert("sqlite", "", ""); err == nil || !strings.Contains(err.Error(), "required for SQLite") { t.Errorf("sqlite: %v", err) } if _, err := readDatabaseForConvert("nope", "x", ""); err == nil || !strings.Contains(err.Error(), "unsupported source format") { t.Errorf("unsupported: %v", err) } if _, err := readDatabaseForConvert("dbml", filepath.Join(t.TempDir(), "missing.dbml"), ""); err == nil || !strings.Contains(err.Error(), "failed to read database") { t.Errorf("missing file: %v", err) } } func TestReadDatabase_DiffReader(t *testing.T) { for _, f := range []string{"dbml", "json", "yaml", "drawdb", "dctx"} { for _, tt := range readableFormats { if tt.format != f { continue } t.Run(f, func(t *testing.T) { db, err := readDatabase(f, filepath.Join(fixturesDir, tt.path), "", "source") if err != nil || db == nil || len(db.Schemas) == 0 { t.Fatalf("read: %v %+v", err, db) } }) } } for _, f := range []string{"dbml", "dctx", "drawdb", "json", "yaml", "sqldir"} { if _, err := readDatabase(f, "", "", "src"); err == nil || !strings.Contains(err.Error(), "src: file path is required") { t.Errorf("%s missing path: %v", f, err) } } if _, err := readDatabase("pgsql", "", "", "src"); err == nil || !strings.Contains(err.Error(), "connection string is required") { t.Errorf("pgsql: %v", err) } if _, err := readDatabase("sqlite", "", "", "src"); err == nil { t.Error("sqlite without path must fail") } if _, err := readDatabase("nope", "x", "", "src"); err == nil || !strings.Contains(err.Error(), "unsupported database format") { t.Errorf("unsupported: %v", err) } if _, err := readDatabase("json", filepath.Join(t.TempDir(), "missing.json"), "", "src"); err == nil || !strings.Contains(err.Error(), "src: failed to read database") { t.Errorf("missing file: %v", err) } } func TestMaskPassword(t *testing.T) { tests := []struct { in, want string }{ {"", ""}, {"postgres://user:secret@host:5432/db", "postgres://user:***@host:5432/db"}, {"postgres://user@host:5432/db", "postgres://user@host:5432/db"}, {"host=h user=u password=secret dbname=d", "host=h user=u password=*** dbname=d"}, {"host=h user=u", "host=h user=u"}, {"/tmp/file.db", "/tmp/file.db"}, } for _, tt := range tests { if got := maskPassword(tt.in); got != tt.want { t.Errorf("maskPassword(%q) = %q, want %q", tt.in, got, tt.want) } if got := maskPasswordInDiff(tt.in); got != tt.want { t.Errorf("maskPasswordInDiff(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestGetSchemaNames(t *testing.T) { db := models.InitDatabase("d") if got := getSchemaNames(db); len(got) != 0 { t.Errorf("empty: %v", got) } db.Schemas = []*models.Schema{{Name: "a"}, {Name: "b"}} if got := strings.Join(getSchemaNames(db), ","); got != "a,b" { t.Errorf("got %s", got) } } func TestLoadExtraFields(t *testing.T) { dir := t.TempDir() write := func(name, body string) string { p := filepath.Join(dir, name) if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } valid := write("valid.json", `[{"name":"extra"}]`) if got, err := loadExtraFields("bun", valid); err != nil || !strings.Contains(got, "extra") { t.Errorf("valid: %q %v", got, err) } if _, err := loadExtraFields("BUN", valid); err != nil { t.Errorf("case-insensitive format: %v", err) } if _, err := loadExtraFields("gorm", valid); err == nil || !strings.Contains(err.Error(), "only supported for Bun") { t.Errorf("non-bun: %v", err) } if _, err := loadExtraFields("bun", filepath.Join(dir, "missing.json")); err == nil || !strings.Contains(err.Error(), "failed to read") { t.Errorf("missing: %v", err) } if _, err := loadExtraFields("bun", write("bad.json", `{not json`)); err == nil || !strings.Contains(err.Error(), "invalid --extra-fields JSON") { t.Errorf("bad json: %v", err) } if _, err := loadExtraFields("bun", write("empty.json", `[]`)); err == nil || !strings.Contains(err.Error(), "at least one field") { t.Errorf("empty: %v", err) } } func multiSchemaDB() *models.Database { db := models.InitDatabase("multi") for _, n := range []string{"a", "b"} { s := models.InitSchema(n) tbl := models.InitTable("t_"+n, n) c := models.InitColumn("id", tbl.Name, n) c.Type = "integer" c.IsPrimaryKey = true tbl.Columns["id"] = c s.Tables = append(s.Tables, tbl) db.Schemas = append(db.Schemas, s) } return db } func TestValidateWriteTarget(t *testing.T) { db := multiSchemaDB() single := models.InitDatabase("single") single.Schemas = []*models.Schema{models.InitSchema("only")} tests := []struct { name string db *models.Database dbType, pkg, schemaFilter, extraFields, wantErrSubstr string }{ {"json ok", db, "json", "", "", "", ""}, {"pgsql alias ok", db, "sql", "", "", "", ""}, {"gorm needs package", db, "gorm", "", "", "", "package name is required"}, {"bun needs package", db, "bun", "", "", "", "package name is required"}, {"gorm with package", db, "gorm", "models", "", "", ""}, {"unsupported", db, "nope", "", "", "", "unsupported target format"}, {"schema filter found", db, "json", "", "a", "", ""}, {"schema filter missing", db, "json", "", "zzz", "", "not found in database"}, {"dctx multi schema", db, "dctx", "", "", "", "multiple schemas found"}, {"dctx multi schema with filter", db, "dctx", "", "a", "", ""}, {"dctx single schema", single, "dctx", "", "", "", ""}, {"dctx no schemas", models.InitDatabase("e"), "dctx", "", "", "", "no schemas found"}, {"extra fields non-bun", db, "json", "", "", "x.json", "only supported for Bun"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateWriteTarget(tt.db, tt.dbType, tt.pkg, tt.schemaFilter, tt.extraFields) if tt.wantErrSubstr == "" { if err != nil { t.Errorf("unexpected error: %v", err) } return } if err == nil || !strings.Contains(err.Error(), tt.wantErrSubstr) { t.Errorf("got %v, want substring %q", err, tt.wantErrSubstr) } }) } } func TestWriteDatabase_Formats(t *testing.T) { db := multiSchemaDB() formats := []struct{ format, file string }{ {"json", "out.json"}, {"yaml", "out.yaml"}, {"yml", "out.yml"}, {"dbml", "out.dbml"}, {"drawdb", "out.drawdb.json"}, {"pgsql", "out.sql"}, {"postgres", "out2.sql"}, {"sql", "out3.sql"}, {"mssql", "out_ms.sql"}, {"mysql", "out_my.sql"}, {"sqlite", "out_lite.sql"}, {"graphql", "out.graphql"}, {"gql", "out2.graphql"}, {"prisma", "out.prisma"}, {"typeorm", "out.ts"}, {"drizzle", "out_drizzle.ts"}, } for _, tt := range formats { t.Run(tt.format, func(t *testing.T) { out := filepath.Join(t.TempDir(), tt.file) if err := writeDatabase(db, tt.format, out, "", "", false, "", "", false, ""); err != nil { t.Fatalf("write: %v", err) } info, err := os.Stat(out) if err != nil || info.Size() == 0 { t.Errorf("output missing or empty: %v", err) } }) } } func TestWriteDatabase_GoFormatsWriteIntoDir(t *testing.T) { db := multiSchemaDB() for _, f := range []string{"gorm", "bun"} { t.Run(f, func(t *testing.T) { out := filepath.Join(t.TempDir(), "models.go") if err := writeDatabase(db, f, out, "models", "", false, "", "", false, ""); err != nil { t.Fatal(err) } if _, err := os.Stat(out); err != nil { t.Errorf("no output: %v", err) } if err := writeDatabase(db, f, out, "", "", false, "", "", false, ""); err == nil || !strings.Contains(err.Error(), "package name is required") { t.Errorf("missing package: %v", err) } }) } } func TestWriteDatabase_SchemaFilterAndDCTX(t *testing.T) { db := multiSchemaDB() out := filepath.Join(t.TempDir(), "o.json") if err := writeDatabase(db, "json", out, "", "a", false, "", "", false, ""); err != nil { t.Errorf("schema filter: %v", err) } if err := writeDatabase(db, "json", out, "", "zzz", false, "", "", false, ""); err == nil || !strings.Contains(err.Error(), "not found in database") { t.Errorf("missing schema: %v", err) } if err := writeDatabase(db, "dctx", out, "", "", false, "", "", false, ""); err == nil || !strings.Contains(err.Error(), "multiple schemas found") { t.Errorf("dctx multi: %v", err) } single := models.InitDatabase("s") single.Schemas = []*models.Schema{db.Schemas[0]} if err := writeDatabase(single, "dctx", filepath.Join(t.TempDir(), "o.dctx"), "", "", false, "", "", false, ""); err != nil { t.Errorf("dctx single: %v", err) } if err := writeDatabase(models.InitDatabase("e"), "dctx", out, "", "", false, "", "", false, ""); err == nil || !strings.Contains(err.Error(), "no schemas found") { t.Errorf("dctx empty: %v", err) } if err := writeDatabase(db, "nope", out, "", "", false, "", "", false, ""); err == nil || !strings.Contains(err.Error(), "unsupported target format") { t.Errorf("unsupported: %v", err) } if err := writeDatabase(db, "json", out, "", "", false, "", "", false, filepath.Join(t.TempDir(), "x.json")); err == nil || !strings.Contains(err.Error(), "only supported for Bun") { t.Errorf("extra fields with json: %v", err) } }