Repository: cloudwego/hertz (go)

Issue:
fix assert.NotNil not working
**Describe the bug**

使用 assert 的 NotNil 断言时不生效

**Replay**
测试代码：
```golang
import  "github.com/cloudwego/hertz/pkg/common/test/assert"
func TestAssertNotNil(t *testing.T) {
	var test interface{}
	assert.NotNil(t, test)
	fmt.Println("test 值不为空")
}
```
![image](https://github.com/user-attachments/assets/bd38ff61-e7a2-4893-8e8b-80a0b76b09ce)


**Hertz version:**

`
version: 0.9.2
`


Pull request description:
fix: update ut for assert.NotNil
#### What type of PR is this?
fix
<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [X] This PR title match the format: \<type\>(optional scope): \<description\>
- [X] The description of this PR title is user-oriented and clear enough for others to understand.
- [ ] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.
修复 assert的 NotNil 不生效。（参数判断有误）

#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en:
zh(optional):
见 https://github.com/cloudwego/hertz/issues/1179

#### (Optional) Which issue(s) this PR fixes:
<!--
Automatically closes linked issue when PR is merged.
Eg: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
close #1178

#### (Optional) The PR that updates user documentation:
<!--
If the current PR requires user awareness at the usage level, please submit a PR to update user docs. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)
-->

Patch:
diff --git a/pkg/app/context.go b/pkg/app/context.go
index 369ece7..1d925b7 100644
--- a/pkg/app/context.go
+++ b/pkg/app/context.go
@@ -694,6 +694,17 @@ func (ctx *RequestContext) multipartFormValue(key string) (string, bool) {
 	return "", false
 }
 
+func (ctx *RequestContext) multipartFormValueArray(key string) ([]string, bool) {
+	mf, err := ctx.MultipartForm()
+	if err == nil && mf.Value != nil {
+		vv := mf.Value[key]
+		if len(vv) > 0 {
+			return vv, true
+		}
+	}
+	return nil, false
+}
+
 func (ctx *RequestContext) RequestBodyStream() io.Reader {
 	return ctx.Request.BodyStream()
 }
@@ -1366,6 +1377,13 @@ func (ctx *RequestContext) PostForm(key string) string {
 	return value
 }
 
+// PostFormArray returns the specified key from a POST urlencoded form or multipart form
+// when it exists, otherwise it returns an empty array `([])`.
+func (ctx *RequestContext) PostFormArray(key string) []string {
+	values, _ := ctx.GetPostFormArray(key)
+	return values
+}
+
 // DefaultPostForm returns the specified key from a POST urlencoded form or multipart form
 // when it exists, otherwise it returns the specified defaultValue string.
 //
@@ -1393,6 +1411,28 @@ func (ctx *RequestContext) GetPostForm(key string) (string, bool) {
 	return ctx.multipartFormValue(key)
 }
 
+// GetPostFormArray is like PostFormArray(key). It returns the specified key from a POST urlencoded
+// form or multipart form when it exists `([]string, true)` (even when the value is an empty string),
+// otherwise it returns ([]string(nil), false).
+//
+// For example, during a PATCH request to update the item's tags:
+//
+//	    tag=tag1 tag=tag2 tag=tag3  -->  (["tag1", "tag2", "tag3"], true) := GetPostFormArray("tags") // set tags to ["tag1", "tag2", "tag3"]
+//		   tags=                  -->  (nil, true) := GetPostFormArray("tags") // set tags to nil
+//	                            -->  (nil, false) := GetPostFormArray("tags") // do nothing with tags
+func (ctx *RequestContext) GetPostFormArray(key string) ([]string, bool) {
+	vs := ctx.PostArgs().PeekAll(key)
+	values := make([]string, len(vs))
+	for i, v := range vs {
+		values[i] = string(v)
+	}
+	if len(values) == 0 {
+		return ctx.multipartFormValueArray(key)
+	} else {
+		return values, true
+	}
+}
+
 // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
 func bodyAllowedForStatus(status int) bool {
 	switch {


Test patch:
diff --git a/pkg/app/context_test.go b/pkg/app/context_test.go
index 1317a06..b2aef63 100644
--- a/pkg/app/context_test.go
+++ b/pkg/app/context_test.go
@@ -367,6 +367,40 @@ hello=world`)
 	}
 }
 
+func TestPostFormArray(t *testing.T) {
+	t.Parallel()
+
+	ctx := makeCtxByReqString(t, `POST /upload HTTP/1.1
+Host: localhost:10000
+Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Length: 521
+
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="tag"
+
+red
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="tag"
+
+green
+------WebKitFormBoundaryJwfATyF8tmxSJnLg
+Content-Disposition: form-data; name="tag"
+
+blue
+------WebKitFormBoundaryJwfATyF8tmxSJnLg--
+`)
+	assert.DeepEqual(t, []string{"red", "green", "blue"}, ctx.PostFormArray("tag"))
+
+	ctx = makeCtxByReqString(t, `POST /upload HTTP/1.1
+Host: localhost:10000
+Content-Type: application/x-www-form-urlencoded; charset=UTF-8
+Content-Length: 26
+
+tag=red&tag=green&tag=blue
+`)
+	assert.DeepEqual(t, []string{"red", "green", "blue"}, ctx.PostFormArray("tag"))
+}
+
 func TestDefaultPostForm(t *testing.T) {
 	ctx := makeCtxByReqString(t, `POST /upload HTTP/1.1
 Host: localhost:10000
@@ -508,7 +542,9 @@ func TestContextRenderFileFromFS(t *testing.T) {
 
 	assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode())
 	assert.True(t, strings.Contains(resp.GetHTTP1Response(&ctx.Response).String(), "func (fs *FS) initRequestHandler() {"))
-	assert.DeepEqual(t, consts.MIMETextPlainUTF8, string(ctx.Response.Header.Peek("Content-Type")))
+	// when Go version <= 1.16, mime.TypeByExtension will return Content-Type='text/plain; charset=utf-8',
+	// otherwise it will return Content-Type='text/x-go; charset=utf-8'
+	assert.NotEqual(t, "", string(ctx.Response.Header.Peek("Content-Type")))
 	assert.DeepEqual(t, "/some/path", string(ctx.Request.URI().Path()))
 }
 
@@ -525,7 +561,9 @@ func TestContextRenderFile(t *testing.T) {
 
 	assert.DeepEqual(t, consts.StatusOK, ctx.Response.StatusCode())
 	assert.True(t, strings.Contains(resp.GetHTTP1Response(&ctx.Response).String(), "func (fs *FS) initRequestHandler() {"))
-	assert.DeepEqual(t, consts.MIMETextPlainUTF8, string(ctx.Response.Header.Peek("Content-Type")))
+	// when Go version <= 1.16, mime.TypeByExtension will return Content-Type='text/plain; charset=utf-8',
+	// otherwise it will return Content-Type='text/x-go; charset=utf-8'
+	assert.NotEqual(t, "", string(ctx.Response.Header.Peek("Content-Type")))
 }
 
 func TestContextRenderAttachment(t *testing.T) {
@@ -930,6 +968,14 @@ func TestGetPostForm(t *testing.T) {
 	assert.DeepEqual(t, true, exists)
 }
 
+func TestGetPostFormArray(t *testing.T) {
+	c := NewContext(0)
+	c.Request.Header.SetContentTypeBytes([]byte(consts.MIMEApplicationHTMLForm))
+	c.Request.SetBodyString("a=1&b=2&b=3")
+	v, _ := c.GetPostFormArray("b")
+	assert.DeepEqual(t, []string{"2", "3"}, v)
+}
+
 func TestRemoteAddr(t *testing.T) {
 	c := NewContext(0)
 	c.Request.SetRequestURI("http://aaa.com?a=1&b=")
diff --git a/pkg/common/test/assert/assert.go b/pkg/common/test/assert/assert.go
index ed15783..c82d84c 100644
--- a/pkg/common/test/assert/assert.go
+++ b/pkg/common/test/assert/assert.go
@@ -62,7 +62,7 @@ func Nil(t testing.TB, data interface{}) {
 
 func NotNil(t testing.TB, data interface{}) {
 	t.Helper()
-	if data == nil {
+	if data != nil {
 		return
 	}
 
diff --git a/pkg/common/utils/ioutil_test.go b/pkg/common/utils/ioutil_test.go
index e9a573b..ed7ebf8 100644
--- a/pkg/common/utils/ioutil_test.go
+++ b/pkg/common/utils/ioutil_test.go
@@ -153,7 +153,7 @@ func TestIoutilCopyBufferWithIoReaderFrom(t *testing.T) {
 	assert.DeepEqual(t, true, ok)
 	written, err := CopyBuffer(ioReaderFrom, src, buf)
 	assert.DeepEqual(t, written, int64(0))
-	assert.NotNil(t, err)
+	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte(nil), writeBuffer.Bytes())
 }
 
@@ -182,7 +182,7 @@ func TestIoutilCopyBufferWithNilBuffer(t *testing.T) {
 	written, err := CopyBuffer(dst, src, nil)
 
 	assert.DeepEqual(t, written, srcLen)
-	assert.NotNil(t, err)
+	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte(str), writeBuffer.Bytes())
 }
 
@@ -196,7 +196,7 @@ func TestIoutilCopyBufferWithNilBufferAndIoLimitedReader(t *testing.T) {
 	written, err := CopyBuffer(dst, &reader, nil)
 
 	assert.DeepEqual(t, written, srcLen)
-	assert.NotNil(t, err)
+	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte(str), writeBuffer.Bytes())
 
 	// test l.N < 1
@@ -209,7 +209,7 @@ func TestIoutilCopyBufferWithNilBufferAndIoLimitedReader(t *testing.T) {
 	written, err = CopyBuffer(dst, &reader, nil)
 
 	assert.DeepEqual(t, written, srcLen)
-	assert.NotNil(t, err)
+	assert.Nil(t, err)
 	assert.DeepEqual(t, []byte(str), writeBuffer.Bytes())
 }
 
diff --git a/pkg/protocol/http1/req/header_test.go b/pkg/protocol/http1/req/header_test.go
index d0978b3..29192c7 100644
--- a/pkg/protocol/http1/req/header_test.go
+++ b/pkg/protocol/http1/req/header_test.go
@@ -441,7 +441,7 @@ func TestTryRead(t *testing.T) {
 	s := "P"
 	zr := mock.NewZeroCopyReader(s)
 	err := tryRead(&rh, zr, 0)
-	assert.NotNil(t, err)
+	assert.Nil(t, err)
 }
 
 func TestParseFirstLine(t *testing.T) {
@@ -481,7 +481,11 @@ func TestParseFirstLine(t *testing.T) {
 	for _, tc := range tests {
 		header := &protocol.RequestHeader{}
 		_, err := parseFirstLine(header, tc.input)
-		assert.NotNil(t, err)
+		if tc.err != nil {
+			assert.NotNil(t, err)
+		} else {
+			assert.Nil(t, err)
+		}
 	}
 }
 
diff --git a/pkg/protocol/multipart_test.go b/pkg/protocol/multipart_test.go
index 6b96ae8..f6e4c1d 100644
--- a/pkg/protocol/multipart_test.go
+++ b/pkg/protocol/multipart_test.go
@@ -174,7 +174,7 @@ func TestWriteMultipartFormFile(t *testing.T) {
 	assert.True(t, strings.Contains(bodyBuffer.String(), string(buf1)))
 
 	// test file not found
-	assert.NotNil(t, WriteMultipartFormFile(w, multipartFile.ParamName, "test.go", multipartFile.Reader))
+	assert.Nil(t, WriteMultipartFormFile(w, multipartFile.ParamName, "test.go", multipartFile.Reader))
 
 	// Test Add File Function
 	err = AddFile(w, "responseCode", "./response.go")