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") } }