[🤖] Add Unit-Tests
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m34s

This commit is contained in:
2026-04-27 10:46:08 +02:00
parent dad0e3240d
commit 02d6894ec6
116 changed files with 18795 additions and 1 deletions
+213
View File
@@ -0,0 +1,213 @@
package sq
import (
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"strings"
"testing"
)
func TestBuildInsertStatementBasic(t *testing.T) {
type r struct {
ID string `db:"id"`
Name string `db:"name"`
}
q := fakeQueryable{}
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Name: "alice"})
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(sqlstr, "INSERT INTO users ("))
tst.AssertTrue(t, strings.Contains(sqlstr, "id"))
tst.AssertTrue(t, strings.Contains(sqlstr, "name"))
tst.AssertEqual(t, 2, len(pp))
values := []any{}
for _, v := range pp {
values = append(values, v)
}
hasID, hasName := false, false
for _, v := range values {
if vs, ok := v.(string); ok {
if vs == "1" {
hasID = true
}
if vs == "alice" {
hasName = true
}
}
}
tst.AssertTrue(t, hasID)
tst.AssertTrue(t, hasName)
}
func TestBuildInsertStatementSkipsUnexported(t *testing.T) {
type r struct {
ID string `db:"id"`
hidden string `db:"hidden"` //nolint:unused
}
q := fakeQueryable{}
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1"})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 1, len(pp))
tst.AssertTrue(t, !strings.Contains(sqlstr, "hidden"))
}
func TestBuildInsertStatementSkipsNoTagAndDash(t *testing.T) {
type r struct {
ID string `db:"id"`
Skip1 string `db:"-"`
Skip2 string
}
q := fakeQueryable{}
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Skip1: "x", Skip2: "y"})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 1, len(pp))
tst.AssertTrue(t, !strings.Contains(sqlstr, "Skip"))
}
func TestBuildInsertStatementNoFields(t *testing.T) {
type r struct {
Skip string
}
q := fakeQueryable{}
_, _, err := BuildInsertStatement(q, "x", r{})
if err == nil {
t.Fatal("expected error for no usable fields")
}
}
func TestBuildInsertStatementNilPointer(t *testing.T) {
type r struct {
ID string `db:"id"`
Note *string `db:"note"`
}
q := fakeQueryable{}
sqlstr, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Note: nil})
tst.AssertNoErr(t, err)
// Only id is parameterized; nil pointer becomes literal NULL
tst.AssertEqual(t, 1, len(pp))
tst.AssertTrue(t, strings.Contains(sqlstr, "NULL"))
}
func TestBuildInsertStatementWithConverter(t *testing.T) {
type r struct {
ID string `db:"id"`
Flag bool `db:"flag"`
}
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
_, pp, err := BuildInsertStatement(q, "users", r{ID: "1", Flag: true})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 2, len(pp))
foundOne := false
for _, v := range pp {
if vi, ok := v.(int64); ok && vi == 1 {
foundOne = true
}
}
tst.AssertTrue(t, foundOne)
}
func TestBuildUpdateStatementBasic(t *testing.T) {
type r struct {
ID string `db:"id"`
Name string `db:"name"`
}
q := fakeQueryable{}
sqlstr, pp, err := BuildUpdateStatement(q, "users", r{ID: "1", Name: "alice"}, "id")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.HasPrefix(sqlstr, "UPDATE users SET "))
tst.AssertTrue(t, strings.Contains(sqlstr, "name = :"))
tst.AssertTrue(t, strings.Contains(sqlstr, "(id = :"))
tst.AssertEqual(t, 2, len(pp))
}
func TestBuildUpdateStatementMissingID(t *testing.T) {
type r struct {
Name string `db:"name"`
}
q := fakeQueryable{}
_, _, err := BuildUpdateStatement(q, "users", r{Name: "alice"}, "id")
if err == nil {
t.Fatal("expected error for missing id column")
}
}
func TestBuildUpdateStatementOnlyID(t *testing.T) {
type r struct {
ID string `db:"id"`
}
q := fakeQueryable{}
_, _, err := BuildUpdateStatement(q, "users", r{ID: "1"}, "id")
if err == nil {
t.Fatal("expected error when no SET clauses")
}
}
func TestBuildUpdateStatementNilPointer(t *testing.T) {
type r struct {
ID string `db:"id"`
Note *string `db:"note"`
}
q := fakeQueryable{}
sqlstr, _, err := BuildUpdateStatement(q, "users", r{ID: "1", Note: nil}, "id")
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.Contains(sqlstr, "note = NULL"))
}
func TestBuildInsertMultipleStatementBasic(t *testing.T) {
type r struct {
ID string `db:"id"`
Name string `db:"name"`
}
q := fakeQueryable{}
sqlstr, pp, err := BuildInsertMultipleStatement(q, "users", []r{
{ID: "1", Name: "alice"},
{ID: "2", Name: "bob"},
})
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.Contains(sqlstr, `INSERT INTO "users"`))
tst.AssertTrue(t, strings.Contains(sqlstr, `"id"`))
tst.AssertTrue(t, strings.Contains(sqlstr, `"name"`))
// 2 rows × 2 fields = 4 placeholders
tst.AssertEqual(t, 4, len(pp))
// Two value tuples should appear -> exactly one "), (" separator
tst.AssertEqual(t, 1, strings.Count(sqlstr, "), ("))
}
func TestBuildInsertMultipleStatementEmpty(t *testing.T) {
type r struct {
ID string `db:"id"`
}
q := fakeQueryable{}
_, _, err := BuildInsertMultipleStatement(q, "x", []r{})
if err == nil {
t.Fatal("expected error for empty input")
}
}
func TestBuildInsertMultipleStatementNilPointer(t *testing.T) {
type r struct {
ID string `db:"id"`
Note *string `db:"note"`
}
q := fakeQueryable{}
sqlstr, _, err := BuildInsertMultipleStatement(q, "users", []r{{ID: "1", Note: nil}})
tst.AssertNoErr(t, err)
tst.AssertTrue(t, strings.Contains(sqlstr, "NULL"))
}
+74
View File
@@ -0,0 +1,74 @@
package sq
import (
"context"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"strings"
"testing"
)
func TestFnTrimCommentsLineOnly(t *testing.T) {
// The line-only comment is replaced with an empty line.
sql := "SELECT *\n-- this is a comment\nFROM users"
pp := PP{}
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, !strings.Contains(sql, "comment"))
tst.AssertTrue(t, strings.Contains(sql, "SELECT *"))
tst.AssertTrue(t, strings.Contains(sql, "FROM users"))
}
func TestFnTrimCommentsTrailing(t *testing.T) {
sql := "SELECT * -- inline\nFROM users -- end"
pp := PP{}
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "SELECT *\nFROM users", sql)
}
func TestFnTrimCommentsIndented(t *testing.T) {
sql := "SELECT *\n -- indented comment\nFROM users"
pp := PP{}
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
tst.AssertNoErr(t, err)
tst.AssertTrue(t, !strings.Contains(sql, "indented comment"))
tst.AssertTrue(t, strings.Contains(sql, "SELECT *"))
tst.AssertTrue(t, strings.Contains(sql, "FROM users"))
}
func TestFnTrimCommentsTrimsTrailingWhitespace(t *testing.T) {
sql := "SELECT * \t\nFROM users "
pp := PP{}
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "SELECT *\nFROM users", sql)
}
func TestFnTrimCommentsNoComment(t *testing.T) {
sql := "SELECT id\nFROM users\nWHERE id=1"
pp := PP{}
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "SELECT id\nFROM users\nWHERE id=1", sql)
}
func TestFnTrimCommentsEmpty(t *testing.T) {
sql := ""
pp := PP{}
err := fnTrimComments(context.Background(), "QUERY", nil, &sql, &pp)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "", sql)
}
func TestCommentTrimmerListener(t *testing.T) {
sql := "SELECT *\n-- a comment\nFROM x"
pp := PP{}
err := CommentTrimmer.PreQuery(context.Background(), nil, &sql, &pp, PreQueryMeta{})
tst.AssertNoErr(t, err)
tst.AssertTrue(t, !strings.Contains(sql, "a comment"))
sql2 := "INSERT INTO x VALUES (1) -- xx"
err = CommentTrimmer.PreExec(context.Background(), nil, &sql2, &pp, PreExecMeta{})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "INSERT INTO x VALUES (1)", sql2)
}
+199
View File
@@ -0,0 +1,199 @@
package sq
import (
"git.blackforestbytes.com/BlackForestBytes/goext/rfctime"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing"
"time"
)
func TestConverterBoolToBit(t *testing.T) {
v, err := ConverterBoolToBit.ModelToDB(true)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(1), v.(int64))
v, err = ConverterBoolToBit.ModelToDB(false)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(0), v.(int64))
v, err = ConverterBoolToBit.DBToModel(int64(1))
tst.AssertNoErr(t, err)
tst.AssertEqual(t, true, v.(bool))
v, err = ConverterBoolToBit.DBToModel(int64(0))
tst.AssertNoErr(t, err)
tst.AssertEqual(t, false, v.(bool))
_, err = ConverterBoolToBit.DBToModel(int64(2))
if err == nil {
t.Fatal("expected error for value not in {0,1}")
}
}
func TestConverterTimeToUnixMillis(t *testing.T) {
t0 := time.Date(2024, 6, 15, 12, 34, 56, int(789*time.Millisecond), time.UTC)
expected := t0.UnixMilli()
v, err := ConverterTimeToUnixMillis.ModelToDB(t0)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(int64))
v, err = ConverterTimeToUnixMillis.DBToModel(expected)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(time.Time).UnixMilli())
}
func TestConverterRFCUnixMilliTime(t *testing.T) {
t0 := rfctime.NewUnixMilli(time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC))
expected := t0.UnixMilli()
v, err := ConverterRFCUnixMilliTimeToUnixMillis.ModelToDB(t0)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(int64))
v, err = ConverterRFCUnixMilliTimeToUnixMillis.DBToModel(expected)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(rfctime.UnixMilliTime).UnixMilli())
}
func TestConverterRFCUnixNanoTime(t *testing.T) {
t0 := rfctime.NewUnixNano(time.Date(2020, 1, 2, 3, 4, 5, 123456789, time.UTC))
expected := t0.UnixNano()
v, err := ConverterRFCUnixNanoTimeToUnixNanos.ModelToDB(t0)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(int64))
v, err = ConverterRFCUnixNanoTimeToUnixNanos.DBToModel(expected)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(rfctime.UnixNanoTime).UnixNano())
}
func TestConverterRFCUnixTime(t *testing.T) {
t0 := rfctime.NewUnix(time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC))
expected := t0.Unix()
v, err := ConverterRFCUnixTimeToUnixSeconds.ModelToDB(t0)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(int64))
v, err = ConverterRFCUnixTimeToUnixSeconds.DBToModel(expected)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, expected, v.(rfctime.UnixTime).Unix())
}
func TestConverterRFC339Time(t *testing.T) {
t0 := rfctime.NewRFC3339(time.Date(2020, 6, 15, 9, 30, 45, 0, time.UTC))
v, err := ConverterRFC339TimeToString.ModelToDB(t0)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "2020-06-15 09:30:45", v.(string))
v, err = ConverterRFC339TimeToString.DBToModel("2020-06-15 09:30:45")
tst.AssertNoErr(t, err)
tt := v.(rfctime.RFC3339Time).Time().UTC()
tst.AssertEqual(t, 2020, tt.Year())
tst.AssertEqual(t, time.June, tt.Month())
tst.AssertEqual(t, 15, tt.Day())
tst.AssertEqual(t, 9, tt.Hour())
tst.AssertEqual(t, 30, tt.Minute())
tst.AssertEqual(t, 45, tt.Second())
_, err = ConverterRFC339TimeToString.DBToModel("garbage")
if err == nil {
t.Fatal("expected parse error")
}
}
func TestConverterRFC339NanoTime(t *testing.T) {
t0 := rfctime.NewRFC3339Nano(time.Date(2020, 6, 15, 9, 30, 45, 123456789, time.UTC))
v, err := ConverterRFC339NanoTimeToString.ModelToDB(t0)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "2020-06-15 09:30:45.123456789", v.(string))
v, err = ConverterRFC339NanoTimeToString.DBToModel("2020-06-15 09:30:45.123456789")
tst.AssertNoErr(t, err)
tt := v.(rfctime.RFC3339NanoTime).Time().UTC()
tst.AssertEqual(t, 123456789, tt.Nanosecond())
_, err = ConverterRFC339NanoTimeToString.DBToModel("not a date")
if err == nil {
t.Fatal("expected parse error")
}
}
func TestConverterRFCDate(t *testing.T) {
d := rfctime.Date{Year: 2024, Month: 3, Day: 9}
v, err := ConverterRFCDateToString.ModelToDB(d)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "2024-03-09", v.(string))
v, err = ConverterRFCDateToString.DBToModel("2024-03-09")
tst.AssertNoErr(t, err)
d2 := v.(rfctime.Date)
tst.AssertEqual(t, 2024, d2.Year)
tst.AssertEqual(t, 3, d2.Month)
tst.AssertEqual(t, 9, d2.Day)
_, err = ConverterRFCDateToString.DBToModel("invalid")
if err == nil {
t.Fatal("expected parse error")
}
}
func TestConverterRFCTime(t *testing.T) {
tm := rfctime.NewTime(13, 30, 45, 0)
v, err := ConverterRFCTimeToString.ModelToDB(tm)
tst.AssertNoErr(t, err)
roundtrip, err := ConverterRFCTimeToString.DBToModel(v.(string))
tst.AssertNoErr(t, err)
tm2 := roundtrip.(rfctime.Time)
tst.AssertEqual(t, 13, tm2.Hour)
tst.AssertEqual(t, 30, tm2.Minute)
tst.AssertEqual(t, 45, tm2.Second)
_, err = ConverterRFCTimeToString.DBToModel("xx:xx:xx")
if err == nil {
t.Fatal("expected parse error")
}
}
func TestConverterRFCSecondsF64(t *testing.T) {
d := 12*time.Second + 500*time.Millisecond
s := rfctime.NewSecondsF64(d)
v, err := ConverterRFCSecondsF64ToString.ModelToDB(s)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 12.5, v.(float64))
v, err = ConverterRFCSecondsF64ToString.DBToModel(12.5)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 12.5, v.(rfctime.SecondsF64).Seconds())
}
func TestConverterJsonObjToString(t *testing.T) {
tst.AssertEqual(t, "sq.JsonObj", ConverterJsonObjToString.ModelTypeString())
tst.AssertEqual(t, "string", ConverterJsonObjToString.DBTypeString())
v, err := ConverterJsonObjToString.ModelToDB(JsonObj{"x": float64(1)})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, `{"x":1}`, v.(string))
r, err := ConverterJsonObjToString.DBToModel(`{"x":1}`)
tst.AssertNoErr(t, err)
tst.AssertStrRepEqual(t, r.(JsonObj)["x"], float64(1))
}
func TestConverterJsonArrToString(t *testing.T) {
tst.AssertEqual(t, "sq.JsonArr", ConverterJsonArrToString.ModelTypeString())
tst.AssertEqual(t, "string", ConverterJsonArrToString.DBTypeString())
v, err := ConverterJsonArrToString.ModelToDB(JsonArr{float64(1), float64(2)})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, `[1,2]`, v.(string))
r, err := ConverterJsonArrToString.DBToModel(`[1,2]`)
tst.AssertNoErr(t, err)
arr := r.(JsonArr)
tst.AssertEqual(t, 2, len(arr))
}
+154
View File
@@ -0,0 +1,154 @@
package sq
import (
"context"
"database/sql"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"github.com/jmoiron/sqlx"
"testing"
)
type fakeQueryable struct {
converters []DBTypeConverter
}
func (f fakeQueryable) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Result, error) {
return nil, errors.New("not implemented")
}
func (f fakeQueryable) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Rows, error) {
return nil, errors.New("not implemented")
}
func (f fakeQueryable) ListConverter() []DBTypeConverter {
return f.converters
}
func TestNewDBTypeConverterTypeStrings(t *testing.T) {
conv := NewDBTypeConverter(func(v bool) (int64, error) { return 0, nil }, func(v int64) (bool, error) { return false, nil })
tst.AssertEqual(t, "bool", conv.ModelTypeString())
tst.AssertEqual(t, "int64", conv.DBTypeString())
}
func TestNewDBTypeConverterModelToDB(t *testing.T) {
conv := NewDBTypeConverter(func(v bool) (int64, error) {
if v {
return 1, nil
}
return 0, nil
}, func(v int64) (bool, error) {
return v != 0, nil
})
r, err := conv.ModelToDB(true)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(1), r.(int64))
r, err = conv.ModelToDB(false)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(0), r.(int64))
}
func TestNewDBTypeConverterDBToModel(t *testing.T) {
conv := NewDBTypeConverter(func(v bool) (int64, error) { return 0, nil }, func(v int64) (bool, error) {
return v != 0, nil
})
r, err := conv.DBToModel(int64(1))
tst.AssertNoErr(t, err)
tst.AssertEqual(t, true, r.(bool))
r, err = conv.DBToModel(int64(0))
tst.AssertNoErr(t, err)
tst.AssertEqual(t, false, r.(bool))
}
func TestNewDBTypeConverterTypeMismatch(t *testing.T) {
conv := NewDBTypeConverter(func(v bool) (int64, error) { return 0, nil }, func(v int64) (bool, error) { return false, nil })
_, err := conv.ModelToDB("not a bool")
if err == nil {
t.Fatal("expected error on type mismatch in ModelToDB")
}
_, err = conv.DBToModel("not int64")
if err == nil {
t.Fatal("expected error on type mismatch in DBToModel")
}
}
func TestNewAutoDBTypeConverter(t *testing.T) {
conv := NewAutoDBTypeConverter(JsonObj{})
tst.AssertEqual(t, "sq.JsonObj", conv.ModelTypeString())
tst.AssertEqual(t, "string", conv.DBTypeString())
r, err := conv.ModelToDB(JsonObj{"k": "v"})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, `{"k":"v"}`, r.(string))
r, err = conv.DBToModel(`{"k":"v"}`)
tst.AssertNoErr(t, err)
parsed, ok := r.(JsonObj)
tst.AssertTrue(t, ok)
tst.AssertStrRepEqual(t, parsed["k"], "v")
}
func TestConvertValueToDBNoConverter(t *testing.T) {
q := fakeQueryable{}
r, err := convertValueToDB(q, "hello")
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "hello", r.(string))
r, err = convertValueToDB(q, 42)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 42, r.(int))
}
func TestConvertValueToDBWithConverter(t *testing.T) {
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
r, err := convertValueToDB(q, true)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(1), r.(int64))
r, err = convertValueToDB(q, false)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(0), r.(int64))
}
func TestConvertValueToDBNilPointer(t *testing.T) {
q := fakeQueryable{}
var s *string
r, err := convertValueToDB(q, s)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, true, r == nil)
}
func TestConvertValueToDBNonNilPointer(t *testing.T) {
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
v := true
r, err := convertValueToDB(q, &v)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, int64(1), r.(int64))
}
func TestConvertValueToModelNoConverter(t *testing.T) {
q := fakeQueryable{}
r, err := convertValueToModel(q, "hello", "string")
tst.AssertNoErr(t, err)
tst.AssertEqual(t, "hello", r.(string))
}
func TestConvertValueToModelWithConverter(t *testing.T) {
q := fakeQueryable{converters: []DBTypeConverter{ConverterBoolToBit}}
r, err := convertValueToModel(q, int64(1), "bool")
tst.AssertNoErr(t, err)
tst.AssertEqual(t, true, r.(bool))
}
+70
View File
@@ -0,0 +1,70 @@
package sq
import (
ct "git.blackforestbytes.com/BlackForestBytes/goext/cursortoken"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing"
)
func TestNewEmptyPaginateFilter(t *testing.T) {
f := NewEmptyPaginateFilter()
pp := PP{}
flt, join, joinTbl := f.SQL(pp)
tst.AssertEqual(t, "1=1", flt)
tst.AssertEqual(t, "", join)
tst.AssertEqual(t, 0, len(joinTbl))
tst.AssertEqual(t, 0, len(pp))
tst.AssertEqual(t, 0, len(f.Sort()))
}
func TestNewSimplePaginateFilter(t *testing.T) {
sortOrder := []FilterSort{
{Field: "name", Direction: ct.SortASC},
}
filterParams := PP{"p1": "value1"}
f := NewSimplePaginateFilter("name = :p1", filterParams, sortOrder)
pp := PP{}
flt, join, joinTbl := f.SQL(pp)
tst.AssertEqual(t, "name = :p1", flt)
tst.AssertEqual(t, "", join)
tst.AssertEqual(t, 0, len(joinTbl))
// filterParams should be merged into pp
tst.AssertEqual(t, 1, len(pp))
tst.AssertEqual(t, "value1", pp["p1"])
srt := f.Sort()
tst.AssertEqual(t, 1, len(srt))
tst.AssertEqual(t, "name", srt[0].Field)
tst.AssertEqual(t, ct.SortASC, srt[0].Direction)
}
func TestNewPaginateFilter(t *testing.T) {
sortOrder := []FilterSort{
{Field: "id", Direction: ct.SortDESC},
}
called := 0
f := NewPaginateFilter(func(params PP) (string, string, []string) {
called++
params.Add("hello")
return "id > 0", "JOIN other ON other.id = main.id", []string{"other"}
}, sortOrder)
pp := PP{}
flt, join, joinTbl := f.SQL(pp)
tst.AssertEqual(t, "id > 0", flt)
tst.AssertEqual(t, "JOIN other ON other.id = main.id", join)
tst.AssertEqual(t, 1, len(joinTbl))
tst.AssertEqual(t, "other", joinTbl[0])
tst.AssertEqual(t, 1, called)
tst.AssertEqual(t, 1, len(pp))
srt := f.Sort()
tst.AssertEqual(t, 1, len(srt))
tst.AssertEqual(t, "id", srt[0].Field)
tst.AssertEqual(t, ct.SortDESC, srt[0].Direction)
}
+85
View File
@@ -0,0 +1,85 @@
package sq
import (
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing"
)
func TestJsonObjMarshalToDB(t *testing.T) {
j := JsonObj{}
out, err := j.MarshalToDB(JsonObj{"key": "value", "num": float64(7)})
tst.AssertNoErr(t, err)
// JSON map ordering is not guaranteed - parse and verify
roundtrip, err := j.UnmarshalToModel(out)
tst.AssertNoErr(t, err)
tst.AssertStrRepEqual(t, roundtrip["key"], "value")
tst.AssertStrRepEqual(t, roundtrip["num"], float64(7))
}
func TestJsonObjUnmarshalToModelInvalid(t *testing.T) {
j := JsonObj{}
_, err := j.UnmarshalToModel("{not valid json}")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
func TestJsonArrMarshalToDB(t *testing.T) {
j := JsonArr{}
out, err := j.MarshalToDB(JsonArr{float64(1), "two", true})
tst.AssertNoErr(t, err)
tst.AssertEqual(t, `[1,"two",true]`, out)
}
func TestJsonArrRoundtrip(t *testing.T) {
j := JsonArr{}
out, err := j.MarshalToDB(JsonArr{"a", "b", "c"})
tst.AssertNoErr(t, err)
r, err := j.UnmarshalToModel(out)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 3, len(r))
tst.AssertEqual(t, "a", r[0])
tst.AssertEqual(t, "b", r[1])
tst.AssertEqual(t, "c", r[2])
}
func TestJsonArrUnmarshalInvalid(t *testing.T) {
j := JsonArr{}
_, err := j.UnmarshalToModel("not json")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
func TestAutoJsonRoundtrip(t *testing.T) {
type inner struct {
A int `json:"a"`
B string `json:"b"`
}
aj := AutoJson[inner]{Value: inner{A: 42, B: "foo"}}
zero := AutoJson[inner]{}
out, err := zero.MarshalToDB(aj)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, `{"a":42,"b":"foo"}`, out)
r, err := zero.UnmarshalToModel(out)
tst.AssertNoErr(t, err)
tst.AssertEqual(t, 42, r.Value.A)
tst.AssertEqual(t, "foo", r.Value.B)
}
func TestAutoJsonUnmarshalInvalid(t *testing.T) {
type inner struct {
A int `json:"a"`
}
zero := AutoJson[inner]{}
_, err := zero.UnmarshalToModel("garbage")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
+208
View File
@@ -0,0 +1,208 @@
package sq
import (
"context"
"errors"
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"testing"
)
func TestNewPrePingListener(t *testing.T) {
called := 0
expected := errors.New("ping err")
l := NewPrePingListener(func(ctx context.Context, meta PrePingMeta) error {
called++
return expected
})
err := l.PrePing(context.Background(), PrePingMeta{})
tst.AssertEqual(t, 1, called)
tst.AssertTrue(t, errors.Is(err, expected))
// Other handlers must not error / panic
tst.AssertNoErr(t, l.PreTxBegin(context.Background(), 0, PreTxBeginMeta{}))
tst.AssertNoErr(t, l.PreTxCommit(0, PreTxCommitMeta{}))
tst.AssertNoErr(t, l.PreTxRollback(0, PreTxRollbackMeta{}))
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, nil, nil, PreQueryMeta{}))
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, nil, nil, PreExecMeta{}))
// Post variants must not panic
l.PostPing(nil, PostPingMeta{})
l.PostTxBegin(0, nil, PostTxBeginMeta{})
l.PostTxCommit(0, nil, PostTxCommitMeta{})
l.PostTxRollback(0, nil, PostTxRollbackMeta{})
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
}
func TestNewPreTxBeginListener(t *testing.T) {
called := 0
l := NewPreTxBeginListener(func(ctx context.Context, txid uint16, meta PreTxBeginMeta) error {
called++
tst.AssertEqual(t, uint16(7), txid)
return nil
})
tst.AssertNoErr(t, l.PreTxBegin(context.Background(), 7, PreTxBeginMeta{}))
tst.AssertEqual(t, 1, called)
}
func TestNewPreTxCommitListener(t *testing.T) {
called := 0
l := NewPreTxCommitListener(func(txid uint16, meta PreTxCommitMeta) error {
called++
return nil
})
tst.AssertNoErr(t, l.PreTxCommit(0, PreTxCommitMeta{}))
tst.AssertEqual(t, 1, called)
}
func TestNewPreTxRollbackListener(t *testing.T) {
called := 0
l := NewPreTxRollbackListener(func(txid uint16, meta PreTxRollbackMeta) error {
called++
return nil
})
tst.AssertNoErr(t, l.PreTxRollback(0, PreTxRollbackMeta{}))
tst.AssertEqual(t, 1, called)
}
func TestNewPreQueryListener(t *testing.T) {
called := 0
l := NewPreQueryListener(func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreQueryMeta) error {
called++
*sql = "modified"
return nil
})
sql := "original"
pp := PP{}
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, &sql, &pp, PreQueryMeta{}))
tst.AssertEqual(t, 1, called)
tst.AssertEqual(t, "modified", sql)
}
func TestNewPreExecListener(t *testing.T) {
called := 0
l := NewPreExecListener(func(ctx context.Context, txID *uint16, sql *string, params *PP, meta PreExecMeta) error {
called++
return nil
})
sql := "x"
pp := PP{}
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, &sql, &pp, PreExecMeta{}))
tst.AssertEqual(t, 1, called)
}
func TestNewPreListenerBoth(t *testing.T) {
queryCalls := 0
execCalls := 0
l := NewPreListener(func(ctx context.Context, cmdtype string, txID *uint16, sql *string, params *PP) error {
switch cmdtype {
case "QUERY":
queryCalls++
case "EXEC":
execCalls++
}
return nil
})
sql := "s"
pp := PP{}
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, &sql, &pp, PreQueryMeta{}))
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, &sql, &pp, PreExecMeta{}))
tst.AssertEqual(t, 1, queryCalls)
tst.AssertEqual(t, 1, execCalls)
}
func TestNewPostPingListener(t *testing.T) {
called := 0
l := NewPostPingListener(func(result error, meta PostPingMeta) {
called++
})
l.PostPing(nil, PostPingMeta{})
tst.AssertEqual(t, 1, called)
}
func TestNewPostTxBeginListener(t *testing.T) {
called := 0
l := NewPostTxBeginListener(func(txid uint16, result error, meta PostTxBeginMeta) {
called++
})
l.PostTxBegin(0, nil, PostTxBeginMeta{})
tst.AssertEqual(t, 1, called)
}
func TestNewPostTxCommitListener(t *testing.T) {
called := 0
l := NewPostTxCommitListener(func(txid uint16, result error, meta PostTxCommitMeta) {
called++
})
l.PostTxCommit(0, nil, PostTxCommitMeta{})
tst.AssertEqual(t, 1, called)
}
func TestNewPostTxRollbackListener(t *testing.T) {
called := 0
l := NewPostTxRollbackListener(func(txid uint16, result error, meta PostTxRollbackMeta) {
called++
})
l.PostTxRollback(0, nil, PostTxRollbackMeta{})
tst.AssertEqual(t, 1, called)
}
func TestNewPostQueryListener(t *testing.T) {
called := 0
l := NewPostQueryListener(func(txID *uint16, sqlOriginal string, sqlReal string, params PP, result error, meta PostQueryMeta) {
called++
})
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
tst.AssertEqual(t, 1, called)
}
func TestNewPostExecListener(t *testing.T) {
called := 0
l := NewPostExecListener(func(txID *uint16, sqlOriginal string, sqlReal string, params PP, result error, meta PostExecMeta) {
called++
})
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
tst.AssertEqual(t, 1, called)
}
func TestNewPostListenerBoth(t *testing.T) {
queryCalls := 0
execCalls := 0
l := NewPostListener(func(cmdtype string, txID *uint16, sqlOriginal string, sqlReal string, result error, params PP) {
switch cmdtype {
case "QUERY":
queryCalls++
case "EXEC":
execCalls++
}
})
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
tst.AssertEqual(t, 1, queryCalls)
tst.AssertEqual(t, 1, execCalls)
}
func TestGenListenerNilHandlersDontPanic(t *testing.T) {
// A listener constructed with one constructor only sets one handler.
// Calls to other handlers should be safe no-ops.
l := NewPostPingListener(func(result error, meta PostPingMeta) {})
// All Pre* return nil
tst.AssertNoErr(t, l.PrePing(context.Background(), PrePingMeta{}))
tst.AssertNoErr(t, l.PreTxBegin(context.Background(), 0, PreTxBeginMeta{}))
tst.AssertNoErr(t, l.PreTxCommit(0, PreTxCommitMeta{}))
tst.AssertNoErr(t, l.PreTxRollback(0, PreTxRollbackMeta{}))
tst.AssertNoErr(t, l.PreQuery(context.Background(), nil, nil, nil, PreQueryMeta{}))
tst.AssertNoErr(t, l.PreExec(context.Background(), nil, nil, nil, PreExecMeta{}))
// All Post* are no-ops
l.PostTxBegin(0, nil, PostTxBeginMeta{})
l.PostTxCommit(0, nil, PostTxCommitMeta{})
l.PostTxRollback(0, nil, PostTxRollbackMeta{})
l.PostQuery(nil, "", "", PP{}, nil, PostQueryMeta{})
l.PostExec(nil, "", "", PP{}, nil, PostExecMeta{})
}
+88
View File
@@ -0,0 +1,88 @@
package sq
import (
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
"strings"
"testing"
)
func TestPPID(t *testing.T) {
id := PPID()
tst.AssertTrue(t, strings.HasPrefix(id, "p_"))
if len(id) != 2+8 {
t.Errorf("expected length 10, got %d (id=%q)", len(id), id)
}
// uniqueness - very high probability with 8 base62 chars
seen := map[string]bool{}
for range 1000 {
x := PPID()
if seen[x] {
t.Errorf("duplicate PPID: %s", x)
}
seen[x] = true
}
}
func TestPPAdd(t *testing.T) {
pp := PP{}
id1 := pp.Add(123)
id2 := pp.Add("hello")
tst.AssertNotEqual(t, id1, id2)
tst.AssertEqual(t, 123, pp[id1])
tst.AssertEqual(t, "hello", pp[id2])
tst.AssertEqual(t, 2, len(pp))
}
func TestPPAddAll(t *testing.T) {
a := PP{"a": 1, "b": 2}
b := PP{"c": 3, "d": 4}
a.AddAll(b)
tst.AssertEqual(t, 4, len(a))
tst.AssertEqual(t, 1, a["a"])
tst.AssertEqual(t, 2, a["b"])
tst.AssertEqual(t, 3, a["c"])
tst.AssertEqual(t, 4, a["d"])
}
func TestPPAddAllOverwrite(t *testing.T) {
a := PP{"a": 1, "b": 2}
b := PP{"a": 99}
a.AddAll(b)
tst.AssertEqual(t, 2, len(a))
tst.AssertEqual(t, 99, a["a"])
tst.AssertEqual(t, 2, a["b"])
}
func TestJoin(t *testing.T) {
a := PP{"a": 1, "b": 2}
b := PP{"c": 3}
c := PP{"d": 4, "a": 99}
r := Join(a, b, c)
tst.AssertEqual(t, 4, len(r))
tst.AssertEqual(t, 99, r["a"])
tst.AssertEqual(t, 2, r["b"])
tst.AssertEqual(t, 3, r["c"])
tst.AssertEqual(t, 4, r["d"])
// Source maps must remain unchanged
tst.AssertEqual(t, 2, len(a))
tst.AssertEqual(t, 1, a["a"])
}
func TestJoinEmpty(t *testing.T) {
r := Join()
tst.AssertEqual(t, 0, len(r))
}
func TestJoinSingle(t *testing.T) {
a := PP{"a": 1}
r := Join(a)
tst.AssertEqual(t, 1, len(r))
tst.AssertEqual(t, 1, r["a"])
}