[🤖] 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
+585
View File
@@ -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)
}