feat(testing): add comprehensive test coverage and code quality improvements #76
@@ -1,15 +1,13 @@
|
|||||||
package header
|
package header
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewRequestFromBytes(t *testing.T) {
|
func TestNewRequest(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
data []byte
|
data []byte
|
||||||
@@ -93,94 +91,6 @@ func TestNewRequestFromBytes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRequestFromReader(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
expectErr bool
|
|
||||||
errContains string
|
|
||||||
expectEOF bool
|
|
||||||
expectMethod string
|
|
||||||
expectPath string
|
|
||||||
expectVersion string
|
|
||||||
expectHeaders map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success",
|
|
||||||
data: []byte("POST /api HTTP/1.1\r\nContent-Type: application/json\r\n\r\n"),
|
|
||||||
expectErr: false,
|
|
||||||
expectMethod: "POST",
|
|
||||||
expectPath: "/api",
|
|
||||||
expectVersion: "HTTP/1.1",
|
|
||||||
expectHeaders: map[string]string{
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "read error on start line",
|
|
||||||
data: []byte{},
|
|
||||||
expectErr: true,
|
|
||||||
expectEOF: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid start line",
|
|
||||||
data: []byte("INVALID\n\n"),
|
|
||||||
expectErr: true,
|
|
||||||
errContains: "invalid start line",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "read error on headers",
|
|
||||||
data: []byte("GET / HTTP/1.1\nHost: example.com"),
|
|
||||||
expectErr: true,
|
|
||||||
expectEOF: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple colons in header",
|
|
||||||
data: []byte("GET / HTTP/1.1\r\nX-Custom: value:with:colons\r\n\r\n"),
|
|
||||||
expectErr: false,
|
|
||||||
expectMethod: "GET",
|
|
||||||
expectPath: "/",
|
|
||||||
expectVersion: "HTTP/1.1",
|
|
||||||
expectHeaders: map[string]string{
|
|
||||||
"X-Custom": "value:with:colons",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
br := bufio.NewReader(bytes.NewReader(tt.data))
|
|
||||||
req, err := NewRequest(br)
|
|
||||||
if tt.expectErr {
|
|
||||||
assert.Error(t, err)
|
|
||||||
if tt.expectEOF {
|
|
||||||
assert.Equal(t, io.EOF, err)
|
|
||||||
}
|
|
||||||
if tt.errContains != "" {
|
|
||||||
assert.Contains(t, err.Error(), tt.errContains)
|
|
||||||
}
|
|
||||||
assert.Nil(t, req)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, req)
|
|
||||||
assert.Equal(t, tt.expectMethod, req.Method())
|
|
||||||
assert.Equal(t, tt.expectPath, req.Path())
|
|
||||||
assert.Equal(t, tt.expectVersion, req.Version())
|
|
||||||
for k, v := range tt.expectHeaders {
|
|
||||||
assert.Equal(t, v, req.Value(k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewRequestUnsupportedType(t *testing.T) {
|
|
||||||
req, err := NewRequest(123)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "unsupported type: int")
|
|
||||||
assert.Nil(t, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequestHeaderMethods(t *testing.T) {
|
func TestRequestHeaderMethods(t *testing.T) {
|
||||||
data := []byte("GET / HTTP/1.1\r\nHost: original\r\n\r\n")
|
data := []byte("GET / HTTP/1.1\r\nHost: original\r\n\r\n")
|
||||||
req, _ := NewRequest(data)
|
req, _ := NewRequest(data)
|
||||||
@@ -315,30 +225,3 @@ func TestSetRemainingHeaders(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseHeadersFromReaderEdgeCases(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
expectHeaders map[string]string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "malformed header line",
|
|
||||||
data: []byte("GET / HTTP/1.1\r\nMalformedLine\r\nK1: V1\r\n\r\n"),
|
|
||||||
expectHeaders: map[string]string{
|
|
||||||
"K1": "V1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
br := bufio.NewReader(bytes.NewReader(tt.data))
|
|
||||||
req, err := parseHeadersFromReader(br)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for k, v := range tt.expectHeaders {
|
|
||||||
assert.Equal(t, v, req.Value(k))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package header
|
package header
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
@@ -36,31 +35,6 @@ func setRemainingHeaders(remaining []byte, header interface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHeadersFromBytes(headerData []byte) (RequestHeader, error) {
|
|
||||||
header := &requestHeader{
|
|
||||||
headers: make(map[string]string, 16),
|
|
||||||
}
|
|
||||||
|
|
||||||
lineEnd := bytes.Index(headerData, []byte("\r\n"))
|
|
||||||
if lineEnd == -1 {
|
|
||||||
return nil, fmt.Errorf("invalid request: no CRLF found in start line")
|
|
||||||
}
|
|
||||||
|
|
||||||
startLine := headerData[:lineEnd]
|
|
||||||
header.startLine = startLine
|
|
||||||
var err error
|
|
||||||
header.method, header.path, header.version, err = parseStartLine(startLine)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining := headerData[lineEnd+2:]
|
|
||||||
|
|
||||||
setRemainingHeaders(remaining, header)
|
|
||||||
|
|
||||||
return header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseStartLine(startLine []byte) (method, path, version string, err error) {
|
func parseStartLine(startLine []byte) (method, path, version string, err error) {
|
||||||
firstSpace := bytes.IndexByte(startLine, ' ')
|
firstSpace := bytes.IndexByte(startLine, ' ')
|
||||||
if firstSpace == -1 {
|
if firstSpace == -1 {
|
||||||
@@ -80,51 +54,6 @@ func parseStartLine(startLine []byte) (method, path, version string, err error)
|
|||||||
return method, path, version, nil
|
return method, path, version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHeadersFromReader(br *bufio.Reader) (RequestHeader, error) {
|
|
||||||
header := &requestHeader{
|
|
||||||
headers: make(map[string]string, 16),
|
|
||||||
}
|
|
||||||
|
|
||||||
startLineBytes, err := br.ReadSlice('\n')
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
startLineBytes = bytes.TrimRight(startLineBytes, "\r\n")
|
|
||||||
header.startLine = make([]byte, len(startLineBytes))
|
|
||||||
copy(header.startLine, startLineBytes)
|
|
||||||
|
|
||||||
header.method, header.path, header.version, err = parseStartLine(header.startLine)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
lineBytes, err := br.ReadSlice('\n')
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lineBytes = bytes.TrimRight(lineBytes, "\r\n")
|
|
||||||
|
|
||||||
if len(lineBytes) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
colonIdx := bytes.IndexByte(lineBytes, ':')
|
|
||||||
if colonIdx == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := bytes.TrimSpace(lineBytes[:colonIdx])
|
|
||||||
value := bytes.TrimSpace(lineBytes[colonIdx+1:])
|
|
||||||
|
|
||||||
header.headers[string(key)] = string(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func finalize(startLine []byte, headers map[string]string) []byte {
|
func finalize(startLine []byte, headers map[string]string) []byte {
|
||||||
size := len(startLine) + 2
|
size := len(startLine) + 2
|
||||||
for key, val := range headers {
|
for key, val := range headers {
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
package header
|
package header
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRequest(r interface{}) (RequestHeader, error) {
|
func NewRequest(headerData []byte) (RequestHeader, error) {
|
||||||
switch v := r.(type) {
|
header := &requestHeader{
|
||||||
case []byte:
|
headers: make(map[string]string, 16),
|
||||||
return parseHeadersFromBytes(v)
|
|
||||||
case *bufio.Reader:
|
|
||||||
return parseHeadersFromReader(v)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported type: %T", r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lineEnd := bytes.Index(headerData, []byte("\r\n"))
|
||||||
|
if lineEnd == -1 {
|
||||||
|
return nil, fmt.Errorf("invalid request: no CRLF found in start line")
|
||||||
|
}
|
||||||
|
|
||||||
|
startLine := headerData[:lineEnd]
|
||||||
|
header.startLine = startLine
|
||||||
|
var err error
|
||||||
|
header.method, header.path, header.version, err = parseStartLine(startLine)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := headerData[lineEnd+2:]
|
||||||
|
|
||||||
|
setRemainingHeaders(remaining, header)
|
||||||
|
|
||||||
|
return header, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *requestHeader) Value(key string) string {
|
func (req *requestHeader) Value(key string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user