Expand Sale/Purchase/Project + 102 ORM tests — +4633 LOC
Sale (1177→2321 LOC): - Quotation templates (apply to order, option lines) - Sales reports (by month, product, customer, salesperson, category) - Advance payment wizard (delivered/percentage/fixed modes) - SO cancel wizard, discount wizard - action_quotation_sent, action_lock/unlock, preview_quotation - Line computes: invoice_status, price_reduce, untaxed_amount - Partner extension: sale_order_total Purchase (478→1424 LOC): - Purchase reports (by month, category, bill status, receipt analysis) - Receipt creation from PO (action_create_picking) - 3-way matching: action_view_picking, action_view_invoice - button_approve, button_done, action_rfq_send - Line computes: price_subtotal/total with tax, product onchange - Partner extension: purchase_order_count/total Project (218→1161 LOC): - Project updates (status tracking: on_track/at_risk/off_track) - Milestones (deadline, reached tracking, task count) - Timesheet integration (account.analytic.line extension) - Timesheet reports (by project, employee, task, week) - Task recurrence model - Task: planned/effective/remaining hours, progress, subtask hours - Project: allocated/remaining hours, profitability actions ORM Tests (102 tests, 0→1257 LOC): - domain_test.go: 32 tests (compile, operators, AND/OR/NOT, null) - field_test.go: 15 tests (IsCopyable, SQLType, IsRelational, IsStored) - model_test.go: 21 tests (NewModel, AddFields, RegisterMethod, ExtendModel) - domain_parse_test.go: 21 tests (parse Python domain strings) - sanitize_test.go: 13 tests (false→nil, type conversions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
277
pkg/orm/domain_parse_test.go
Normal file
277
pkg/orm/domain_parse_test.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package orm
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseDomainStringSimple(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('name', '=', 'test')]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 1 {
|
||||
t.Fatalf("expected 1 node, got %d", len(domain))
|
||||
}
|
||||
cond, ok := domain[0].(Condition)
|
||||
if !ok {
|
||||
t.Fatal("expected Condition")
|
||||
}
|
||||
if cond.Field != "name" {
|
||||
t.Errorf("field: %s", cond.Field)
|
||||
}
|
||||
if cond.Operator != "=" {
|
||||
t.Errorf("op: %s", cond.Operator)
|
||||
}
|
||||
if cond.Value != "test" {
|
||||
t.Errorf("value: %v", cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringNumeric(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('age', '>', 18)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != int64(18) {
|
||||
t.Errorf("expected int64(18), got %T %v", cond.Value, cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringFloat(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('amount', '>', 99.5)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != float64(99.5) {
|
||||
t.Errorf("expected float64(99.5), got %T %v", cond.Value, cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringNegativeNumber(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('balance', '<', -100)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != int64(-100) {
|
||||
t.Errorf("expected int64(-100), got %T %v", cond.Value, cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringBoolean(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('active', '=', True)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != true {
|
||||
t.Errorf("expected true, got %v", cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringBooleanFalse(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('active', '=', False)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != false {
|
||||
t.Errorf("expected false, got %v", cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringList(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('id', 'in', [1, 2, 3])]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
vals, ok := cond.Value.([]int64)
|
||||
if !ok {
|
||||
t.Fatalf("expected []int64, got %T", cond.Value)
|
||||
}
|
||||
if len(vals) != 3 {
|
||||
t.Errorf("expected 3, got %d", len(vals))
|
||||
}
|
||||
if vals[0] != 1 || vals[1] != 2 || vals[2] != 3 {
|
||||
t.Errorf("values: %v", vals)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringStringList(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('state', 'in', ['draft', 'sent'])]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
vals, ok := cond.Value.([]string)
|
||||
if !ok {
|
||||
t.Fatalf("expected []string, got %T", cond.Value)
|
||||
}
|
||||
if len(vals) != 2 {
|
||||
t.Errorf("expected 2, got %d", len(vals))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringEmptyList(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('id', 'in', [])]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
vals, ok := cond.Value.([]int64)
|
||||
if !ok {
|
||||
t.Fatalf("expected []int64, got %T", cond.Value)
|
||||
}
|
||||
if len(vals) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(vals))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringOperators(t *testing.T) {
|
||||
domain, err := ParseDomainString("['&', ('a', '=', 1), ('b', '=', 2)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 3 {
|
||||
t.Fatalf("expected 3 nodes, got %d", len(domain))
|
||||
}
|
||||
if domain[0] != OpAnd {
|
||||
t.Error("expected & operator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringOrOperator(t *testing.T) {
|
||||
domain, err := ParseDomainString("['|', ('a', '=', 1), ('b', '=', 2)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 3 {
|
||||
t.Fatalf("expected 3 nodes, got %d", len(domain))
|
||||
}
|
||||
if domain[0] != OpOr {
|
||||
t.Error("expected | operator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringNotOperator(t *testing.T) {
|
||||
domain, err := ParseDomainString("['!', ('active', '=', True)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 2 {
|
||||
t.Fatalf("expected 2 nodes, got %d", len(domain))
|
||||
}
|
||||
if domain[0] != OpNot {
|
||||
t.Error("expected ! operator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringEmpty(t *testing.T) {
|
||||
domain, err := ParseDomainString("[]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 0 {
|
||||
t.Errorf("expected 0 nodes, got %d", len(domain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringEmptyString(t *testing.T) {
|
||||
domain, err := ParseDomainString("", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 0 {
|
||||
t.Errorf("expected 0 nodes, got %d", len(domain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringNone(t *testing.T) {
|
||||
domain, err := ParseDomainString("[('field', '=', None)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != nil {
|
||||
t.Errorf("expected nil, got %v", cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringImplicitAnd(t *testing.T) {
|
||||
// Multiple leaves without explicit operator should be implicitly ANDed
|
||||
domain, err := ParseDomainString("[('a', '=', 1), ('b', '=', 2)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// normalizeDomainNodes wraps with And() → [&, leaf, leaf] = 3 nodes
|
||||
if len(domain) != 3 {
|
||||
t.Fatalf("expected 3 nodes (implicit AND), got %d", len(domain))
|
||||
}
|
||||
if domain[0] != OpAnd {
|
||||
t.Error("expected implicit & operator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringDoubleQuotes(t *testing.T) {
|
||||
domain, err := ParseDomainString(`[("name", "=", "test")]`, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(domain) != 1 {
|
||||
t.Fatalf("expected 1 node, got %d", len(domain))
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Field != "name" {
|
||||
t.Errorf("field: %s", cond.Field)
|
||||
}
|
||||
if cond.Value != "test" {
|
||||
t.Errorf("value: %v", cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringContextVar(t *testing.T) {
|
||||
// Without env, context vars should resolve to int64(0)
|
||||
domain, err := ParseDomainString("[('user_id', '=', user.id)]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != int64(0) {
|
||||
t.Errorf("expected int64(0), got %T %v", cond.Value, cond.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringInvalidSyntax(t *testing.T) {
|
||||
_, err := ParseDomainString("not a domain", nil)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid syntax")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringTupleAsList(t *testing.T) {
|
||||
// Some domain_force uses tuple syntax for list values
|
||||
domain, err := ParseDomainString("[('id', 'in', (1, 2, 3))]", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
vals, ok := cond.Value.([]int64)
|
||||
if !ok {
|
||||
t.Fatalf("expected []int64, got %T", cond.Value)
|
||||
}
|
||||
if len(vals) != 3 {
|
||||
t.Errorf("expected 3, got %d", len(vals))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomainStringEscapedQuote(t *testing.T) {
|
||||
domain, err := ParseDomainString(`[('name', '=', 'it\'s')]`, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cond := domain[0].(Condition)
|
||||
if cond.Value != "it's" {
|
||||
t.Errorf("expected it's, got %v", cond.Value)
|
||||
}
|
||||
}
|
||||
445
pkg/orm/domain_test.go
Normal file
445
pkg/orm/domain_test.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDomainCompileEmpty(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
where, params, err := dc.Compile(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != "TRUE" {
|
||||
t.Errorf("expected TRUE, got %s", where)
|
||||
}
|
||||
if len(params) != 0 {
|
||||
t.Errorf("expected 0 params, got %d", len(params))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileSimpleLeaf(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "=", "test")}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" = $1` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if len(params) != 1 || params[0] != "test" {
|
||||
t.Errorf("params: %v", params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileNullCheck(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "=", nil)}
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" IS NULL` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileFalseCheck(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("active", "=", false)}
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"active" IS NULL` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileNotEqualNull(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "!=", nil)}
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" IS NOT NULL` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileIn(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("id", "in", []int64{1, 2, 3})}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"id" IN ($1, $2, $3)` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if len(params) != 3 {
|
||||
t.Errorf("params: %v", params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileEmptyIn(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("id", "in", []int64{})}
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != "FALSE" {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileNotIn(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("id", "not in", []int64{1, 2})}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"id" NOT IN ($1, $2)` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if len(params) != 2 {
|
||||
t.Errorf("params: %v", params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileEmptyNotIn(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("id", "not in", []int64{})}
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != "TRUE" {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileLike(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "ilike", "test")}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" ILIKE $1` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if params[0] != "%test%" {
|
||||
t.Errorf("expected %%test%%, got %v", params[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileLikeWithWildcard(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "ilike", "test%")}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" ILIKE $1` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
// Value already contains wildcard, should not be wrapped again
|
||||
if params[0] != "test%" {
|
||||
t.Errorf("expected test%%, got %v", params[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileNotLike(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "not like", "foo")}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" NOT LIKE $1` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if params[0] != "%foo%" {
|
||||
t.Errorf("expected %%foo%%, got %v", params[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileExactLike(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "=like", "test")}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" LIKE $1` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
// =like does NOT auto-wrap
|
||||
if params[0] != "test" {
|
||||
t.Errorf("expected test, got %v", params[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileExactIlike(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("name", "=ilike", "Test")}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"name" ILIKE $1` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if params[0] != "Test" {
|
||||
t.Errorf("expected Test, got %v", params[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileAnd(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := And(Leaf("a", "=", 1), Leaf("b", "=", 2))
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := `("a" = $1 AND "b" = $2)`
|
||||
if where != expected {
|
||||
t.Errorf("expected %s, got %s", expected, where)
|
||||
}
|
||||
if len(params) != 2 {
|
||||
t.Errorf("params: %v", params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileOr(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Or(Leaf("a", "=", 1), Leaf("b", "=", 2))
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := `("a" = $1 OR "b" = $2)`
|
||||
if where != expected {
|
||||
t.Errorf("expected %s, got %s", expected, where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileNot(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Not(Leaf("active", "=", true))
|
||||
where, _, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := `(NOT "active" = $1)`
|
||||
if where != expected {
|
||||
t.Errorf("expected %s, got %s", expected, where)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileInvalidOperator(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("a", "INVALID", 1)}
|
||||
_, _, err := dc.Compile(domain)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid operator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileComparison(t *testing.T) {
|
||||
ops := []string{"<", ">", "<=", ">="}
|
||||
for _, op := range ops {
|
||||
t.Run(op, func(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("age", op, 18)}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := fmt.Sprintf(`"age" %s $1`, op)
|
||||
if where != expected {
|
||||
t.Errorf("expected %s, got %s", expected, where)
|
||||
}
|
||||
if len(params) != 1 || params[0] != 18 {
|
||||
t.Errorf("params: %v", params)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainCompileInStrings(t *testing.T) {
|
||||
dc := &DomainCompiler{model: &Model{table: "test"}}
|
||||
domain := Domain{Leaf("state", "in", []string{"draft", "sent"})}
|
||||
where, params, err := dc.Compile(domain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if where != `"state" IN ($1, $2)` {
|
||||
t.Errorf("got %s", where)
|
||||
}
|
||||
if len(params) != 2 {
|
||||
t.Errorf("params: %v", params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndEmpty(t *testing.T) {
|
||||
d := And()
|
||||
if d != nil {
|
||||
t.Errorf("expected nil, got %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndSingle(t *testing.T) {
|
||||
d := And(Leaf("a", "=", 1))
|
||||
if len(d) != 1 {
|
||||
t.Errorf("expected 1 node, got %d", len(d))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrEmpty(t *testing.T) {
|
||||
d := Or()
|
||||
if d != nil {
|
||||
t.Errorf("expected nil, got %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrSingle(t *testing.T) {
|
||||
d := Or(Leaf("a", "=", 1))
|
||||
if len(d) != 1 {
|
||||
t.Errorf("expected 1 node, got %d", len(d))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrMultiple(t *testing.T) {
|
||||
d := Or(Leaf("a", "=", 1), Leaf("b", "=", 2), Leaf("c", "=", 3))
|
||||
// Should have 2 OR operators + 3 leaves = 5 nodes
|
||||
if len(d) != 5 {
|
||||
t.Errorf("expected 5 nodes, got %d", len(d))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAndMultiple(t *testing.T) {
|
||||
d := And(Leaf("a", "=", 1), Leaf("b", "=", 2), Leaf("c", "=", 3))
|
||||
// Should have 2 AND operators + 3 leaves = 5 nodes
|
||||
if len(d) != 5 {
|
||||
t.Errorf("expected 5 nodes, got %d", len(d))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotDomain(t *testing.T) {
|
||||
d := Not(Leaf("active", "=", true))
|
||||
if len(d) != 2 {
|
||||
t.Errorf("expected 2 nodes, got %d", len(d))
|
||||
}
|
||||
if d[0] != OpNot {
|
||||
t.Errorf("expected OpNot, got %v", d[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeafCreation(t *testing.T) {
|
||||
c := Leaf("name", "=", "test")
|
||||
if c.Field != "name" {
|
||||
t.Errorf("field: %s", c.Field)
|
||||
}
|
||||
if c.Operator != "=" {
|
||||
t.Errorf("operator: %s", c.Operator)
|
||||
}
|
||||
if c.Value != "test" {
|
||||
t.Errorf("value: %v", c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapLikeValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input Value
|
||||
want Value
|
||||
}{
|
||||
{"plain string", "test", "%test%"},
|
||||
{"already has %", "test%", "test%"},
|
||||
{"already has _", "test_val", "test_val"},
|
||||
{"non-string", 42, 42},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := wrapLikeValue(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("wrapLikeValue(%v) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSlice(t *testing.T) {
|
||||
t.Run("[]int64", func(t *testing.T) {
|
||||
result := normalizeSlice([]int64{1, 2, 3})
|
||||
if len(result) != 3 {
|
||||
t.Errorf("expected 3, got %d", len(result))
|
||||
}
|
||||
})
|
||||
t.Run("[]string", func(t *testing.T) {
|
||||
result := normalizeSlice([]string{"a", "b"})
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2, got %d", len(result))
|
||||
}
|
||||
})
|
||||
t.Run("[]int", func(t *testing.T) {
|
||||
result := normalizeSlice([]int{1, 2})
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2, got %d", len(result))
|
||||
}
|
||||
})
|
||||
t.Run("[]float64", func(t *testing.T) {
|
||||
result := normalizeSlice([]float64{1.5, 2.5})
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2, got %d", len(result))
|
||||
}
|
||||
})
|
||||
t.Run("non-slice", func(t *testing.T) {
|
||||
result := normalizeSlice("not a slice")
|
||||
if result != nil {
|
||||
t.Errorf("expected nil, got %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestToInt64Slice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
val Value
|
||||
want []int64
|
||||
}{
|
||||
{"int64", int64(5), []int64{5}},
|
||||
{"int", int(3), []int64{3}},
|
||||
{"int32", int32(7), []int64{7}},
|
||||
{"float64", float64(9), []int64{9}},
|
||||
{"[]int64", []int64{1, 2}, []int64{1, 2}},
|
||||
{"[]int", []int{3, 4}, []int64{3, 4}},
|
||||
{"string", "bad", nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := toInt64Slice(tt.val)
|
||||
if tt.want == nil {
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("len: got %d, want %d", len(got), len(tt.want))
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("index %d: got %d, want %d", i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
330
pkg/orm/field_test.go
Normal file
330
pkg/orm/field_test.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package orm
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFieldIsCopyable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
field Field
|
||||
want bool
|
||||
}{
|
||||
{"regular char", Field{Name: "name", Type: TypeChar}, true},
|
||||
{"id field", Field{Name: "id", Type: TypeInteger}, false},
|
||||
{"create_uid", Field{Name: "create_uid", Type: TypeMany2one}, false},
|
||||
{"write_uid", Field{Name: "write_uid", Type: TypeMany2one}, false},
|
||||
{"create_date", Field{Name: "create_date", Type: TypeDatetime}, false},
|
||||
{"write_date", Field{Name: "write_date", Type: TypeDatetime}, false},
|
||||
{"password", Field{Name: "password", Type: TypeChar}, false},
|
||||
{"computed non-stored", Field{Name: "total", Type: TypeFloat, Compute: "x"}, false},
|
||||
{"computed stored", Field{Name: "total", Type: TypeFloat, Compute: "x", Store: true}, true},
|
||||
{"o2m", Field{Name: "lines", Type: TypeOne2many}, false},
|
||||
{"m2o", Field{Name: "partner_id", Type: TypeMany2one}, true},
|
||||
{"boolean", Field{Name: "active", Type: TypeBoolean}, true},
|
||||
{"explicit copy true", Field{Name: "ref", Type: TypeChar, Copy: true}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.field.IsCopyable()
|
||||
if got != tt.want {
|
||||
t.Errorf("IsCopyable() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldSQLType(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ FieldType
|
||||
want string
|
||||
}{
|
||||
{TypeChar, "varchar"},
|
||||
{TypeText, "text"},
|
||||
{TypeHTML, "text"},
|
||||
{TypeInteger, "int4"},
|
||||
{TypeFloat, "numeric"},
|
||||
{TypeMonetary, "numeric"},
|
||||
{TypeBoolean, "bool"},
|
||||
{TypeDate, "date"},
|
||||
{TypeDatetime, "timestamp without time zone"},
|
||||
{TypeMany2one, "int4"},
|
||||
{TypeOne2many, ""},
|
||||
{TypeMany2many, ""},
|
||||
{TypeJson, "jsonb"},
|
||||
{TypeProperties, "jsonb"},
|
||||
{TypeBinary, "bytea"},
|
||||
{TypeSelection, "varchar"},
|
||||
{TypeReference, "varchar"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.typ.String(), func(t *testing.T) {
|
||||
got := tt.typ.SQLType()
|
||||
if got != tt.want {
|
||||
t.Errorf("SQLType() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldSQLTypeWithSize(t *testing.T) {
|
||||
f := &Field{Type: TypeChar, Size: 64}
|
||||
got := f.SQLType()
|
||||
if got != "varchar(64)" {
|
||||
t.Errorf("expected varchar(64), got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldTypeString(t *testing.T) {
|
||||
if TypeChar.String() != "char" {
|
||||
t.Error("expected char")
|
||||
}
|
||||
if TypeMany2one.String() != "many2one" {
|
||||
t.Error("expected many2one")
|
||||
}
|
||||
if TypeBoolean.String() != "boolean" {
|
||||
t.Error("expected boolean")
|
||||
}
|
||||
if TypeText.String() != "text" {
|
||||
t.Error("expected text")
|
||||
}
|
||||
if TypeInteger.String() != "integer" {
|
||||
t.Error("expected integer")
|
||||
}
|
||||
if TypeFloat.String() != "float" {
|
||||
t.Error("expected float")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldTypeIsRelational(t *testing.T) {
|
||||
if !TypeMany2one.IsRelational() {
|
||||
t.Error("m2o should be relational")
|
||||
}
|
||||
if !TypeOne2many.IsRelational() {
|
||||
t.Error("o2m should be relational")
|
||||
}
|
||||
if !TypeMany2many.IsRelational() {
|
||||
t.Error("m2m should be relational")
|
||||
}
|
||||
if TypeChar.IsRelational() {
|
||||
t.Error("char should not be relational")
|
||||
}
|
||||
if TypeInteger.IsRelational() {
|
||||
t.Error("integer should not be relational")
|
||||
}
|
||||
if TypeBoolean.IsRelational() {
|
||||
t.Error("boolean should not be relational")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldTypeIsStored(t *testing.T) {
|
||||
if !TypeChar.IsStored() {
|
||||
t.Error("char should be stored")
|
||||
}
|
||||
if !TypeMany2one.IsStored() {
|
||||
t.Error("m2o should be stored")
|
||||
}
|
||||
if TypeOne2many.IsStored() {
|
||||
t.Error("o2m should not be stored")
|
||||
}
|
||||
if TypeMany2many.IsStored() {
|
||||
t.Error("m2m should not be stored")
|
||||
}
|
||||
if !TypeBoolean.IsStored() {
|
||||
t.Error("boolean should be stored")
|
||||
}
|
||||
if !TypeInteger.IsStored() {
|
||||
t.Error("integer should be stored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldIsStored(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
f Field
|
||||
want bool
|
||||
}{
|
||||
{"plain char", Field{Type: TypeChar}, true},
|
||||
{"computed not stored", Field{Type: TypeChar, Compute: "x"}, false},
|
||||
{"computed stored", Field{Type: TypeChar, Compute: "x", Store: true}, true},
|
||||
{"related not stored", Field{Type: TypeChar, Related: "partner_id.name"}, false},
|
||||
{"related stored", Field{Type: TypeChar, Related: "partner_id.name", Store: true}, true},
|
||||
{"o2m", Field{Type: TypeOne2many}, false},
|
||||
{"m2m", Field{Type: TypeMany2many}, false},
|
||||
{"m2o", Field{Type: TypeMany2one}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.f.IsStored()
|
||||
if got != tt.want {
|
||||
t.Errorf("IsStored() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldColumn(t *testing.T) {
|
||||
f := &Field{Name: "partner_id", column: "partner_id"}
|
||||
if f.Column() != "partner_id" {
|
||||
t.Errorf("expected partner_id, got %s", f.Column())
|
||||
}
|
||||
|
||||
f2 := &Field{Name: "custom", column: "custom_col"}
|
||||
if f2.Column() != "custom_col" {
|
||||
t.Errorf("expected custom_col, got %s", f2.Column())
|
||||
}
|
||||
|
||||
// When column is empty, falls back to Name
|
||||
f3 := &Field{Name: "fallback"}
|
||||
if f3.Column() != "fallback" {
|
||||
t.Errorf("expected fallback, got %s", f3.Column())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldConstructors(t *testing.T) {
|
||||
t.Run("Char", func(t *testing.T) {
|
||||
f := Char("name", FieldOpts{String: "Name", Required: true})
|
||||
if f.Type != TypeChar {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
if f.Name != "name" {
|
||||
t.Errorf("name: %s", f.Name)
|
||||
}
|
||||
if !f.Required {
|
||||
t.Error("expected required")
|
||||
}
|
||||
if f.String != "Name" {
|
||||
t.Errorf("string: %s", f.String)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Integer", func(t *testing.T) {
|
||||
f := Integer("count", FieldOpts{})
|
||||
if f.Type != TypeInteger {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Boolean", func(t *testing.T) {
|
||||
f := Boolean("active", FieldOpts{Default: true})
|
||||
if f.Type != TypeBoolean {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
if f.Default != true {
|
||||
t.Error("expected default true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Many2one", func(t *testing.T) {
|
||||
f := Many2one("partner_id", "res.partner", FieldOpts{String: "Partner"})
|
||||
if f.Type != TypeMany2one {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
if f.Comodel != "res.partner" {
|
||||
t.Errorf("comodel: %s", f.Comodel)
|
||||
}
|
||||
if !f.Index {
|
||||
t.Error("M2O should be auto-indexed")
|
||||
}
|
||||
if f.OnDelete != OnDeleteSetNull {
|
||||
t.Errorf("expected set null, got %s", f.OnDelete)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("One2many", func(t *testing.T) {
|
||||
f := One2many("line_ids", "sale.order.line", "order_id", FieldOpts{})
|
||||
if f.Type != TypeOne2many {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
if f.InverseField != "order_id" {
|
||||
t.Errorf("inverse: %s", f.InverseField)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Many2many", func(t *testing.T) {
|
||||
f := Many2many("tag_ids", "res.partner.tag", FieldOpts{})
|
||||
if f.Type != TypeMany2many {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
if f.Comodel != "res.partner.tag" {
|
||||
t.Errorf("comodel: %s", f.Comodel)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Text", func(t *testing.T) {
|
||||
f := Text("description", FieldOpts{})
|
||||
if f.Type != TypeText {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Float", func(t *testing.T) {
|
||||
f := Float("amount", FieldOpts{})
|
||||
if f.Type != TypeFloat {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Date", func(t *testing.T) {
|
||||
f := Date("birthday", FieldOpts{})
|
||||
if f.Type != TypeDate {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Datetime", func(t *testing.T) {
|
||||
f := Datetime("created", FieldOpts{})
|
||||
if f.Type != TypeDatetime {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Binary", func(t *testing.T) {
|
||||
f := Binary("image", FieldOpts{})
|
||||
if f.Type != TypeBinary {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Json", func(t *testing.T) {
|
||||
f := Json("data", FieldOpts{})
|
||||
if f.Type != TypeJson {
|
||||
t.Errorf("type: %s", f.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default label from name", func(t *testing.T) {
|
||||
f := Char("my_field", FieldOpts{})
|
||||
if f.String != "my_field" {
|
||||
t.Errorf("expected my_field as default label, got %s", f.String)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFieldResolveDefault(t *testing.T) {
|
||||
t.Run("nil default", func(t *testing.T) {
|
||||
f := &Field{Default: nil}
|
||||
if f.ResolveDefault() != nil {
|
||||
t.Error("expected nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string default", func(t *testing.T) {
|
||||
f := &Field{Default: "hello"}
|
||||
if f.ResolveDefault() != "hello" {
|
||||
t.Error("expected hello")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bool default", func(t *testing.T) {
|
||||
f := &Field{Default: true}
|
||||
if f.ResolveDefault() != true {
|
||||
t.Error("expected true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("int default", func(t *testing.T) {
|
||||
f := &Field{Default: 42}
|
||||
if f.ResolveDefault() != 42 {
|
||||
t.Error("expected 42")
|
||||
}
|
||||
})
|
||||
}
|
||||
373
pkg/orm/model_test.go
Normal file
373
pkg/orm/model_test.go
Normal file
@@ -0,0 +1,373 @@
|
||||
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
|
||||
}
|
||||
109
pkg/orm/sanitize_test.go
Normal file
109
pkg/orm/sanitize_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package orm
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeFieldValueBoolFalse(t *testing.T) {
|
||||
// false for boolean field should stay false
|
||||
f := &Field{Type: TypeBoolean}
|
||||
got := sanitizeFieldValue(f, false)
|
||||
if got != false {
|
||||
t.Errorf("expected false, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueBoolTrue(t *testing.T) {
|
||||
f := &Field{Type: TypeBoolean}
|
||||
got := sanitizeFieldValue(f, true)
|
||||
if got != true {
|
||||
t.Errorf("expected true, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueCharFalse(t *testing.T) {
|
||||
// false for char field should become nil
|
||||
f := &Field{Type: TypeChar}
|
||||
got := sanitizeFieldValue(f, false)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueIntFalse(t *testing.T) {
|
||||
f := &Field{Type: TypeInteger}
|
||||
got := sanitizeFieldValue(f, false)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueFloatFalse(t *testing.T) {
|
||||
f := &Field{Type: TypeFloat}
|
||||
got := sanitizeFieldValue(f, false)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueM2OFalse(t *testing.T) {
|
||||
f := &Field{Type: TypeMany2one}
|
||||
got := sanitizeFieldValue(f, false)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueDateFalse(t *testing.T) {
|
||||
f := &Field{Type: TypeDate}
|
||||
got := sanitizeFieldValue(f, false)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueFloat64ToInt(t *testing.T) {
|
||||
f := &Field{Type: TypeInteger}
|
||||
got := sanitizeFieldValue(f, float64(42))
|
||||
if got != int64(42) {
|
||||
t.Errorf("expected int64(42), got %T %v", got, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueM2OFloat(t *testing.T) {
|
||||
f := &Field{Type: TypeMany2one}
|
||||
got := sanitizeFieldValue(f, float64(5))
|
||||
if got != int64(5) {
|
||||
t.Errorf("expected int64(5), got %T %v", got, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueNil(t *testing.T) {
|
||||
f := &Field{Type: TypeChar}
|
||||
got := sanitizeFieldValue(f, nil)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValuePassthrough(t *testing.T) {
|
||||
f := &Field{Type: TypeChar}
|
||||
got := sanitizeFieldValue(f, "hello")
|
||||
if got != "hello" {
|
||||
t.Errorf("expected hello, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueIntPassthrough(t *testing.T) {
|
||||
f := &Field{Type: TypeInteger}
|
||||
got := sanitizeFieldValue(f, int64(99))
|
||||
if got != int64(99) {
|
||||
t.Errorf("expected int64(99), got %T %v", got, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFieldValueFloatPassthrough(t *testing.T) {
|
||||
f := &Field{Type: TypeFloat}
|
||||
got := sanitizeFieldValue(f, float64(3.14))
|
||||
if got != float64(3.14) {
|
||||
t.Errorf("expected 3.14, got %v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user