Repository: gavv/httpexpect
Pull request #188 (2022-12-03 16:01:55)

feat(string): implement string prefix and suffix assertion
Issue #153 
Hi @gavv kindly help to review this PR and let me know if anything to be revised. Thank you

coveralls: 
[![Coverage Status](https://coveralls.io/builds/54811239/badge)](https://coveralls.io/builds/54811239)

Coverage decreased (-0.2%) to 92.208% when pulling **7e6e500c0f13faee0205a649ea507773add7f356 on edwinwijaya94:f_edwin_string_prefix_suffix** into **f7e16e005c4254aff1b19d4a9ac8763a42ba0be7 on gavv:master**.


Patch:
diff --git a/.gitignore b/.gitignore
index 0bf55fd..8a1c41d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +1,2 @@
 TODO.org
+vendor/
\ No newline at end of file
diff --git a/string.go b/string.go
index fccda44..fd6f401 100644
--- a/string.go
+++ b/string.go
@@ -359,6 +359,234 @@ func (s *String) NotContainsFold(value string) *String {
 	return s
 }
 
+// HasPrefix succeeds if string has given Go string as prefix
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.HasPrefix("Hello")
+func (s *String) HasPrefix(value string) *String {
+	s.chain.enter("HasPrefix()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if !strings.HasPrefix(s.value, value) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string has prefix"),
+			},
+		})
+	}
+
+	return s
+}
+
+// NotHasPrefix succeeds if string doesn't have given Go string as prefix
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.NotHasPrefix("Bye")
+func (s *String) NotHasPrefix(value string) *String {
+	s.chain.enter("NotHasPrefix()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if strings.HasPrefix(s.value, value) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertNotContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string doesn't have prefix"),
+			},
+		})
+	}
+
+	return s
+}
+
+// HasSuffix succeeds if string has given Go string as suffix
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.HasSuffix("World")
+func (s *String) HasSuffix(value string) *String {
+	s.chain.enter("HasSuffix()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if !strings.HasSuffix(s.value, value) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string has suffix"),
+			},
+		})
+	}
+
+	return s
+}
+
+// NotHasSuffix succeeds if string doesn't have given Go string as suffix
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.NotHasSuffix("Hello")
+func (s *String) NotHasSuffix(value string) *String {
+	s.chain.enter("NotHasSuffix()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if strings.HasSuffix(s.value, value) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertNotContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string doesn't have suffix"),
+			},
+		})
+	}
+
+	return s
+}
+
+// HasPrefixFold succeeds if string has given Go string as prefix
+// after applying Unicode case-folding (so it's a case-insensitive match).
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.HasPrefixFold("hello")
+func (s *String) HasPrefixFold(value string) *String {
+	s.chain.enter("HasPrefixFold()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if !strings.HasPrefix(strings.ToLower(s.value), strings.ToLower(value)) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string has prefix (if folded)"),
+			},
+		})
+	}
+
+	return s
+}
+
+// NotHasPrefixFold succeeds if string doesn't have given Go string as prefix
+// after applying Unicode case-folding (so it's a case-insensitive match).
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.NotHasPrefixFold("Bye")
+func (s *String) NotHasPrefixFold(value string) *String {
+	s.chain.enter("NotHasPrefixFold()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if strings.HasPrefix(strings.ToLower(s.value), strings.ToLower(value)) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertNotContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string doesn't have prefix (if folded)"),
+			},
+		})
+	}
+
+	return s
+}
+
+// HasSuffixFold succeeds if string has given Go string as suffix
+// after applying Unicode case-folding (so it's a case-insensitive match).
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.HasSuffixFold("world")
+func (s *String) HasSuffixFold(value string) *String {
+	s.chain.enter("HasSuffixFold()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if !strings.HasSuffix(strings.ToLower(s.value), strings.ToLower(value)) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string has suffix (if folded)"),
+			},
+		})
+	}
+
+	return s
+}
+
+// NotHasSuffixFold succeeds if string doesn't have given Go string as suffix
+// after applying Unicode case-folding (so it's a case-insensitive match).
+//
+// Example:
+//
+//	str := NewString(t, "Hello World")
+//	str.NotHasSuffixFold("Bye")
+func (s *String) NotHasSuffixFold(value string) *String {
+	s.chain.enter("NotHasSuffix()")
+	defer s.chain.leave()
+
+	if s.chain.failed() {
+		return s
+	}
+
+	if strings.HasSuffix(strings.ToLower(s.value), strings.ToLower(value)) {
+		s.chain.fail(AssertionFailure{
+			Type:     AssertNotContainsSubset,
+			Actual:   &AssertionValue{s.value},
+			Expected: &AssertionValue{value},
+			Errors: []error{
+				errors.New("expected: string doesn't have suffix (if folded)"),
+			},
+		})
+	}
+
+	return s
+}
+
 // Match matches the string with given regexp and returns a new Match instance
 // with found submatches.
 //
