package template import ( "errors" "os" "path/filepath" "strings" "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/writers" ) func writeTemplateFile(t *testing.T, body string) string { t.Helper() p := filepath.Join(t.TempDir(), "t.tmpl") if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } func modeDB() *models.Database { db := models.InitDatabase("shop") for _, sn := range []string{"a", "b"} { s := models.InitSchema(sn) for _, tn := range []string{"t1", "t2"} { s.Tables = append(s.Tables, models.InitTable(tn, sn)) } s.Scripts = append(s.Scripts, &models.Script{Name: "seed_" + sn}) db.Schemas = append(db.Schemas, s) } db.Domains = append(db.Domains, &models.Domain{Name: "billing"}) return db } func newTestWriter(t *testing.T, body, mode, pattern, out string) (*Writer, error) { t.Helper() meta := map[string]interface{}{"template_path": writeTemplateFile(t, body)} if mode != "" { meta["mode"] = mode } if pattern != "" { meta["filename_pattern"] = pattern } return NewWriter(&writers.WriterOptions{OutputPath: out, Metadata: meta}) } func TestNewWriterErrors(t *testing.T) { if _, err := NewWriter(&writers.WriterOptions{}); err == nil { t.Error("expected error for missing template path") } _, err := NewWriter(&writers.WriterOptions{Metadata: map[string]interface{}{"template_path": "/no/such/file"}}) var te *TemplateError if !errors.As(err, &te) || te.Phase != "load" { t.Errorf("load error: %v", err) } _, err = newTestWriter(t, "{{ .Unclosed ", "", "", "") if !errors.As(err, &te) || te.Phase != "parse" { t.Errorf("parse error: %v", err) } } func TestWriterModes(t *testing.T) { tests := []struct { name, mode, body, pattern string wantFiles []string }{ {"database", "database", "{{.Database.Name}}", "", []string{"out.txt"}}, {"schema", "schema", "{{.Schema.Name}}", "{{.Name}}.txt", []string{"a.txt", "b.txt"}}, {"table", "table", "{{.Table.Name}}", "{{.ParentSchema.Name}}_{{.Name}}.txt", []string{"a_t1.txt", "a_t2.txt", "b_t1.txt", "b_t2.txt"}}, {"script", "script", "{{.Script.Name}}", "{{.Name}}.sql", []string{"seed_a.sql", "seed_b.sql"}}, {"domain", "domain", "{{.Domain.Name}}", "{{.Name}}.md", []string{"billing.md"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { outDir := t.TempDir() out := outDir if tt.mode == "database" { out = filepath.Join(outDir, "out.txt") } w, err := newTestWriter(t, tt.body, tt.mode, tt.pattern, out) if err != nil { t.Fatal(err) } if err := w.WriteDatabase(modeDB()); err != nil { t.Fatal(err) } for _, f := range tt.wantFiles { if _, err := os.Stat(filepath.Join(outDir, f)); err != nil { t.Errorf("missing %s: %v", f, err) } } entries, _ := os.ReadDir(outDir) if len(entries) != len(tt.wantFiles) { t.Errorf("got %d files, want %d", len(entries), len(tt.wantFiles)) } }) } } func TestWriterDatabaseModeContent(t *testing.T) { out := filepath.Join(t.TempDir(), "sub", "dir", "o.txt") w, err := newTestWriter(t, "{{.Database.Name}}:{{len .Database.Schemas}}", "", "", out) if err != nil { t.Fatal(err) } if err := w.WriteDatabase(modeDB()); err != nil { t.Fatal(err) } data, err := os.ReadFile(out) if err != nil || string(data) != "shop:2" { t.Errorf("content %q err %v", data, err) } } func TestWriterUnknownMode(t *testing.T) { w, err := newTestWriter(t, "x", "bogus", "", "") if err != nil { t.Fatal(err) } if err := w.WriteDatabase(modeDB()); err == nil || !strings.Contains(err.Error(), "unknown entrypoint mode") { t.Errorf("got %v", err) } } func TestWriterExecuteErrors(t *testing.T) { // Execution failure: field does not exist on TemplateData. for _, mode := range []string{"database", "schema", "table", "script", "domain"} { t.Run(mode, func(t *testing.T) { w, err := newTestWriter(t, "{{.NoSuchField}}", mode, "", t.TempDir()) if err != nil { t.Fatal(err) } err = w.WriteDatabase(modeDB()) var te *TemplateError if !errors.As(err, &te) || te.Phase != "execute" { t.Errorf("got %v", err) } }) } } func TestWriterBadFilenamePattern(t *testing.T) { for _, pattern := range []string{"{{.Unclosed", "{{.NoSuchField}}"} { for _, mode := range []string{"schema", "table", "script", "domain"} { w, err := newTestWriter(t, "x", mode, pattern, t.TempDir()) if err != nil { t.Fatal(err) } if err := w.WriteDatabase(modeDB()); err == nil { t.Errorf("mode %s pattern %q: expected error", mode, pattern) } } } } func TestWriterWriteOutputFailure(t *testing.T) { // Output path whose parent is a regular file cannot be created. blocker := filepath.Join(t.TempDir(), "file") if err := os.WriteFile(blocker, nil, 0o644); err != nil { t.Fatal(err) } w, err := newTestWriter(t, "x", "database", "", filepath.Join(blocker, "child", "o.txt")) if err != nil { t.Fatal(err) } if err := w.WriteDatabase(modeDB()); err == nil { t.Error("expected write failure") } } func TestWriterGenerateFilenameOutputPathForms(t *testing.T) { dir := t.TempDir() data := NewTableData(models.InitTable("users", "public"), nil, nil, nil) tests := []struct { name, out, want string }{ {"no output path", "", "users.txt"}, {"existing dir", dir, filepath.Join(dir, "users.txt")}, {"trailing separator", filepath.Join(dir, "new") + string(filepath.Separator), filepath.Join(dir, "new", "users.txt")}, {"file path uses its dir", filepath.Join(dir, "x.out"), filepath.Join(dir, "users.txt")}, {"bare file name", "x.out", "users.txt"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w, err := newTestWriter(t, "x", "table", "{{.Name}}.txt", tt.out) if err != nil { t.Fatal(err) } got, err := w.generateFilename(data) if err != nil || got != tt.want { t.Errorf("got %q err %v, want %q", got, err, tt.want) } }) } } func TestWriterWriteSchemaAndTable(t *testing.T) { out := filepath.Join(t.TempDir(), "o.txt") w, err := newTestWriter(t, "{{range .Database.Schemas}}{{.Name}}:{{len .Tables}};{{end}}", "", "", out) if err != nil { t.Fatal(err) } db := modeDB() if err := w.WriteSchema(db.Schemas[0]); err != nil { t.Fatal(err) } if data, _ := os.ReadFile(out); string(data) != "a:2;" { t.Errorf("WriteSchema: %q", data) } if err := w.WriteTable(db.Schemas[1].Tables[0]); err != nil { t.Fatal(err) } if data, _ := os.ReadFile(out); string(data) != "b:1;" { t.Errorf("WriteTable: %q", data) } }