This commit is contained in:
+585
@@ -0,0 +1,585 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user