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