diff --git a/internal/modifiers/attr_modifiers.go b/internal/modifiers/attr_wrapper.go similarity index 100% rename from internal/modifiers/attr_modifiers.go rename to internal/modifiers/attr_wrapper.go diff --git a/internal/modifiers/attr_wrapper_test.go b/internal/modifiers/attr_wrapper_test.go new file mode 100644 index 0000000..bb9dfd1 --- /dev/null +++ b/internal/modifiers/attr_wrapper_test.go @@ -0,0 +1,63 @@ +package modifiers + +import ( + "context" + "errors" + "testing" + + tt "github.com/vingarcia/ksql/internal/testtools" +) + +func TestAttrWrapper(t *testing.T) { + ctx := context.Background() + + var scanArgs map[string]interface{} + var valueArgs map[string]interface{} + wrapper := AttrWrapper{ + Ctx: ctx, + Attr: "fakeAttr", + Modifier: AttrModifierMock{ + AttrScanFn: func(ctx context.Context, opInfo OpInfo, attrPtr interface{}, dbValue interface{}) error { + scanArgs = map[string]interface{}{ + "opInfo": opInfo, + "attrPtr": attrPtr, + "dbValue": dbValue, + } + return errors.New("fakeScanErrMsg") + }, + AttrValueFn: func(ctx context.Context, opInfo OpInfo, inputValue interface{}) (outputValue interface{}, _ error) { + valueArgs = map[string]interface{}{ + "opInfo": opInfo, + "inputValue": inputValue, + } + return "fakeOutputValue", errors.New("fakeValueErrMsg") + }, + }, + OpInfo: OpInfo{ + Method: "fakeMethod", + DriverName: "fakeDriverName", + }, + } + + err := wrapper.Scan("fakeDbValue") + tt.AssertErrContains(t, err, "fakeScanErrMsg") + tt.AssertEqual(t, scanArgs, map[string]interface{}{ + "opInfo": OpInfo{ + Method: "fakeMethod", + DriverName: "fakeDriverName", + }, + "attrPtr": "fakeAttr", + "dbValue": "fakeDbValue", + }) + + value, err := wrapper.Value() + tt.AssertErrContains(t, err, "fakeValueErrMsg") + tt.AssertEqual(t, valueArgs, map[string]interface{}{ + "opInfo": OpInfo{ + Method: "fakeMethod", + DriverName: "fakeDriverName", + }, + "inputValue": "fakeAttr", + }) + tt.AssertEqual(t, value, "fakeOutputValue") +} diff --git a/internal/modifiers/global_modifiers_test.go b/internal/modifiers/global_modifiers_test.go new file mode 100644 index 0000000..4607cbc --- /dev/null +++ b/internal/modifiers/global_modifiers_test.go @@ -0,0 +1,45 @@ +package modifiers + +import ( + "testing" + + tt "github.com/vingarcia/ksql/internal/testtools" +) + +func TestRegisterAttrModifier(t *testing.T) { + t.Run("should register new modifiers correctly", func(t *testing.T) { + modifier1 := AttrModifierMock{} + modifier2 := AttrModifierMock{} + + RegisterAttrModifier("fakeModifierName1", &modifier1) + RegisterAttrModifier("fakeModifierName2", &modifier2) + + mod, err := LoadGlobalModifier("fakeModifierName1") + tt.AssertNoErr(t, err) + tt.AssertEqual(t, mod, &modifier1) + + mod, err = LoadGlobalModifier("fakeModifierName2") + tt.AssertNoErr(t, err) + tt.AssertEqual(t, mod, &modifier2) + }) + + t.Run("should panic registering a modifier and the name already exists", func(t *testing.T) { + modifier1 := AttrModifierMock{} + modifier2 := AttrModifierMock{} + + RegisterAttrModifier("fakeModifierName", &modifier1) + panicPayload := tt.PanicHandler(func() { + RegisterAttrModifier("fakeModifierName", &modifier2) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "KSQL", "fakeModifierName", "name is already in use") + }) + + t.Run("should return an error when loading an inexistent modifier", func(t *testing.T) { + mod, err := LoadGlobalModifier("nonExistentModifier") + tt.AssertErrContains(t, err, "nonExistentModifier") + tt.AssertEqual(t, mod, nil) + }) +} diff --git a/internal/modifiers/json_modifier_test.go b/internal/modifiers/json_modifier_test.go new file mode 100644 index 0000000..968e468 --- /dev/null +++ b/internal/modifiers/json_modifier_test.go @@ -0,0 +1,122 @@ +package modifiers + +import ( + "context" + "testing" + + tt "github.com/vingarcia/ksql/internal/testtools" +) + +func TestAttrScan(t *testing.T) { + ctx := context.Background() + + type FakeAttr struct { + Foo string `json:"foo"` + } + + tests := []struct { + desc string + dbInput interface{} + expectedValue interface{} + expectErrToContain []string + }{ + { + desc: "should set struct to zero value if input is nil", + dbInput: nil, + expectedValue: FakeAttr{}, + }, + { + desc: "should work when input is a byte slice", + dbInput: []byte(`{"foo":"bar"}`), + expectedValue: FakeAttr{ + Foo: "bar", + }, + }, + { + desc: "should work when input is a string", + dbInput: `{"foo":"bar"}`, + expectedValue: FakeAttr{ + Foo: "bar", + }, + }, + { + desc: "should report error if input type is unsupported", + dbInput: 10, + expectErrToContain: []string{"unexpected type", "int"}, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + fakeAttr := FakeAttr{ + Foo: "notZeroValue", + } + err := jsonModifier{}.AttrScan(ctx, OpInfo{}, &fakeAttr, test.dbInput) + if test.expectErrToContain != nil { + tt.AssertErrContains(t, err, test.expectErrToContain...) + t.Skip() + } + + tt.AssertNoErr(t, err) + tt.AssertEqual(t, fakeAttr, test.expectedValue) + }) + } +} + +func TestAttrValue(t *testing.T) { + ctx := context.Background() + + type FakeAttr struct { + Foo string `json:"foo"` + } + + tests := []struct { + desc string + dbInput interface{} + opInfoInput OpInfo + attrValue interface{} + + expectedOutput interface{} + expectErrToContain []string + }{ + { + desc: "should return a byte array when the driver is not sqlserver", + dbInput: []byte(`{"foo":"bar"}`), + opInfoInput: OpInfo{ + DriverName: "notSQLServer", + }, + attrValue: FakeAttr{ + Foo: "bar", + }, + expectedOutput: tt.ToJSON(t, map[string]interface{}{ + "foo": "bar", + }), + }, + { + desc: "should return a string when the driver is sqlserver", + dbInput: []byte(`{"foo":"bar"}`), + opInfoInput: OpInfo{ + DriverName: "sqlserver", + }, + attrValue: FakeAttr{ + Foo: "bar", + }, + expectedOutput: string(tt.ToJSON(t, map[string]interface{}{ + "foo": "bar", + })), + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + output, err := jsonModifier{}.AttrValue(ctx, test.opInfoInput, test.attrValue) + if test.expectErrToContain != nil { + tt.AssertErrContains(t, err, test.expectErrToContain...) + t.Skip() + } + + tt.AssertNoErr(t, err) + tt.AssertEqual(t, output, test.expectedOutput) + }) + } +} diff --git a/internal/modifiers/mocks.go b/internal/modifiers/mocks.go new file mode 100644 index 0000000..d023dc9 --- /dev/null +++ b/internal/modifiers/mocks.go @@ -0,0 +1,38 @@ +package modifiers + +import "context" + +// AttrModifierMock mocks the modifiers.AttrModifier interface +type AttrModifierMock struct { + AttrScanFn func( + ctx context.Context, + opInfo OpInfo, + attrPtr interface{}, + dbValue interface{}, + ) error + + AttrValueFn func( + ctx context.Context, + opInfo OpInfo, + inputValue interface{}, + ) (outputValue interface{}, _ error) +} + +// AttrScan mocks the AttrScan method +func (a AttrModifierMock) AttrScan( + ctx context.Context, + opInfo OpInfo, + attrPtr interface{}, + dbValue interface{}, +) error { + return a.AttrScanFn(ctx, opInfo, attrPtr, dbValue) +} + +// AttrValue mocks the AttrValue method +func (a AttrModifierMock) AttrValue( + ctx context.Context, + opInfo OpInfo, + inputValue interface{}, +) (outputValue interface{}, _ error) { + return a.AttrValueFn(ctx, opInfo, inputValue) +} diff --git a/internal/testtools/json.go b/internal/testtools/json.go new file mode 100644 index 0000000..b8712bf --- /dev/null +++ b/internal/testtools/json.go @@ -0,0 +1,13 @@ +package tt + +import ( + "encoding/json" + "testing" +) + +func ToJSON(t *testing.T, obj interface{}) []byte { + rawJSON, err := json.Marshal(obj) + AssertNoErr(t, err) + + return rawJSON +}