586 lines
14 KiB
Go
586 lines
14 KiB
Go
package wmo
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
|
|
ct "git.blackforestbytes.com/BlackForestBytes/goext/cursortoken"
|
|
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
|
|
"git.blackforestbytes.com/BlackForestBytes/goext/tst"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
|
)
|
|
|
|
// --- Mock helpers for Decodable / Cursorable -------------------------------
|
|
|
|
type mockDecodable struct {
|
|
docs []bson.M
|
|
cursor int
|
|
err error
|
|
}
|
|
|
|
func (m *mockDecodable) Decode(v any) error {
|
|
if m.err != nil {
|
|
return m.err
|
|
}
|
|
if m.cursor >= len(m.docs) {
|
|
return mongo.ErrNoDocuments
|
|
}
|
|
raw, err := bson.Marshal(m.docs[m.cursor])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return bson.Unmarshal(raw, v)
|
|
}
|
|
|
|
type mockCursor struct {
|
|
docs []bson.M
|
|
idx int
|
|
closed bool
|
|
allErr error
|
|
startBefore int
|
|
}
|
|
|
|
func newMockCursor(docs []bson.M) *mockCursor {
|
|
return &mockCursor{docs: docs, idx: -1}
|
|
}
|
|
|
|
func (m *mockCursor) Decode(v any) error {
|
|
if m.idx < 0 || m.idx >= len(m.docs) {
|
|
return mongo.ErrNoDocuments
|
|
}
|
|
raw, err := bson.Marshal(m.docs[m.idx])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return bson.Unmarshal(raw, v)
|
|
}
|
|
|
|
func (m *mockCursor) Err() error { return nil }
|
|
func (m *mockCursor) Close(_ context.Context) error { m.closed = true; return nil }
|
|
func (m *mockCursor) RemainingBatchLength() int { return len(m.docs) - (m.idx + 1) }
|
|
func (m *mockCursor) Next(_ context.Context) bool { m.idx++; return m.idx < len(m.docs) }
|
|
func (m *mockCursor) All(_ context.Context, results any) error {
|
|
if m.allErr != nil {
|
|
return m.allErr
|
|
}
|
|
raws := make([]bson.Raw, 0, len(m.docs))
|
|
for _, d := range m.docs {
|
|
r, err := bson.Marshal(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raws = append(raws, r)
|
|
}
|
|
rv := reflect.ValueOf(results).Elem()
|
|
rv.SetLen(0)
|
|
elemType := rv.Type().Elem()
|
|
for _, r := range raws {
|
|
ev := reflect.New(elemType)
|
|
if err := bson.Unmarshal(r, ev.Interface()); err != nil {
|
|
return err
|
|
}
|
|
rv.Set(reflect.Append(rv, ev.Elem()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- init() / reflection edge cases ---------------------------------------
|
|
|
|
func TestInitIgnoresUnsupportedFields(t *testing.T) {
|
|
|
|
type Inlined struct {
|
|
Inner string `bson:"inner"`
|
|
}
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Skipped string `bson:"-"`
|
|
NoTag string // no bson tag => skipped
|
|
unexported string `bson:"unexp"`
|
|
WithOpts string `bson:"opt,omitempty"`
|
|
Inline Inlined `bson:",inline"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
_, errSkipped := coll.getFieldType("-")
|
|
tst.AssertTrue(t, errSkipped != nil)
|
|
|
|
_, errNoTag := coll.getFieldType("NoTag")
|
|
tst.AssertTrue(t, errNoTag != nil)
|
|
|
|
_, errUnexp := coll.getFieldType("unexp")
|
|
tst.AssertTrue(t, errUnexp != nil)
|
|
|
|
id, err := coll.getFieldType("_id")
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, id.Name, "ID")
|
|
|
|
opt, err := coll.getFieldType("opt")
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, opt.Name, "WithOpts")
|
|
|
|
inner, err := coll.getFieldType("inner")
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, inner.Name, "Inner")
|
|
|
|
_ = TestData{unexported: "x"}
|
|
}
|
|
|
|
func TestInitRecursivePointerStructDoesNotInfinitelyRecurse(t *testing.T) {
|
|
|
|
type Recursive struct {
|
|
Val int `bson:"val"`
|
|
Inner *Recursive `bson:"inner"`
|
|
}
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Rec Recursive `bson:"rec"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
val, err := coll.getFieldType("rec.val")
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, val.Name, "Val")
|
|
|
|
inner, err := coll.getFieldType("rec.inner")
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, inner.IsPointer, true)
|
|
|
|
_, err = coll.getFieldType("rec.inner.inner.inner.inner.val")
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestGetFieldTypeUnknownField(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
_, err := coll.getFieldType("does_not_exist")
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestGetFieldValueUnknownField(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
_, err := coll.getFieldValue(TestData{ID: "x"}, "does_not_exist")
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestGetFieldValueInterfaceUnknownType(t *testing.T) {
|
|
|
|
type TestImpl struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
type Iface any
|
|
|
|
coll := W[Iface](&mongo.Collection{})
|
|
// no decoder registered, the impl-type-map is empty
|
|
_, err := coll.getFieldValue(TestImpl{ID: "x"}, "_id")
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestGetFieldValueInterfaceUnknownField(t *testing.T) {
|
|
|
|
type TestImpl struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
type Iface any
|
|
|
|
df := func(ctx context.Context, dec Decodable) (Iface, error) { return TestImpl{}, nil }
|
|
coll := W[Iface](&mongo.Collection{}).WithDecodeFunc(df, TestImpl{})
|
|
|
|
_, err := coll.getFieldValue(TestImpl{ID: "x"}, "missing_field")
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestEnsureInitializedReflectionIdempotent(t *testing.T) {
|
|
|
|
type TestImpl struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
type Iface any
|
|
|
|
df := func(ctx context.Context, dec Decodable) (Iface, error) { return TestImpl{}, nil }
|
|
coll := W[Iface](&mongo.Collection{}).WithDecodeFunc(df, TestImpl{})
|
|
|
|
tst.AssertEqual(t, 1, len(coll.implDataTypeMap))
|
|
|
|
// pointer-deref path + idempotency: calling again should not duplicate.
|
|
coll.EnsureInitializedReflection(&TestImpl{ID: "x"})
|
|
tst.AssertEqual(t, 1, len(coll.implDataTypeMap))
|
|
|
|
coll.EnsureInitializedReflection(TestImpl{ID: "y"})
|
|
tst.AssertEqual(t, 1, len(coll.implDataTypeMap))
|
|
}
|
|
|
|
func TestEnsureInitializedReflectionNoOpForStruct(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
before := len(coll.implDataTypeMap)
|
|
coll.EnsureInitializedReflection(TestData{ID: "x"})
|
|
tst.AssertEqual(t, before, len(coll.implDataTypeMap))
|
|
}
|
|
|
|
// --- hooks ----------------------------------------------------------------
|
|
|
|
func TestWithUnmarshalHookRunsOnDecodeSingle(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Value string `bson:"value"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{}).WithUnmarshalHook(func(d TestData) TestData {
|
|
d.Value = d.Value + "_hook"
|
|
return d
|
|
})
|
|
|
|
dec := &mockDecodable{docs: []bson.M{{"_id": "1", "value": "raw"}}}
|
|
res, err := coll.decodeSingle(context.Background(), dec)
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, res.Value, "raw_hook")
|
|
}
|
|
|
|
func TestWithUnmarshalHookRunsOnDecodeAll(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Value string `bson:"value"`
|
|
}
|
|
|
|
calls := 0
|
|
coll := W[TestData](&mongo.Collection{}).WithUnmarshalHook(func(d TestData) TestData {
|
|
calls++
|
|
d.Value = d.Value + "!"
|
|
return d
|
|
})
|
|
|
|
cur := newMockCursor([]bson.M{
|
|
{"_id": "1", "value": "a"},
|
|
{"_id": "2", "value": "b"},
|
|
})
|
|
res, err := coll.decodeAll(context.Background(), cur)
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, len(res), 2)
|
|
tst.AssertEqual(t, res[0].Value, "a!")
|
|
tst.AssertEqual(t, res[1].Value, "b!")
|
|
tst.AssertEqual(t, calls, 2)
|
|
}
|
|
|
|
func TestWithMarshalHookAppendsHook(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{}).
|
|
WithMarshalHook(func(d TestData) TestData { d.ID = "1-" + d.ID; return d }).
|
|
WithMarshalHook(func(d TestData) TestData { d.ID = "2-" + d.ID; return d })
|
|
|
|
tst.AssertEqual(t, len(coll.marshalHooks), 2)
|
|
|
|
out := coll.marshalHooks[0](TestData{ID: "x"})
|
|
tst.AssertEqual(t, out.ID, "1-x")
|
|
|
|
out = coll.marshalHooks[1](TestData{ID: "x"})
|
|
tst.AssertEqual(t, out.ID, "2-x")
|
|
}
|
|
|
|
func TestCustomDecoderUsedInDecodeSingle(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Value string `bson:"value"`
|
|
}
|
|
|
|
type Iface any
|
|
|
|
df := func(ctx context.Context, dec Decodable) (Iface, error) {
|
|
var raw bson.M
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
return TestData{ID: raw["_id"].(string), Value: "from-custom"}, nil
|
|
}
|
|
|
|
coll := W[Iface](&mongo.Collection{}).WithDecodeFunc(df, TestData{})
|
|
|
|
dec := &mockDecodable{docs: []bson.M{{"_id": "abc", "value": "raw"}}}
|
|
res, err := coll.decodeSingle(context.Background(), dec)
|
|
tst.AssertNoErr(t, err)
|
|
|
|
td, ok := res.(TestData)
|
|
tst.AssertTrue(t, ok)
|
|
tst.AssertEqual(t, td.ID, "abc")
|
|
tst.AssertEqual(t, td.Value, "from-custom")
|
|
}
|
|
|
|
func TestCustomDecoderUsedInDecodeAll(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
type Iface any
|
|
|
|
df := func(ctx context.Context, dec Decodable) (Iface, error) {
|
|
var raw bson.M
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return nil, err
|
|
}
|
|
return TestData{ID: raw["_id"].(string) + "!"}, nil
|
|
}
|
|
|
|
coll := W[Iface](&mongo.Collection{}).WithDecodeFunc(df, TestData{})
|
|
|
|
cur := newMockCursor([]bson.M{
|
|
{"_id": "a"},
|
|
{"_id": "b"},
|
|
{"_id": "c"},
|
|
})
|
|
res, err := coll.decodeAll(context.Background(), cur)
|
|
tst.AssertNoErr(t, err)
|
|
tst.AssertEqual(t, len(res), 3)
|
|
tst.AssertEqual(t, res[0].(TestData).ID, "a!")
|
|
tst.AssertEqual(t, res[2].(TestData).ID, "c!")
|
|
}
|
|
|
|
func TestCustomDecoderErrorPropagates(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
type Iface any
|
|
|
|
myErr := errors.New("custom-decoder-failure")
|
|
df := func(ctx context.Context, dec Decodable) (Iface, error) {
|
|
return nil, myErr
|
|
}
|
|
|
|
coll := W[Iface](&mongo.Collection{}).WithDecodeFunc(df, TestData{})
|
|
|
|
_, err := coll.decodeSingle(context.Background(), &mockDecodable{docs: []bson.M{{"_id": "a"}}})
|
|
tst.AssertTrue(t, err != nil)
|
|
|
|
cur := newMockCursor([]bson.M{{"_id": "a"}})
|
|
_, err = coll.decodeAll(context.Background(), cur)
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestDecodeSingleWithDecodeError(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
_, err := coll.decodeSingle(context.Background(), &mockDecodable{err: errors.New("boom")})
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
// --- pipeline / sort detection --------------------------------------------
|
|
|
|
func TestWithModifyingPipelineAppends(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{}).WithModifyingPipeline(mongo.Pipeline{
|
|
bson.D{{Key: "$set", Value: bson.M{"x": 1}}},
|
|
})
|
|
|
|
tst.AssertEqual(t, len(coll.extraModPipeline), 1)
|
|
|
|
stages := coll.extraModPipeline[0](context.Background())
|
|
tst.AssertEqual(t, len(stages), 1)
|
|
tst.AssertEqual(t, stages[0][0].Key, "$set")
|
|
}
|
|
|
|
func TestWithModifyingPipelineFunc(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
called := 0
|
|
coll := W[TestData](&mongo.Collection{}).WithModifyingPipelineFunc(func(ctx context.Context) mongo.Pipeline {
|
|
called++
|
|
return mongo.Pipeline{bson.D{{Key: "$project", Value: bson.M{"_id": 1}}}}
|
|
})
|
|
|
|
tst.AssertEqual(t, len(coll.extraModPipeline), 1)
|
|
stages := coll.extraModPipeline[0](context.Background())
|
|
tst.AssertEqual(t, called, 1)
|
|
tst.AssertEqual(t, stages[0][0].Key, "$project")
|
|
}
|
|
|
|
func TestNeedsDoubleSortFalseWithoutGroup(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{}).
|
|
WithModifyingPipeline(mongo.Pipeline{
|
|
bson.D{{Key: "$set", Value: bson.M{"x": 1}}},
|
|
bson.D{{Key: "$unset", Value: "y"}},
|
|
})
|
|
|
|
tst.AssertEqual(t, coll.needsDoubleSort(context.Background()), false)
|
|
}
|
|
|
|
func TestNeedsDoubleSortTrueWithGroup(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{}).
|
|
WithModifyingPipeline(mongo.Pipeline{
|
|
bson.D{{Key: "$set", Value: bson.M{"x": 1}}},
|
|
bson.D{{Key: "$group", Value: bson.M{"_id": "$cat"}}},
|
|
})
|
|
|
|
tst.AssertEqual(t, coll.needsDoubleSort(context.Background()), true)
|
|
}
|
|
|
|
func TestNeedsDoubleSortNoExtraPipeline(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
tst.AssertEqual(t, coll.needsDoubleSort(context.Background()), false)
|
|
}
|
|
|
|
// --- token creation -------------------------------------------------------
|
|
|
|
func TestCreateTokenPrimaryOnly(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Value int `bson:"value"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
pageSize := 50
|
|
tok, err := coll.createToken("_id", ct.SortASC, nil, nil, TestData{ID: "abc", Value: 1}, &pageSize)
|
|
tst.AssertNoErr(t, err)
|
|
|
|
cts, ok := tok.(ct.CTKeySort)
|
|
tst.AssertTrue(t, ok)
|
|
tst.AssertEqual(t, cts.ValuePrimary, "abc")
|
|
tst.AssertEqual(t, cts.ValueSecondary, "")
|
|
tst.AssertEqual(t, cts.Direction, ct.SortASC)
|
|
tst.AssertEqual(t, cts.PageSize, 50)
|
|
}
|
|
|
|
func TestCreateTokenWithSecondary(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
Value int `bson:"value"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
secField := "value"
|
|
secDir := ct.SortDESC
|
|
tok, err := coll.createToken("_id", ct.SortASC, &secField, &secDir, TestData{ID: "abc", Value: 7}, nil)
|
|
tst.AssertNoErr(t, err)
|
|
|
|
cts, ok := tok.(ct.CTKeySort)
|
|
tst.AssertTrue(t, ok)
|
|
tst.AssertEqual(t, cts.ValuePrimary, "abc")
|
|
tst.AssertEqual(t, cts.ValueSecondary, "7")
|
|
tst.AssertEqual(t, cts.PageSize, 0)
|
|
}
|
|
|
|
func TestCreateTokenUnknownPrimaryField(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
_, err := coll.createToken("nonexistent", ct.SortASC, nil, nil, TestData{ID: "x"}, nil)
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
func TestCreateTokenUnknownSecondaryField(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
sec := "nope"
|
|
dir := ct.SortASC
|
|
_, err := coll.createToken("_id", ct.SortASC, &sec, &dir, TestData{ID: "x"}, nil)
|
|
tst.AssertTrue(t, err != nil)
|
|
}
|
|
|
|
// --- W constructor & Coll basics ------------------------------------------
|
|
|
|
func TestWInitializesMaps(t *testing.T) {
|
|
|
|
type TestData struct {
|
|
ID string `bson:"_id"`
|
|
}
|
|
|
|
coll := W[TestData](&mongo.Collection{})
|
|
|
|
tst.AssertTrue(t, coll != nil)
|
|
tst.AssertTrue(t, coll.dataTypeMap != nil)
|
|
tst.AssertTrue(t, coll.implDataTypeMap != nil)
|
|
tst.AssertEqual(t, coll.isInterfaceDataType, false)
|
|
tst.AssertTrue(t, len(coll.dataTypeMap) > 0)
|
|
}
|
|
|
|
func TestWInitForInterfaceLeavesDataTypeMapEmpty(t *testing.T) {
|
|
|
|
type Iface any
|
|
|
|
coll := W[Iface](&mongo.Collection{})
|
|
|
|
tst.AssertEqual(t, coll.isInterfaceDataType, true)
|
|
tst.AssertEqual(t, len(coll.dataTypeMap), 0)
|
|
tst.AssertEqual(t, len(coll.implDataTypeMap), 0)
|
|
}
|
|
|
|
// quick sanity: the langext.Ptr helper used across tests behaves as expected.
|
|
func TestLangextPtrSanity(t *testing.T) {
|
|
p := langext.Ptr(42)
|
|
tst.AssertTrue(t, p != nil)
|
|
tst.AssertEqual(t, *p, 42)
|
|
}
|