package orm import ( "fmt" "testing" ) func TestNewModel(t *testing.T) { m := NewModel("test.model.unit.new", ModelOpts{ Description: "Test Model", RecName: "name", }) if m.Name() != "test.model.unit.new" { t.Errorf("name: %s", m.Name()) } if m.Table() != "test_model_unit_new" { t.Errorf("table: %s", m.Table()) } if m.Description() != "Test Model" { t.Errorf("desc: %s", m.Description()) } if m.RecName() != "name" { t.Errorf("rec_name: %s", m.RecName()) } } func TestNewModelDefaults(t *testing.T) { m := NewModel("test.model.defaults", ModelOpts{}) if m.Order() != "id" { t.Errorf("default order: %s", m.Order()) } if m.RecName() != "name" { t.Errorf("default rec_name: %s", m.RecName()) } if m.IsAbstract() { t.Error("should not be abstract") } if m.IsTransient() { t.Error("should not be transient") } } func TestNewModelCustomTable(t *testing.T) { m := NewModel("test.model.custom.table", ModelOpts{ Table: "my_custom_table", }) if m.Table() != "my_custom_table" { t.Errorf("table: %s", m.Table()) } } func TestNewModelAbstract(t *testing.T) { m := NewModel("test.model.abstract", ModelOpts{ Type: ModelAbstract, }) if m.IsAbstract() != true { t.Error("should be abstract") } // Abstract models have no table if m.Table() != "" { t.Errorf("abstract should have no table, got %s", m.Table()) } } func TestNewModelTransient(t *testing.T) { m := NewModel("test.model.transient", ModelOpts{ Type: ModelTransient, }) if m.IsTransient() != true { t.Error("should be transient") } } func TestModelMagicFields(t *testing.T) { m := NewModel("test.model.magic", ModelOpts{}) // Magic fields should be auto-created if f := m.GetField("id"); f == nil { t.Error("id field missing") } if f := m.GetField("display_name"); f == nil { t.Error("display_name field missing") } if f := m.GetField("create_uid"); f == nil { t.Error("create_uid field missing") } if f := m.GetField("create_date"); f == nil { t.Error("create_date field missing") } if f := m.GetField("write_uid"); f == nil { t.Error("write_uid field missing") } if f := m.GetField("write_date"); f == nil { t.Error("write_date field missing") } } func TestModelAddFields(t *testing.T) { m := NewModel("test.model.fields.add", ModelOpts{}) m.AddFields( Char("name", FieldOpts{String: "Name", Required: true}), Integer("age", FieldOpts{String: "Age"}), Boolean("active", FieldOpts{String: "Active", Default: true}), ) if f := m.GetField("name"); f == nil { t.Error("name field missing") } if f := m.GetField("age"); f == nil { t.Error("age field missing") } if f := m.GetField("active"); f == nil { t.Error("active field missing") } if f := m.GetField("nonexistent"); f != nil { t.Error("should be nil") } nameF := m.GetField("name") if nameF.Type != TypeChar { t.Error("expected char") } if !nameF.Required { t.Error("expected required") } if nameF.String != "Name" { t.Error("expected Name label") } } func TestModelAddFieldSetsModel(t *testing.T) { m := NewModel("test.model.field.backref", ModelOpts{}) f := Char("ref", FieldOpts{}) m.AddField(f) if f.model != m { t.Error("field should have back-reference to model") } } func TestModelStoredFields(t *testing.T) { m := NewModel("test.model.stored", ModelOpts{}) m.AddFields( Char("name", FieldOpts{}), Char("computed_field", FieldOpts{Compute: "x"}), One2many("lines", "other.model", "parent_id", FieldOpts{}), ) stored := m.StoredFields() // Should include magic fields + name, but not computed_field or o2m nameFound := false computedFound := false linesFound := false for _, f := range stored { switch f.Name { case "name": nameFound = true case "computed_field": computedFound = true case "lines": linesFound = true } } if !nameFound { t.Error("name should be in stored fields") } if computedFound { t.Error("computed_field should not be in stored fields") } if linesFound { t.Error("o2m lines should not be in stored fields") } } func TestModelRegisterMethod(t *testing.T) { m := NewModel("test.model.methods.reg", ModelOpts{}) called := false m.RegisterMethod("test_action", func(rs *Recordset, args ...interface{}) (interface{}, error) { called = true return "ok", nil }) if _, ok := m.Methods["test_action"]; !ok { t.Error("method not registered") } result, err := m.Methods["test_action"](nil) if err != nil { t.Fatal(err) } if result != "ok" { t.Error("expected ok") } if !called { t.Error("method not called") } } func TestExtendModel(t *testing.T) { NewModel("test.model.base.ext", ModelOpts{Description: "Base"}) ext := ExtendModel("test.model.base.ext") ext.AddFields(Char("extra_field", FieldOpts{String: "Extra"})) base := Registry.Get("test.model.base.ext") if f := base.GetField("extra_field"); f == nil { t.Error("extension field missing") } } func TestExtendModelPanics(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("expected panic for missing model") } }() ExtendModel("nonexistent.model.xyz.panic") } func TestRegistryGet(t *testing.T) { NewModel("test.registry.get.model", ModelOpts{}) if m := Registry.Get("test.registry.get.model"); m == nil { t.Error("model not found") } if m := Registry.Get("nonexistent.registry.model"); m != nil { t.Error("should be nil") } } func TestRegistryMustGetPanics(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("expected panic for missing model") } }() Registry.MustGet("nonexistent.mustget.model") } func TestRegistryAll(t *testing.T) { NewModel("test.registry.all.model", ModelOpts{}) all := Registry.All() found := false for _, name := range all { if name == "test.registry.all.model" { found = true break } } if !found { t.Error("model not in Registry.All()") } } func TestRegistryModels(t *testing.T) { NewModel("test.registry.models.model", ModelOpts{}) models := Registry.Models() if _, ok := models["test.registry.models.model"]; !ok { t.Error("model not in Registry.Models()") } } func TestModelSQLConstraint(t *testing.T) { m := NewModel("test.model.constraint.sql", ModelOpts{}) m.AddSQLConstraint("unique_name", "UNIQUE(name)", "Name must be unique") if len(m.SQLConstraints) != 1 { t.Error("constraint not added") } if m.SQLConstraints[0].Name != "unique_name" { t.Error("wrong name") } if m.SQLConstraints[0].Definition != "UNIQUE(name)" { t.Error("wrong definition") } if m.SQLConstraints[0].Message != "Name must be unique" { t.Error("wrong message") } } func TestModelAddConstraint(t *testing.T) { m := NewModel("test.model.constraint.func", ModelOpts{}) m.AddConstraint(func(rs *Recordset) error { return fmt.Errorf("test error") }) if len(m.Constraints) != 1 { t.Error("constraint not added") } } func TestModelRegisterOnchange(t *testing.T) { m := NewModel("test.model.onchange", ModelOpts{}) m.RegisterOnchange("partner_id", func(env *Environment, vals Values) Values { return Values{"name": "changed"} }) if m.OnchangeHandlers == nil { t.Fatal("OnchangeHandlers should not be nil") } if _, ok := m.OnchangeHandlers["partner_id"]; !ok { t.Error("onchange handler not registered") } } func TestModelRegisterInverse(t *testing.T) { m := NewModel("test.model.inverse", ModelOpts{}) m.AddFields(Char("computed", FieldOpts{Compute: "_compute_computed"})) m.RegisterInverse("computed", func(rs *Recordset, args ...interface{}) (interface{}, error) { return nil, nil }) f := m.GetField("computed") if f.Inverse != "_inverse_computed" { t.Errorf("expected _inverse_computed, got %s", f.Inverse) } if _, ok := m.Methods["_inverse_computed"]; !ok { t.Error("inverse method not registered") } } func TestModelCreateTableSQL(t *testing.T) { m := NewModel("test.model.ddl", ModelOpts{}) m.AddFields( Char("name", FieldOpts{Required: true}), Integer("count", FieldOpts{}), ) sql := m.CreateTableSQL() if sql == "" { t.Fatal("expected non-empty SQL") } // Should contain the table name if !containsStr(sql, `"test_model_ddl"`) { t.Error("missing table name in DDL") } // Should contain name column if !containsStr(sql, `"name"`) { t.Error("missing name column in DDL") } // Should contain NOT NULL for required if !containsStr(sql, "NOT NULL") { t.Error("missing NOT NULL for required field") } } func TestModelCreateTableSQLAbstract(t *testing.T) { m := NewModel("test.model.ddl.abstract", ModelOpts{Type: ModelAbstract}) sql := m.CreateTableSQL() if sql != "" { t.Error("abstract model should have empty DDL") } } func TestModelFields(t *testing.T) { m := NewModel("test.model.all.fields", ModelOpts{}) m.AddFields(Char("name", FieldOpts{})) fields := m.Fields() if _, ok := fields["name"]; !ok { t.Error("name not in Fields()") } if _, ok := fields["id"]; !ok { t.Error("id not in Fields()") } } // containsStr is a test helper - checks if s contains substr func containsStr(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContains(s, substr)) } func stringContains(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }