145 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|