Files
hse-2026/homework/03-http-server/tests/contract_test.go
T
2026-09-24 21:30:37 +03:00

145 lines
4.7 KiB
Go

package hw3test
import (
"bufio"
"errors"
"io"
"net"
"os"
"path/filepath"
"strings"
"testing"
)
func TestContractResponseRejectsBadFraming(t *testing.T) {
for _, tc := range []struct {
name, response string
valid bool
}{
{"ok", "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 4\r\n\r\ndata", true},
{"short", "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 5\r\n\r\ndata", false},
{"long", "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 3\r\n\r\ndata", false},
{"no-close", "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", false},
{"no-length", "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", false},
{"204", "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n", true},
{"204-length", "HTTP/1.1 204 No Content\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", false},
{"204-body", "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\nx", false},
{"chunked", "HTTP/1.1 200 OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n", false},
} {
t.Run(tc.name, func(t *testing.T) {
_, _, err := readContractResponse(bufio.NewReader(strings.NewReader(tc.response)), false)
if (err == nil) != tc.valid {
t.Fatalf("valid=%v, error=%v", tc.valid, err)
}
})
}
}
type resetReader struct{}
func (resetReader) Read([]byte) (int, error) {
return 0, &net.OpError{Op: "read", Net: "tcp", Err: errors.New("connection reset")}
}
func TestContractResponseAfterEarlyRejection(t *testing.T) {
for _, tc := range []struct {
name, body string
requestHasBody bool
valid bool
}{
{"complete error then reset", "error", true, true},
{"reset without request body", "error", false, false},
{"truncated error", "err", true, false},
{"extra response byte", "error!", true, false},
} {
t.Run(tc.name, func(t *testing.T) {
wire := "HTTP/1.1 409 Conflict\r\nConnection: close\r\nContent-Length: 5\r\n\r\n" + tc.body
reader := bufio.NewReader(io.MultiReader(strings.NewReader(wire), resetReader{}))
_, _, err := readContractResponse(reader, tc.requestHasBody)
if (err == nil) != tc.valid {
t.Fatalf("valid=%v, error=%v", tc.valid, err)
}
})
}
}
func TestSnapshotDetectsErrorSideEffects(t *testing.T) {
for _, mutation := range []string{"overwrite", "create", "delete", "mkdir"} {
t.Run(mutation, func(t *testing.T) {
root := t.TempDir()
file := filepath.Join(root, "file")
if err := os.WriteFile(file, []byte("before"), 0644); err != nil {
t.Fatal(err)
}
before, err := snapshotTree(root)
if err != nil {
t.Fatal(err)
}
after, err := snapshotTree(root)
if err != nil {
t.Fatal(err)
}
if err := compareSnapshots(before, after); err != nil {
t.Fatal(err)
}
switch mutation {
case "overwrite":
err = os.WriteFile(file, []byte("after!"), 0644)
case "create":
err = os.WriteFile(filepath.Join(root, "new"), nil, 0644)
case "delete":
err = os.Remove(file)
case "mkdir":
err = os.Mkdir(filepath.Join(root, "new"), 0755)
}
if err != nil {
t.Fatal(err)
}
after, err = snapshotTree(root)
if err != nil {
t.Fatal(err)
}
if err := compareSnapshots(before, after); err == nil {
t.Fatal("side effect was accepted")
}
})
}
}
func TestMissingPathActionsAndRoot(t *testing.T) {
env := &Env{RootDir: &EnvDir{Listing: map[string]EnvNode{"file": &EnvFile{}, "dir": &EnvDir{Listing: map[string]EnvNode{}}}}}
opts := &RunOpts{ServerDomain: "localhost"}
for _, tc := range []struct {
method, path string
status int
}{
{"POST", "missing/new", 404}, {"POST", "file/new", 404},
{"POST", "dir", 409}, {"PUT", "missing", 404}, {"PUT", "dir", 409}, {"DELETE", "missing", 404},
{"POST", "/", 409}, {"PUT", "/", 409}, {"DELETE", "/", 403},
} {
q := Query{Method: tc.method, Path: tc.path, HostHeader: "LOCALHOST"}
action, ok := q.Action(env, opts).(HttpErrorAction)
if !ok || action.Status != tc.status {
t.Errorf("%s %s: got %#v", tc.method, tc.path, q.Action(env, opts))
}
}
q := Query{Method: "GET", Path: "", HostHeader: "LOCALHOST"}
if _, ok := q.Action(env, opts).(GetDirAction); !ok {
t.Fatal("root GET is not a listing")
}
for _, root := range []string{"", "/"} {
for _, remove := range []bool{false, true} {
q := Query{Method: "DELETE", Path: root, HostHeader: "localhost", RemoveDirectory: remove}
action, ok := q.Action(env, opts).(HttpErrorAction)
if !ok || action.Status != 403 {
t.Fatalf("root DELETE with RemoveDirectory=%v: got %#v", remove, action)
}
q.HostHeader = "wrong.example"
action, ok = q.Action(env, opts).(HttpErrorAction)
if !ok || action.Status != 400 {
t.Fatalf("Host must be checked before root DELETE: got %#v", action)
}
}
}
}