@@ -552,32 +780,59 @@ func (s *String) AsNumber(base ...int) *Number {
 		return newNumber(s.chain, 0)
 	}
 
-	var num float64
+	b := 10
+	if len(base) != 0 {
+		b = base[0]
+	}
+
+	var fnum float64
+	var inum int64
+	var unum uint64
 	var err error
 
-	if len(base) == 0 || base[0] == 10 {
-		num, err = strconv.ParseFloat(s.value, 64)
-	} else {
-		var inum int64
+	inum, err = strconv.ParseInt(s.value, b, 64)
+	fnum = float64(inum)
 
-		inum, err = strconv.ParseInt(s.value, base[0], 64)
-		num = float64(inum)
+	if err == nil && int64(fnum) != inum {
+		s.chain.fail(AssertionFailure{
+			Type:   AssertValid,
+			Actual: &AssertionValue{s.value},
+			Errors: []error{
+				errors.New("expected:" +
+					" number can be represented as float64 without precision loss"),
+			},
+		})
+		return newNumber(s.chain, 0)
+	}
 
-		if err == strconv.ErrRange {
-			var unum uint64
+	if err != nil && errors.Is(err, strconv.ErrRange) {
+		unum, err = strconv.ParseUint(s.value, b, 64)
+		fnum = float64(unum)
 
-			unum, err = strconv.ParseUint(s.value, base[0], 64)
-			num = float64(unum)
+		if err == nil && uint64(fnum) != unum {
+			s.chain.fail(AssertionFailure{
+				Type:   AssertValid,
+				Actual: &AssertionValue{s.value},
+				Errors: []error{
+					errors.New("expected:" +
+						" number can be represented as float64 without precision loss"),
+				},
+			})
+			return newNumber(s.chain, 0)
 		}
 	}
 
+	if err != nil && b == 10 {
+		fnum, err = strconv.ParseFloat(s.value, 64)
+	}
+
 	if err != nil {
-		if len(base) == 0 || base[0] == 10 {
+		if b == 10 {
 			s.chain.fail(AssertionFailure{
 				Type:   AssertValid,
 				Actual: &AssertionValue{s.value},
 				Errors: []error{
-					errors.New("expected: string can be parsed to float"),
+					errors.New("expected: string can be parsed to integer or float"),
 					err,
 				},
 			})
@@ -596,7 +851,7 @@ func (s *String) AsNumber(base ...int) *Number {
 		return newNumber(s.chain, 0)
 	}
 
-	return newNumber(s.chain, num)
+	return newNumber(s.chain, fnum)
 }
 
 // AsBoolean parses true/false value string and returns a new Boolean instance


Test patch:
diff --git a/number_test.go b/number_test.go
index ce33b5e..e4ce0be 100644
--- a/number_test.go
+++ b/number_test.go
@@ -252,6 +252,14 @@ func TestNumberInRange(t *testing.T) {
 	value.NotInRange(1234+1, 1234-1)
 	value.chain.assertOK(t)
 	value.chain.reset()
+
+	value.NotInRange(1234+1, "1234+2")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotInRange("1234+1", 1234+2)
+	value.chain.assertFailed(t)
+	value.chain.reset()
 }
 
 func TestNumberConvertEqual(t *testing.T) {
diff --git a/string_test.go b/string_test.go
index 8aba3cd..b122ba1 100644
--- a/string_test.go
+++ b/string_test.go
@@ -31,6 +31,14 @@ func TestStringFailed(t *testing.T) {
 	value.NotContains("")
 	value.ContainsFold("")
 	value.NotContainsFold("")
+	value.HasPrefix("")
+	value.NotHasPrefix("")
+	value.HasSuffix("")
+	value.NotHasSuffix("")
+	value.HasPrefixFold("")
+	value.NotHasPrefixFold("")
+	value.HasSuffixFold("")
+	value.NotHasSuffixFold("")
 	value.Match("")
 	value.NotMatch("")
 	value.MatchAll("")
@@ -334,13 +342,53 @@ func TestStringAsNumber(t *testing.T) {
 		num1 := value1.AsNumber(16)
 		value1.chain.assertOK(t)
 		num1.chain.assertOK(t)
-		assert.Equal(t, float64(256), num1.Raw())
+		assert.Equal(t, float64(0x100), num1.Raw())
 
 		value2 := NewString(reporter, "11.22")
 		num2 := value2.AsNumber(16)
 		value2.chain.assertFailed(t)
 		num2.chain.assertFailed(t)
 		assert.Equal(t, float64(0), num2.Raw())
+
+		value3 := NewString(reporter, "4000000000000000")
+		num3 := value3.AsNumber(16)
+		value3.chain.assertOK(t)
+		num3.chain.assertOK(t)
+		assert.Equal(t, float64(0x4000000000000000), num3.Raw())
+	})
+
+	t.Run("float_precision", func(t *testing.T) {
+		value1 := NewString(reporter, "4611686018427387905")
+		num1 := value1.AsNumber()
+		value1.chain.assertFailed(t)
+		num1.chain.assertFailed(t)
+		assert.Equal(t, float64(0), num1.Raw())
+
+		value2 := NewString(reporter, "4611686018427387905")
+		num2 := value2.AsNumber(10)
+		value2.chain.assertFailed(t)
+		num2.chain.assertFailed(t)
+		assert.Equal(t, float64(0), num2.Raw())
+
+		value3 := NewString(reporter, "8000000000000001")
+		num3 := value3.AsNumber(16)
+		value3.chain.assertFailed(t)
+		num3.chain.assertFailed(t)
+		assert.Equal(t, float64(0), num3.Raw())
+
+		value4 := NewString(reporter, "-4000000000000001")
+		num4 := value4.AsNumber(16)
+		value4.chain.assertFailed(t)
+		num4.chain.assertFailed(t)
+		assert.Equal(t, float64(0), num4.Raw())
+	})
+
+	t.Run("multiple_base", func(t *testing.T) {
+		value1 := NewString(reporter, "100")
+		num1 := value1.AsNumber(10, 16)
+		value1.chain.assertFailed(t)
+		num1.chain.assertFailed(t)
+		assert.Equal(t, float64(0), num1.Raw())
 	})
 }
 
@@ -434,3 +482,159 @@ func TestStringAsDateTime(t *testing.T) {
 		}
 	}
 }
+
+func TestStringHasPrefix(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewString(reporter, "Hello World")
+
+	value.HasPrefix("Hello")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefix("Hello World")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefix("")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefix("Hello!")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.HasPrefix("hello")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.HasPrefix("World")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasPrefix("Bye")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.NotHasPrefix("Hello")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasPrefix("hello")
+	value.chain.assertOK(t)
+	value.chain.reset()
+}
+
+func TestStringHasSuffix(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewString(reporter, "Hello World")
+
+	value.HasSuffix("World")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasSuffix("Hello World")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasSuffix("")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefix("World!")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.HasSuffix("world")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.HasSuffix("Hello")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasSuffix("Bye")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.NotHasSuffix("World")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasSuffix("world")
+	value.chain.assertOK(t)
+	value.chain.reset()
+}
+
+func TestStringHasPrefixFold(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewString(reporter, "Hello World")
+
+	value.HasPrefixFold("hello")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefixFold("HeLlO wOrLd")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefixFold("")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasPrefixFold("World")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasPrefixFold("Bye")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.NotHasPrefixFold("world")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.NotHasPrefixFold("world!")
+	value.chain.assertOK(t)
+	value.chain.reset()
+}
+
+func TestStringHasSuffixFold(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	value := NewString(reporter, "Hello World")
+
+	value.HasSuffixFold("world")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasSuffixFold("hElLo WoRlD")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasSuffixFold("")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.HasSuffixFold("hello")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.HasSuffixFold("world!")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasSuffixFold("Bye")
+	value.chain.assertOK(t)
+	value.chain.reset()
+
+	value.NotHasSuffixFold("world")
+	value.chain.assertFailed(t)
+	value.chain.reset()
+
+	value.NotHasSuffixFold("world!")
+	value.chain.assertOK(t)
+	value.chain.reset()
+}