Files
goodie/pkg/orm/domain_test.go
Marc fad2a37d1c 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>
2026-04-03 23:39:41 +02:00

446 lines
10 KiB
Go

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])
}
}
})
}
}