270 lines
7.4 KiB
Go
270 lines
7.4 KiB
Go
package hw3test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestResponseEndChecksWireBytes(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
response string
|
|
valid bool
|
|
}{
|
|
{"valid body", "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndata", true},
|
|
{"extra body bytes", "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndataextra", false},
|
|
{"valid empty body", "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", true},
|
|
{"body after zero length", "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\nextra", false},
|
|
{"valid no content", "HTTP/1.1 204 No Content\r\n\r\n", true},
|
|
{"body after no content", "HTTP/1.1 204 No Content\r\n\r\nextra", false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go func() {
|
|
server, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer server.Close()
|
|
_, _ = io.WriteString(server, tc.response)
|
|
}()
|
|
client, err := net.Dial("tcp", listener.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
reader := bufio.NewReader(client)
|
|
response, err := http.ReadResponse(reader, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer response.Body.Close()
|
|
if _, err := io.Copy(io.Discard, response.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := checkResponseEnd(reader, false); (err == nil) != tc.valid {
|
|
t.Fatalf("valid=%v, checkResponseEnd error: %v", tc.valid, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseContentTypeRequiresSubtype(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
value string
|
|
valid bool
|
|
}{
|
|
{"application/octet-stream", true},
|
|
{"text/plain; charset=utf-8", true},
|
|
{"nonsense", false},
|
|
{"text/", false},
|
|
{"", false},
|
|
} {
|
|
_, err := parseContentType(tc.value)
|
|
if (err == nil) != tc.valid {
|
|
t.Errorf("content type %q: valid=%v, error=%v", tc.value, tc.valid, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunQueryAcceptsEarlyErrorResponse(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go func() {
|
|
server, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer server.Close()
|
|
reader := bufio.NewReader(server)
|
|
for {
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil || line == "\r\n" {
|
|
break
|
|
}
|
|
}
|
|
_, _ = io.WriteString(server, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 5\r\n\r\nerror")
|
|
}()
|
|
t.Setenv("SOLUTION_HOST", "127.0.0.1")
|
|
RunQuery(NewTestContext(t), &Env{}, t.TempDir(), RunOpts{
|
|
Port: listener.Addr().(*net.TCPAddr).Port,
|
|
ServerDomain: "localhost",
|
|
}, Query{
|
|
Method: "PUT",
|
|
Path: "item",
|
|
HostHeader: "other.example",
|
|
FileContent: &EnvFile{GenSeed: 42, Size: 8 * 1024 * 1024},
|
|
})
|
|
}
|
|
|
|
func TestRunQueryWaitsForLargeRequestBeforeCloseTimeout(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
server, err := listener.Accept()
|
|
if err != nil {
|
|
done <- err
|
|
return
|
|
}
|
|
defer server.Close()
|
|
request, err := http.ReadRequest(bufio.NewReader(server))
|
|
if err != nil {
|
|
done <- err
|
|
return
|
|
}
|
|
if _, err := io.WriteString(server, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 5\r\n\r\nerror"); err != nil {
|
|
done <- err
|
|
return
|
|
}
|
|
time.Sleep(2500 * time.Millisecond)
|
|
_, err = io.Copy(io.Discard, request.Body)
|
|
done <- err
|
|
}()
|
|
t.Setenv("SOLUTION_HOST", "127.0.0.1")
|
|
RunQuery(NewTestContext(t), &Env{}, t.TempDir(), RunOpts{
|
|
Port: listener.Addr().(*net.TCPAddr).Port,
|
|
ServerDomain: "localhost",
|
|
}, Query{
|
|
Method: "POST",
|
|
Path: "item",
|
|
HostHeader: "other.example",
|
|
FileContent: &EnvFile{GenSeed: 42, Size: 160 << 20},
|
|
})
|
|
if err := <-done; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestIdleDeadlineWriter(t *testing.T) {
|
|
t.Run("stalled write", func(t *testing.T) {
|
|
client, server := net.Pipe()
|
|
defer client.Close()
|
|
defer server.Close()
|
|
writer := &idleDeadlineWriter{Conn: client, Timeout: 50 * time.Millisecond}
|
|
_, err := writer.Write([]byte("x"))
|
|
var netErr net.Error
|
|
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
|
t.Fatalf("expected write timeout, got %v", err)
|
|
}
|
|
})
|
|
t.Run("request body timeout", func(t *testing.T) {
|
|
client, server := net.Pipe()
|
|
defer client.Close()
|
|
defer server.Close()
|
|
release := make(chan struct{})
|
|
defer close(release)
|
|
go func() {
|
|
_, _ = http.ReadRequest(bufio.NewReader(server))
|
|
<-release
|
|
}()
|
|
writer := &idleDeadlineWriter{Conn: client, Timeout: 50 * time.Millisecond}
|
|
request := (&Query{Method: "POST", FileContent: &EnvFile{GenSeed: 42, Size: 1 << 20}}).
|
|
CreateRequest(NewTestContext(t), "http://localhost/item")
|
|
if err := request.Write(writer); err == nil {
|
|
t.Fatal("expected request write to time out")
|
|
}
|
|
var netErr net.Error
|
|
if !errors.As(writer.lastError, &netErr) || !netErr.Timeout() {
|
|
t.Fatalf("expected underlying write timeout, got %v", writer.lastError)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompressedGetResponseValidation(t *testing.T) {
|
|
file := &EnvFile{GenSeed: 42, Size: 1024}
|
|
directory := &EnvDir{Listing: map[string]EnvNode{"item": file}}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
body io.Reader
|
|
validate func(*TC, *http.Response)
|
|
}{
|
|
{
|
|
name: "file",
|
|
body: file.Open(),
|
|
validate: func(t *TC, resp *http.Response) {
|
|
GetFileAction{File: file, Compression: true, VerifyHeaders: true}.VerifyResponse(t, nil, resp)
|
|
},
|
|
},
|
|
{
|
|
name: "directory",
|
|
body: bytes.NewBufferString("item\n"),
|
|
validate: func(t *TC, resp *http.Response) {
|
|
GetDirAction{Dir: directory, Compression: true, VerifyHeaders: true}.VerifyResponse(t, nil, resp)
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(tt *testing.T) {
|
|
testContext := NewTestContext(tt)
|
|
var encoded bytes.Buffer
|
|
writer := gzip.NewWriter(&encoded)
|
|
_, err := io.Copy(writer, tc.body)
|
|
if err != nil {
|
|
tt.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
tt.Fatal(err)
|
|
}
|
|
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
ContentLength: int64(encoded.Len()),
|
|
Header: http.Header{
|
|
"Content-Length": {strconv.Itoa(encoded.Len())},
|
|
"Content-Encoding": {"gzip"},
|
|
"Content-Type": {"text/plain"},
|
|
"Server": {"test-server"},
|
|
"Connection": {"close"},
|
|
},
|
|
Body: io.NopCloser(bytes.NewReader(encoded.Bytes())),
|
|
}
|
|
defer resp.Body.Close()
|
|
q := Query{Gzip: true, VerifyHeaders: true}
|
|
req := q.CreateRequest(testContext, "http://localhost/item")
|
|
if got := req.Header.Get("Accept-Encoding"); got != "gzip" {
|
|
tt.Fatalf("expected Accept-Encoding: gzip, got %q", got)
|
|
}
|
|
q.CommonValidate(testContext, req, resp)
|
|
tc.validate(testContext, resp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareFileContentBlocks(t *testing.T) {
|
|
file := &EnvFile{GenSeed: 17, Size: 64*1024 + 17}
|
|
context := NewTestContext(t)
|
|
if err := CompareFileContent(context, file.Open(), file); err != nil {
|
|
t.Fatalf("valid file was rejected: %v", err)
|
|
}
|
|
if err := CompareFileContent(context, io.LimitReader(file.Open(), file.Size-1), file); err == nil {
|
|
t.Fatal("truncated file was accepted")
|
|
}
|
|
content, err := io.ReadAll(file.Open())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content[len(content)-1] ^= 1
|
|
if err := CompareFileContent(context, bytes.NewReader(content), file); err == nil {
|
|
t.Fatal("changed final byte was accepted")
|
|
}
|
|
}
|