package jreader import ( "errors" "fmt" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" "github.com/stretchr/testify/require" ) // This uses the framework defined in the commontest package to exercise Reader with a large number // of valid and invalid JSON inputs. All we need to implement here is the logic for calling the // appropriate Reader methods that correspond to the commontest abstractions. type readerTestContext struct { input []byte r *Reader } type readerValueTestFactory struct{} type readerErrorTestFactory struct{} // The behavior of Reader is flexible so that callers can choose to read the same JSON value in // several different ways. Therefore, we generate variants for each value test as follows: // - A null JSON value could be read either as a null, or as a nullable value of another type. // - A JSON number could be read as an int (if the test value is an int), a float, or a nullable // int or float. // - Any other non-null value could be read as its own type, or as a nullable value of that type. // - Any value could be read with a nonspecific type using the Any() method. // - Any value could be skipped instead of read. const ( defaultVariant commontest.ValueVariant = "" nullableValue commontest.ValueVariant = "nullable" numberAsInt commontest.ValueVariant = "int" nullableNumberAsInt commontest.ValueVariant = "nullable int" nullableBoolIsNull commontest.ValueVariant = "nullable bool is" nullableIntIsNull commontest.ValueVariant = "nullable int is" nullableFloatIsNull commontest.ValueVariant = "nullable float is" nullableStringIsNull commontest.ValueVariant = "nullable string is" nullableArrayIsNull commontest.ValueVariant = "nullable array is" nullableObjectIsNull commontest.ValueVariant = "nullable object is" ) var variantsForNullValues = []commontest.ValueVariant{defaultVariant, nullableBoolIsNull, nullableIntIsNull, nullableFloatIsNull, nullableStringIsNull, nullableArrayIsNull, nullableObjectIsNull, commontest.UntypedVariant, commontest.SkipValueVariant} var variantsForInts = []commontest.ValueVariant{defaultVariant, numberAsInt, nullableValue, nullableNumberAsInt, commontest.UntypedVariant, commontest.SkipValueVariant} var variantsForFloats = []commontest.ValueVariant{defaultVariant, nullableValue, commontest.UntypedVariant, commontest.SkipValueVariant} var variantsForNonNullValues = []commontest.ValueVariant{defaultVariant, nullableValue, commontest.UntypedVariant, commontest.SkipValueVariant} var shouldHaveBeenNullError = errors.New("should have been null") var shouldNotHaveBeenNullError = errors.New("should not have been null") func TestReader(t *testing.T) { ts := commontest.ReaderTestSuite{ ContextFactory: func(input []byte) commontest.TestContext { r := NewReader(input) return &readerTestContext{input: input, r: &r} }, ValueTestFactory: readerValueTestFactory{}, ReadErrorTestFactory: readerErrorTestFactory{}, } ts.Run(t) } func (c readerTestContext) JSONData() []byte { return c.input } func (f readerValueTestFactory) EOF() commontest.Action { return func(c commontest.TestContext) error { return c.(*readerTestContext).r.RequireEOF() } } func (f readerValueTestFactory) Variants(value commontest.AnyValue) []commontest.ValueVariant { switch value.Kind { case commontest.NullValue: return variantsForNullValues case commontest.NumberValue: if float64(int(value.Number)) == value.Number { return variantsForInts } return variantsForFloats default: return variantsForNonNullValues } } func (f readerValueTestFactory) Value(value commontest.AnyValue, variant commontest.ValueVariant) commontest.Action { return func(c commontest.TestContext) error { ctx := c.(*readerTestContext) r := ctx.r if variant == commontest.SkipValueVariant { return r.SkipValue() } if variant == commontest.UntypedVariant { return assertReadAnyValue(ctx, r, value) } switch value.Kind { case commontest.NullValue: return assertReadNull(r, variant) case commontest.BoolValue: switch variant { case nullableValue: gotVal, nonNull := r.BoolOrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(value.Bool, gotVal)) default: gotVal := r.Bool() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(value.Bool, gotVal)) } case commontest.NumberValue: switch variant { case nullableNumberAsInt: gotVal, nonNull := r.IntOrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(int(value.Number), gotVal)) case numberAsInt: gotVal := r.Int() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(int(value.Number), gotVal)) case nullableValue: gotVal, nonNull := r.Float64OrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(value.Number, gotVal)) default: gotVal := r.Float64() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(value.Number, gotVal)) } case commontest.StringValue: switch variant { case nullableValue: gotVal, nonNull := r.StringOrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(value.String, gotVal)) default: gotVal := r.String() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(value.String, gotVal)) } case commontest.ArrayValue: var arr ArrayState if variant == nullableValue { arr = r.ArrayOrNull() } else { arr = r.Array() } if r.Error() != nil { return r.Error() } if err := commontest.AssertTrue(arr.IsDefined(), shouldNotHaveBeenNullError.Error()); err != nil { return err } return assertReadArray(ctx, &arr, value) case commontest.ObjectValue: var obj ObjectState if variant == nullableValue { obj = r.ObjectOrNull() } else { obj = r.Object() } if r.Error() != nil { return r.Error() } if err := commontest.AssertTrue(obj.IsDefined(), shouldNotHaveBeenNullError.Error()); err != nil { return err } return assertReadObject(ctx, &obj, value) } return nil } } func assertReadNull(r *Reader, variant commontest.ValueVariant) error { var gotVal, expectVal interface{} var nonNull bool switch variant { case defaultVariant: return r.Null() case nullableBoolIsNull: gotVal, nonNull = r.BoolOrNull() expectVal = false case nullableIntIsNull: gotVal, nonNull = r.IntOrNull() expectVal = 0 case nullableFloatIsNull: gotVal, nonNull = r.Float64OrNull() expectVal = float64(0) case nullableStringIsNull: gotVal, nonNull = r.StringOrNull() expectVal = "" case nullableArrayIsNull: arr := r.ArrayOrNull() if r.Error() != nil { return r.Error() } if arr.IsDefined() { return TypeError{Expected: NullValue, Actual: ArrayValue} } return nil case nullableObjectIsNull: obj := r.ObjectOrNull() if r.Error() != nil { return r.Error() } if obj.IsDefined() { return TypeError{Expected: NullValue, Actual: ObjectValue} } return nil } return commontest.AssertNoErrors( r.Error(), commontest.AssertTrue(!nonNull, shouldHaveBeenNullError.Error()), commontest.AssertEqual(expectVal, gotVal)) } func assertReadArray(ctx *readerTestContext, arr *ArrayState, value commontest.AnyValue) error { if err := commontest.AssertTrue(arr.IsDefined(), shouldNotHaveBeenNullError.Error()); err != nil { return err } for _, e := range value.Array { if err := commontest.AssertTrue(arr.Next(), "array ended too soon"); err != nil { return err } if err := e(ctx); err != nil { return err } } return commontest.AssertTrue(!arr.Next(), "expected end of array") } func assertReadObject(ctx *readerTestContext, obj *ObjectState, value commontest.AnyValue) error { if err := commontest.AssertTrue(obj.IsDefined(), "should not have been null"); err != nil { return err } for _, p := range value.Object { if err := commontest.AssertNoErrors( commontest.AssertTrue(obj.Next(), "object ended too soon"), commontest.AssertEqual(p.Name, string(obj.Name())), ); err != nil { return err } if err := p.Action(ctx); err != nil { return err } } return commontest.AssertTrue(!obj.Next(), "expected end of object") } func assertReadAnyValue(ctx *readerTestContext, r *Reader, value commontest.AnyValue) error { av := r.Any() if r.Error() != nil { return r.Error() } switch value.Kind { case commontest.NullValue: return commontest.AssertEqual(NullValue, av.Kind) case commontest.BoolValue: return commontest.AssertNoErrors(commontest.AssertEqual(BoolValue, av.Kind), commontest.AssertEqual(value.Bool, av.Bool)) case commontest.NumberValue: return commontest.AssertNoErrors(commontest.AssertEqual(NumberValue, av.Kind), commontest.AssertEqual(value.Number, av.Number)) case commontest.StringValue: return commontest.AssertNoErrors(commontest.AssertEqual(StringValue, av.Kind), commontest.AssertEqual(value.String, av.String)) case commontest.ArrayValue: if err := commontest.AssertEqual(ArrayValue, av.Kind); err != nil { return err } return assertReadArray(ctx, &av.Array, value) case commontest.ObjectValue: if err := commontest.AssertEqual(ObjectValue, av.Kind); err != nil { return err } return assertReadObject(ctx, &av.Object, value) } return nil } func (f readerErrorTestFactory) ExpectEOFError(err error) error { return tokenReaderErrorTestFactory{}.ExpectEOFError(err) } func (f readerErrorTestFactory) ExpectWrongTypeError(err error, expected commontest.ValueKind, variant commontest.ValueVariant, actual commontest.ValueKind) error { // Here our behavior is different from tokenReaderErrorTestFactory, because Reader has more possible // kinds of errors due to the convenience features for reading values as "some type *or* null". expectedError := TypeError{ Expected: valueKindFromTestValueKind(expected), Actual: valueKindFromTestValueKind(actual), } switch variant { case nullableValue, nullableNumberAsInt: expectedError.Nullable = true case nullableBoolIsNull: expectedError.Nullable = true expectedError.Expected = BoolValue case nullableIntIsNull, nullableFloatIsNull: expectedError.Nullable = true expectedError.Expected = NumberValue case nullableStringIsNull: expectedError.Nullable = true expectedError.Expected = StringValue case nullableArrayIsNull: expectedError.Nullable = true expectedError.Expected = ArrayValue case nullableObjectIsNull: expectedError.Nullable = true expectedError.Expected = ObjectValue } if expectedError.Nullable && valueKindFromTestValueKind(actual) == expectedError.Expected { return commontest.AssertEqual(shouldHaveBeenNullError, err) } if expectedError.Nullable && actual == commontest.NullValue { return commontest.AssertEqual(shouldNotHaveBeenNullError, err) } if te, ok := err.(TypeError); ok { expectedError.Offset = te.Offset if te == expectedError { return nil } } return fmt.Errorf("expected %T %+v, got %T %+v", expectedError, expectedError, err, err) } func (f readerErrorTestFactory) ExpectSyntaxError(err error) error { return tokenReaderErrorTestFactory{}.ExpectSyntaxError(err) } func TestReaderAddErrorDoesNotOverridePreviousError(t *testing.T) { fakeError := errors.New("sorry") r := NewReader([]byte(`"not a bool"`)) _ = r.Bool() r.AddError(fakeError) require.Error(t, r.Error()) require.NotEqual(t, fakeError, r.Error()) require.IsType(t, TypeError{}, r.Error()) } func TestReaderSetErrorOverridesPreviousError(t *testing.T) { fakeError := errors.New("sorry") r := NewReader([]byte(`"not a bool"`)) _ = r.Bool() r.ReplaceError(fakeError) require.Error(t, r.Error()) require.Equal(t, fakeError, r.Error()) } func TestReaderSkipValue(t *testing.T) { t.Run("Next() skips array element if it was not read", func(t *testing.T) { data := []byte(`["a", ["b1", "b2"], "c"]`) r := NewReader(data) arr := r.Array() require.NoError(t, r.Error()) require.True(t, arr.Next()) val1 := r.String() require.NoError(t, r.Error()) require.Equal(t, "a", val1) require.True(t, arr.Next()) require.True(t, arr.Next()) val3 := r.String() require.NoError(t, r.Error()) require.Equal(t, "c", val3) require.False(t, arr.Next()) }) t.Run("Next() skips property value if it was not read", func(t *testing.T) { data := []byte(`{"a":1, "b":{"b1":2, "b2":3}, "c":4}`) r := NewReader(data) obj := r.Object() require.NoError(t, r.Error()) require.True(t, obj.Next()) require.Equal(t, "a", string(obj.Name())) val1 := r.Int() require.NoError(t, r.Error()) require.Equal(t, 1, val1) require.True(t, obj.Next()) require.True(t, obj.Next()) require.Equal(t, "c", string(obj.Name())) val3 := r.Int() require.NoError(t, r.Error()) require.Equal(t, 4, val3) require.False(t, obj.Next()) }) }