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