diff --git a/array.go b/array.go index 4b9a33a98..9d3d9463f 100644 --- a/array.go +++ b/array.go @@ -746,6 +746,115 @@ func (a *Array) Equal(value interface{}) *Array { return a.IsEqual(value) } +// InList succeeds if whole array is equal to one of the elements from given +// list of arrays. +// Before comparison, both array and each value are converted to canonical +// form. +// +// Each value should be a slice of any type. +// +// Example: +// +// array := NewArray(t, []interface{}{"foo", 123}) +// array.InList([]interface{}{"foo", 123}, []interface{}{"bar", "456"}) +func (a *Array) InList(values ...interface{}) *Array { + opChain := a.chain.enter("InList()") + defer opChain.leave() + + if opChain.failed() { + return a + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return a + } + + var isListed bool + for _, v := range values { + expected, ok := canonArray(opChain, v) + if !ok { + return a + } + + if reflect.DeepEqual(expected, a.value) { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{a.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: arrays is equal to one of the values"), + }, + }) + } + + return a +} + +// NotInList succeeds if whole array is not equal to any of the elements +// from given list of arrays. +// Before comparison, both array and each value are converted to canonical +// form. +// +// Each value should be a slice of any type. +// +// Example: +// +// array := NewArray(t, []interface{}{"foo", 123}) +// array.NotInList([]interface{}{"bar", 456}, []interface{}{"baz", "foo"}) +func (a *Array) NotInList(values ...interface{}) *Array { + opChain := a.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return a + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return a + } + + for _, v := range values { + expected, ok := canonArray(opChain, v) + if !ok { + return a + } + + if reflect.DeepEqual(expected, a.value) { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{a.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: arrays is not equal to any of the values"), + }, + }) + break + } + } + + return a +} + // IsEqualUnordered succeeds if array is equal to another array, ignoring element // order. Before comparison, both arrays are converted to canonical form. // diff --git a/array_test.go b/array_test.go index 020622933..dc58c00b8 100644 --- a/array_test.go +++ b/array_test.go @@ -31,6 +31,8 @@ func TestArray_Failed(t *testing.T) { value.NotEmpty() value.IsEqual([]interface{}{}) value.NotEqual([]interface{}{}) + value.InList([]interface{}{}) + value.NotInList([]interface{}{}) value.IsEqualUnordered([]interface{}{}) value.NotEqualUnordered([]interface{}{}) value.ConsistsOf("foo") @@ -377,6 +379,62 @@ func TestArray_EqualNotEmpty(t *testing.T) { value.chain.clearFailed() } +func TestArray_InList(t *testing.T) { + reporter := newMockReporter(t) + + value := NewArray(reporter, []interface{}{"foo", "bar"}) + + assert.Equal(t, []interface{}{"foo", "bar"}, value.Raw()) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList("foo", "bar") + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList("foo", "bar") + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList([]interface{}{}) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList([]interface{}{}) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList([]interface{}{"bar", "foo"}) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList([]interface{}{"bar", "foo"}) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList([]interface{}{"bar", "foo"}, []interface{}{"foo", "bar"}) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList([]interface{}{"bar", "foo"}, []interface{}{"foo", "bar"}) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList([]interface{}{"bar", "foo"}, []interface{}{"FOO", "BAR"}) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList([]interface{}{"bar", "foo"}, []interface{}{"FOO", "BAR"}) + value.chain.assertNotFailed(t) + value.chain.clearFailed() +} + func TestArray_EqualTypes(t *testing.T) { reporter := newMockReporter(t) diff --git a/assertion_test.go b/assertion_test.go index 765160b3b..e7d59433b 100644 --- a/assertion_test.go +++ b/assertion_test.go @@ -336,18 +336,6 @@ func TestAssertion_ValidateTraits(t *testing.T) { List: fieldRequired, }, }, - { - testName: "AssertionList should not be slice of single element", - errorContainsText: "AssertionList", - failure: AssertionFailure{ - Expected: &AssertionValue{ - Value: AssertionList{[]int{1}}, - }, - }, - traits: fieldTraits{ - List: fieldRequired, - }, - }, } for _, test := range tests { @@ -627,18 +615,6 @@ func TestAssertion_ValidateAssertion(t *testing.T) { Expected: &AssertionValue{AssertionList{}}, }, }, - { - testName: "List has one element and it's list", - errorContainsText: "AssertionList", - input: AssertionFailure{ - Type: AssertBelongs, - Errors: []error{ - errors.New("test"), - }, - Actual: &AssertionValue{}, - Expected: &AssertionValue{AssertionList{[]string{"test"}}}, - }, - }, } for _, test := range tests { diff --git a/assertion_validation.go b/assertion_validation.go index 3edcb4f09..2a9ae069b 100644 --- a/assertion_validation.go +++ b/assertion_validation.go @@ -3,7 +3,6 @@ package httpexpect import ( "errors" "fmt" - "reflect" ) func validateAssertion(failure *AssertionFailure) error { @@ -182,12 +181,6 @@ func validateTraits(failure *AssertionFailure, traits fieldTraits) error { if len(lst) == 0 { return errors.New("AssertionList should be non-empty") } - - if len(lst) == 1 && reflect.ValueOf(lst[0]).Kind() == reflect.Slice { - return errors.New( - "AssertionList should contain a list of values," + - " but it contains a single element which itself is a list") - } } } } diff --git a/boolean.go b/boolean.go index 86120b15d..ac977d0f9 100644 --- a/boolean.go +++ b/boolean.go @@ -162,6 +162,97 @@ func (b *Boolean) Equal(value bool) *Boolean { return b.IsEqual(value) } +// InList succeeds if boolean is equal to one of the elements from given +// list of booleans. +// +// Example: +// +// boolean := NewBoolean(t, true) +// boolean.InList(true, false) +func (b *Boolean) InList(values ...bool) *Boolean { + opChain := b.chain.enter("InList()") + defer opChain.leave() + + if opChain.failed() { + return b + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return b + } + + var isListed bool + for _, v := range values { + if b.value == v { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{b.value}, + Expected: &AssertionValue{AssertionList(boolList(values))}, + Errors: []error{ + errors.New("expected: boolean is equal to one of the values"), + }, + }) + } + + return b +} + +// NotInList succeeds if boolean is not equal to any of the elements from +// given list of booleans. +// +// Example: +// +// boolean := NewBoolean(t, true) +// boolean.NotInList(true, false) +func (b *Boolean) NotInList(values ...bool) *Boolean { + opChain := b.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return b + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return b + } + + for _, v := range values { + if b.value == v { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{b.value}, + Expected: &AssertionValue{AssertionList(boolList(values))}, + Errors: []error{ + errors.New("expected: boolean is not equal to any of the values"), + }, + }) + break + } + } + + return b +} + // True succeeds if boolean is true. // // Example: @@ -217,3 +308,12 @@ func (b *Boolean) False() *Boolean { return b } + +func boolList(values []bool) []interface{} { + l := make([]interface{}, 0, len(values)) + for _, v := range values { + l = append(l, v) + } + + return l +} diff --git a/boolean_test.go b/boolean_test.go index 4d52c2cb1..11e7ddba9 100644 --- a/boolean_test.go +++ b/boolean_test.go @@ -21,6 +21,8 @@ func TestBoolean_Failed(t *testing.T) { value.IsEqual(false) value.NotEqual(false) + value.InList(false) + value.NotInList(false) value.True() value.False() } @@ -160,6 +162,14 @@ func TestBoolean_True(t *testing.T) { value.False() value.chain.assertFailed(t) value.chain.clearFailed() + + value.InList(true, true) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(true, false) + value.chain.assertFailed(t) + value.chain.clearFailed() } func TestBoolean_False(t *testing.T) { @@ -192,4 +202,26 @@ func TestBoolean_False(t *testing.T) { value.False() value.chain.assertNotFailed(t) value.chain.clearFailed() + + value.InList(true, true) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(true, true) + value.chain.assertNotFailed(t) + value.chain.clearFailed() +} + +func TestBoolean_ZeroLengthInList(t *testing.T) { + reporter := newMockReporter(t) + + value := NewBoolean(reporter, true) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList() + value.chain.assertFailed(t) + value.chain.clearFailed() } diff --git a/datetime.go b/datetime.go index 5de349e9d..fa6af2e29 100644 --- a/datetime.go +++ b/datetime.go @@ -422,6 +422,97 @@ func (dt *DateTime) NotInRange(min, max time.Time) *DateTime { return dt } +// InList succeeds if DateTime is equal to one of the elements from given +// list of time.Time. +// +// Example: +// +// dt := NewDateTime(t, time.Unix(0, 2)) +// dt.InRange(time.Unix(0, 1), time.Unix(0, 2)) +func (dt *DateTime) InList(values ...time.Time) *DateTime { + opChain := dt.chain.enter("InRange()") + defer opChain.leave() + + if opChain.failed() { + return dt + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return dt + } + + var isListed bool + for _, v := range values { + if dt.value.Equal(v) { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{dt.value}, + Expected: &AssertionValue{AssertionList(timeList(values))}, + Errors: []error{ + errors.New("expected: time point is equal to one of the values"), + }, + }) + } + + return dt +} + +// NotInList succeeds if DateTime is not equal to any of the elements from +// given list of time.Time. +// +// Example: +// +// dt := NewDateTime(t, time.Unix(0, 2)) +// dt.InRange(time.Unix(0, 1), time.Unix(0, 3)) +func (dt *DateTime) NotInList(values ...time.Time) *DateTime { + opChain := dt.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return dt + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return dt + } + + for _, v := range values { + if dt.value.Equal(v) { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{dt.value}, + Expected: &AssertionValue{AssertionList(timeList(values))}, + Errors: []error{ + errors.New("expected: time point is not equal to any of the values"), + }, + }) + break + } + } + + return dt +} + // Gt succeeds if DateTime is greater than given value. // // Example: @@ -569,3 +660,12 @@ func (dt *DateTime) AsLocal() *DateTime { return newDateTime(opChain, dt.value.Local()) } + +func timeList(values []time.Time) []interface{} { + l := make([]interface{}, 0, len(values)) + for _, v := range values { + l = append(l, v) + } + + return l +} diff --git a/datetime_test.go b/datetime_test.go index fb511bc77..8da670225 100644 --- a/datetime_test.go +++ b/datetime_test.go @@ -25,6 +25,8 @@ func TestDateTime_Failed(t *testing.T) { value.Le(tm) value.InRange(tm, tm) value.NotInRange(tm, tm) + value.InList(tm, tm) + value.NotInList(tm, tm) value.Zone() value.Year() value.Month() @@ -244,3 +246,65 @@ func TestDateTime_InRange(t *testing.T) { value.chain.assertNotFailed(t) value.chain.clearFailed() } + +func TestDateTime_InList(t *testing.T) { + reporter := newMockReporter(t) + + value := NewDateTime(reporter, time.Unix(0, 1234)) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Unix(0, 1234), time.Unix(0, 1234)) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Unix(0, 1234), time.Unix(0, 1234)) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Unix(0, 1234-1), time.Unix(0, 1234)) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Unix(0, 1234-1), time.Unix(0, 1234)) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Unix(0, 1234), time.Unix(0, 1234+1)) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Unix(0, 1234), time.Unix(0, 1234+1)) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Unix(0, 1234+1), time.Unix(0, 1234+2)) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Unix(0, 1234+1), time.Unix(0, 1234+2)) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList(time.Unix(0, 1234-2), time.Unix(0, 1234-1)) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Unix(0, 1234-2), time.Unix(0, 1234-1)) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList(time.Unix(0, 1234+1), time.Unix(0, 1234-1)) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Unix(0, 1234+1), time.Unix(0, 1234-1)) + value.chain.assertNotFailed(t) + value.chain.clearFailed() +} diff --git a/duration.go b/duration.go index 35e64d8ca..78d02e60f 100644 --- a/duration.go +++ b/duration.go @@ -429,3 +429,126 @@ func (d *Duration) NotInRange(min, max time.Duration) *Duration { return d } + +// InList succeeds if Duration is equal to one of the elements from given +// list of time.Duration. +// +// Example: +// +// d := NewDuration(t, time.Minute) +// d.InList(time.Minute, time.Hour) +func (d *Duration) InList(values ...time.Duration) *Duration { + opChain := d.chain.enter("InList()") + defer opChain.leave() + + if opChain.failed() { + return d + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return d + } + + if d.value == nil { + opChain.fail(AssertionFailure{ + Type: AssertNotNil, + Actual: &AssertionValue{d.value}, + Errors: []error{ + errors.New("expected: duration is present"), + }, + }) + return d + } + + var isListed bool + for _, v := range values { + if *d.value == v { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{d.value}, + Expected: &AssertionValue{AssertionList(durationList(values))}, + Errors: []error{ + errors.New("expected: duration is equal to one of the values"), + }, + }) + } + + return d +} + +// NotInList succeeds if Duration is not equal to any of the elements from +// given list of time.Duration. +// +// Example: +// +// d := NewDuration(t, time.Minute) +// d.NotInList(time.Second, time.Hour) +func (d *Duration) NotInList(values ...time.Duration) *Duration { + opChain := d.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return d + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return d + } + + if d.value == nil { + opChain.fail(AssertionFailure{ + Type: AssertNotNil, + Actual: &AssertionValue{d.value}, + Errors: []error{ + errors.New("expected: duration is present"), + }, + }) + + return d + } + + for _, v := range values { + if *d.value == v { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{d.value}, + Expected: &AssertionValue{AssertionList(durationList(values))}, + Errors: []error{ + errors.New("expected: duration is not equal to any of the values"), + }, + }) + break + } + } + + return d +} + +func durationList(values []time.Duration) []interface{} { + l := make([]interface{}, 0, len(values)) + for _, v := range values { + l = append(l, v) + } + + return l +} diff --git a/duration_test.go b/duration_test.go index add4e745e..4144736be 100644 --- a/duration_test.go +++ b/duration_test.go @@ -16,6 +16,8 @@ func TestDuration_Failed(t *testing.T) { value.IsEqual(tm) value.NotEqual(tm) + value.InList(tm) + value.NotInList(tm) value.Gt(tm) value.Ge(tm) value.Lt(tm) @@ -223,3 +225,52 @@ func TestDuration_InRange(t *testing.T) { value.chain.assertNotFailed(t) value.chain.clearFailed() } + +func TestDuration_InList(t *testing.T) { + reporter := newMockReporter(t) + + newDuration(newMockChain(t), nil).InList(time.Second).chain.assertFailed(t) + newDuration(newMockChain(t), nil).NotInList(time.Second).chain.assertFailed(t) + + value := NewDuration(reporter, time.Second) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Second, time.Minute) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Second, time.Minute) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Second-1, time.Minute) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Second-1, time.Minute) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList(time.Second, time.Second+1) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Second, time.Second+1) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(time.Second+1, time.Second-1) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(time.Second+1, time.Second-1) + value.chain.assertNotFailed(t) + value.chain.clearFailed() +} diff --git a/number.go b/number.go index 0ab4a0fbc..4606fd04e 100644 --- a/number.go +++ b/number.go @@ -366,6 +366,113 @@ func (n *Number) NotInRange(min, max interface{}) *Number { return n } +// InList succeeds if number is equal to one of the elements from given +// list of numbers. +// Before comparison, each value is converted to canonical form. +// +// Each value should be numeric type convertible to float64. +// +// Example: +// +// number := NewNumber(t, 123) +// number.InList(float64(123), int32(123)) +func (n *Number) InList(values ...interface{}) *Number { + opChain := n.chain.enter("IsList()") + defer opChain.leave() + + if opChain.failed() { + return n + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return n + } + + var isListed bool + for _, v := range values { + num, ok := canonNumber(opChain, v) + if !ok { + return n + } + + if n.value == num { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{n.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: number is equal to one of the values"), + }, + }) + } + + return n +} + +// NotInList succeeds if whole array is not equal to any of the elements +// from given list of numbers. +// Before comparison, each value is converted to canonical form. +// +// Each value should be numeric type convertible to float64. +// +// Example: +// +// number := NewNumber(t, 123) +// number.NotInList(float64(456), int32(456)) +func (n *Number) NotInList(values ...interface{}) *Number { + opChain := n.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return n + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return n + } + + for _, v := range values { + num, ok := canonNumber(opChain, v) + if !ok { + return n + } + + if n.value == num { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{n.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: number is not equal to any of the values"), + }, + }) + break + } + } + + return n +} + // Gt succeeds if number is greater than given value. // // value should have numeric type convertible to float64. Before comparison, diff --git a/number_test.go b/number_test.go index 7c2a83b88..6f7f0ec6d 100644 --- a/number_test.go +++ b/number_test.go @@ -22,6 +22,8 @@ func TestNumber_Failed(t *testing.T) { value.IsEqual(0) value.NotEqual(0) + value.InList(0) + value.NotInList(0) value.InDelta(0, 0) value.NotInDelta(0, 0) value.Gt(0) @@ -309,6 +311,52 @@ func TestNumber_InRange(t *testing.T) { value.chain.clearFailed() } +func TestNumber_InList(t *testing.T) { + reporter := newMockReporter(t) + + value := NewNumber(reporter, 1234) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(1234, 4567) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(1234, 4567) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(1234.00, 4567.00) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(1234.00, 4567.00) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(4567.00, 1234.01) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(4567.00, 1234.01) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList(1234+1, "1234") + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList("1234+1", 1234+2) + value.chain.assertFailed(t) + value.chain.clearFailed() +} + func TestNumber_Greater(t *testing.T) { reporter := newMockReporter(t) diff --git a/object.go b/object.go index df80dc25f..cfe21afce 100644 --- a/object.go +++ b/object.go @@ -744,6 +744,119 @@ func (o *Object) Equal(value interface{}) *Object { return o.IsEqual(value) } +// InList succeeds if whole object is equal to one of the elements from given +// list of objects. +// Before comparison, each object is converted to canonical form. +// +// Each value should be map[string]interface{} or struct. +// +// Example: +// +// object := NewObject(t, map[string]interface{}{"foo": 123}) +// object.InList( +// map[string]interface{}{"foo": 123}, +// map[string]interface{}{"bar": 456}, +// ) +func (o *Object) InList(values ...interface{}) *Object { + opChain := o.chain.enter("InList()") + defer opChain.leave() + + if opChain.failed() { + return o + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return o + } + + var isListed bool + for _, v := range values { + expected, ok := canonMap(opChain, v) + if !ok { + return o + } + + if reflect.DeepEqual(expected, o.value) { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{o.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: map is equal to one of the values"), + }, + }) + } + + return o +} + +// NotInList succeeds if whole object is equal to any of the elements from +// given list of objects. +// Before comparison, each object is converted to canonical form. +// +// Each value should be map[string]interface{} or struct. +// +// Example: +// +// object := NewObject(t, map[string]interface{}{"foo": 123}) +// object.NotInList( +// map[string]interface{}{"bar": 456}, +// map[string]interface{}{"baz": 789}, +// ) +func (o *Object) NotInList(values ...interface{}) *Object { + opChain := o.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return o + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return o + } + + for _, v := range values { + expected, ok := canonMap(opChain, v) + if !ok { + return o + } + + if reflect.DeepEqual(expected, o.value) { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{o.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: map is not equal to any of the values"), + }, + }) + break + } + } + + return o +} + // ContainsKey succeeds if object contains given key. // // Example: diff --git a/object_test.go b/object_test.go index 3e790a76f..aa73b98b5 100644 --- a/object_test.go +++ b/object_test.go @@ -26,6 +26,8 @@ func TestObject_Failed(t *testing.T) { value.NotEmpty() value.IsEqual(nil) value.NotEqual(nil) + value.InList(nil) + value.NotInList(nil) value.ContainsKey("foo") value.NotContainsKey("foo") value.ContainsValue("foo") @@ -421,6 +423,92 @@ func TestObject_Equal(t *testing.T) { value.chain.clearFailed() } +func TestObject_InList(t *testing.T) { + reporter := newMockReporter(t) + + value := NewObject(reporter, map[string]interface{}{"foo": 123.0}) + + assert.Equal(t, map[string]interface{}{"foo": 123.0}, value.Raw()) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(map[string]interface{}{}) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(map[string]interface{}{}) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList( + map[string]interface{}{"FOO": 123.0}, + map[string]interface{}{"BAR": 456.0}, + ) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList( + map[string]interface{}{"FOO": 123.0}, + map[string]interface{}{"BAR": 456.0}, + ) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList( + map[string]interface{}{"foo": 456.0}, + map[string]interface{}{"bar": 123.0}, + ) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList( + map[string]interface{}{"foo": 456.0}, + map[string]interface{}{"bar": 123.0}, + ) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList( + map[string]interface{}{"foo": 123.0}, + map[string]interface{}{"bar": 456.0}, + ) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList( + map[string]interface{}{"foo": 123.0}, + map[string]interface{}{"bar": 456.0}, + ) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(struct { + Foo float64 `json:"foo"` + }{Foo: 123.00}) + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList(struct { + Foo float64 `json:"foo"` + }{Foo: 123.00}) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList(nil) + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList(nil) + value.chain.assertFailed(t) + value.chain.clearFailed() +} + func TestObject_EqualStruct(t *testing.T) { reporter := newMockReporter(t) diff --git a/string.go b/string.go index f6b97575d..834c5ec9f 100644 --- a/string.go +++ b/string.go @@ -245,6 +245,96 @@ func (s *String) Equal(value string) *String { return s.IsEqual(value) } +// InList succeeds if string is equal to one of the elements from given +// list of strings. +// +// Example: +// +// str := NewString(t, "Hello") +// str.InList("Hello", "Goodbye") +func (s *String) InList(values ...string) *String { + opChain := s.chain.enter("InList()") + defer opChain.leave() + + if opChain.failed() { + return s + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return s + } + + var isListed bool + for _, v := range values { + if s.value == v { + isListed = true + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{s.value}, + Expected: &AssertionValue{AssertionList(stringList(values))}, + Errors: []error{ + errors.New("expected: string is equal to one of the values"), + }, + }) + } + + return s +} + +// NotInList succeeds if string is not equal to any of the elements from +// given list of strings. +// +// Example: +// +// str := NewString(t, "Hello") +// str.NotInList("NotInList", "Goodbye") +func (s *String) NotInList(values ...string) *String { + opChain := s.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return s + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return s + } + + for _, v := range values { + if s.value == v { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{s.value}, + Expected: &AssertionValue{AssertionList(stringList(values))}, + Errors: []error{ + errors.New("expected: string is not equal to any of the values"), + }, + }) + break + } + } + + return s +} + // IsEqualFold succeeds if string is equal to given Go string after applying Unicode // case-folding (so it's a case-insensitive match). // @@ -1142,3 +1232,12 @@ func (s *String) Number() *Number { func (s *String) DateTime(layout ...string) *DateTime { return s.AsDateTime(layout...) } + +func stringList(values []string) []interface{} { + s := make([]interface{}, 0, len(values)) + for _, v := range values { + s = append(s, v) + } + + return s +} diff --git a/string_test.go b/string_test.go index 03d7df258..e2a43d6dd 100644 --- a/string_test.go +++ b/string_test.go @@ -29,6 +29,8 @@ func TestString_Failed(t *testing.T) { value.NotEmpty() value.IsEqual("") value.NotEqual("") + value.InList("") + value.NotInList("") value.IsEqualFold("") value.NotEqualFold("") value.Contains("") @@ -218,6 +220,38 @@ func TestString_Equal(t *testing.T) { value.chain.clearFailed() } +func TestString_List(t *testing.T) { + reporter := newMockReporter(t) + + value := NewString(reporter, "foo") + + assert.Equal(t, "foo", value.Raw()) + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList() + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.InList("foo", "bar") + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.InList("FOO", "BAR") + value.chain.assertFailed(t) + value.chain.clearFailed() + + value.NotInList("FOO", "bar") + value.chain.assertNotFailed(t) + value.chain.clearFailed() + + value.NotInList("foo", "BAR") + value.chain.assertFailed(t) + value.chain.clearFailed() +} + func TestString_EqualFold(t *testing.T) { reporter := newMockReporter(t) diff --git a/value.go b/value.go index 19632d569..c32bd1d97 100644 --- a/value.go +++ b/value.go @@ -523,3 +523,106 @@ func (v *Value) NotEqual(value interface{}) *Value { func (v *Value) Equal(value interface{}) *Value { return v.IsEqual(value) } + +// InList succeeds if whole value is equal to one of the elements from given +// list of values (e.g. map, slice, string, etc). +// Before comparison, each value are converted to canonical form. +// +// Example: +// +// value := NewValue(t, "foo") +// value.InList("foo", map[string]interface{}{"bar": true}) +func (v *Value) InList(values ...interface{}) *Value { + opChain := v.chain.enter("InList()") + defer opChain.leave() + + if opChain.failed() { + return v + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return v + } + + var isListed bool + for _, val := range values { + expected, ok := canonValue(opChain, val) + if !ok { + return v + } + + if reflect.DeepEqual(expected, v.value) { + isListed = true + break + } + } + + if !isListed { + opChain.fail(AssertionFailure{ + Type: AssertBelongs, + Actual: &AssertionValue{v.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: value is equal to one of the values"), + }, + }) + } + + return v +} + +// NotInList succeeds if whole value is not equal to any of the elements from +// given list of values (e.g. map, slice, string, etc). +// Before comparison, each value are converted to canonical form. +// +// Example: +// +// value := NewValue(t, "foo") +// value.NotInList("bar", map[string]interface{}{"bar": true}) +func (v *Value) NotInList(values ...interface{}) *Value { + opChain := v.chain.enter("NotInList()") + defer opChain.leave() + + if opChain.failed() { + return v + } + + if len(values) == 0 { + opChain.fail(AssertionFailure{ + Type: AssertUsage, + Errors: []error{ + errors.New("unexpected empty list argument"), + }, + }) + + return v + } + + for _, val := range values { + expected, ok := canonValue(opChain, val) + if !ok { + return v + } + + if reflect.DeepEqual(expected, v.value) { + opChain.fail(AssertionFailure{ + Type: AssertNotBelongs, + Actual: &AssertionValue{v.value}, + Expected: &AssertionValue{AssertionList(values)}, + Errors: []error{ + errors.New("expected: value is not equal to any of the values"), + }, + }) + break + } + } + + return v +} diff --git a/value_test.go b/value_test.go index eb684bb7a..5d0e6106f 100644 --- a/value_test.go +++ b/value_test.go @@ -42,6 +42,9 @@ func TestValue_Failed(t *testing.T) { value.IsEqual(nil) value.NotEqual(nil) + + value.InList(nil) + value.NotInList(nil) } func TestValue_Constructors(t *testing.T) { @@ -406,6 +409,39 @@ func TestValue_Equal(t *testing.T) { NewValue(reporter, data1).NotEqual(func() {}).chain.assertFailed(t) } +func TestValue_InList(t *testing.T) { + reporter := newMockReporter(t) + + data1 := map[string]interface{}{"foo": "bar"} + data2 := "baz" + data3 := struct { + Data []int `json:"data"` + }{ + Data: []int{1, 2, 3, 4}, + } + + NewValue(reporter, data1).InList().chain.assertFailed(t) + NewValue(reporter, data2).NotInList().chain.assertFailed(t) + + NewValue(reporter, data1).InList(data1, data3).chain.assertNotFailed(t) + NewValue(reporter, data2).NotInList(data1, data3).chain.assertNotFailed(t) + + NewValue(reporter, data1).InList(data2, data3).chain.assertFailed(t) + NewValue(reporter, data2).NotInList(data2, data3).chain.assertFailed(t) + + NewValue(reporter, data1).InList(data2).chain.assertFailed(t) + NewValue(reporter, data2).NotInList(data2).chain.assertFailed(t) + + NewValue(reporter, data1).InList(data1).chain.assertNotFailed(t) + NewValue(reporter, data2).NotInList(data1).chain.assertNotFailed(t) + + NewValue(reporter, nil).InList(map[string]interface{}(nil)).chain.assertNotFailed(t) + NewValue(reporter, nil).NotInList(map[string]interface{}{}).chain.assertNotFailed(t) + + NewValue(reporter, data1).InList(func() {}).chain.assertFailed(t) + NewValue(reporter, data1).NotInList(func() {}).chain.assertFailed(t) +} + func TestValue_PathObject(t *testing.T) { reporter := newMockReporter(t)