package hw3test import ( "compress/gzip" "fmt" "github.com/stretchr/testify/require" "io" "mime" "net/http" "os" "path" "strings" ) func parseContentType(value string) (string, error) { mediaType, _, err := mime.ParseMediaType(value) if err != nil { return "", err } if !strings.Contains(mediaType, "/") { return "", fmt.Errorf("content type %q has no subtype", value) } return mediaType, nil } // Query represents a query to the solution server. type Query struct { // Seed is used as a query ID. Seed int64 // Method of a request, e.g. "GET", "POST", "PUT", "DELETE". Method string // Simple file path, splitted by slashes. E.g. "foo/bar/baz". Path string // If true, will use `Accept-Encoding` for GET query. Gzip bool // If true, will pass `Create-Directory: True` in POST query. CreateDirectory bool // If true, will pass `Remove-Directory: True` in DELETE query. RemoveDirectory bool // Pass the Host header. HostHeader string // Verify all server headers: `Content-Length`, `Content-Type`, `Server`. VerifyHeaders bool // FileContent when creating a file. FileContent *EnvFile } func (q *Query) CreateRequest(t *TC, queryURL string) *http.Request { var body io.Reader if q.FileContent != nil { body = q.FileContent.Open() if q.FileContent.Size == 0 { body = http.NoBody } } req, err := http.NewRequest(q.Method, queryURL, body) require.NoError(t, err, "failed to create go request") if q.FileContent != nil { req.ContentLength = q.FileContent.Size } if q.CreateDirectory { req.Header.Set("Create-Directory", "True") } if q.RemoveDirectory { req.Header.Set("Remove-Directory", "True") } if q.HostHeader != "" { req.Host = q.HostHeader } if q.Gzip { req.Header.Set("Accept-Encoding", "gzip") } return req } func (q *Query) CommonValidate(t *TC, r *http.Request, resp *http.Response) { require.NoError(t, validateResponseFraming(resp)) if q.VerifyHeaders { require.NotEmpty(t, resp.Header.Get("Server"), "missing Server header") } } type Action interface { // VerifyBefore verifies file content on disk before sending query to the server. VerifyBefore(t *TC, workdir string) // VerifyAfter verifies file content on disk after sending query to the server. VerifyAfter(t *TC, workdir string) // ApplyEnv applies the action to the environment. ApplyEnv(t *TC, env *Env) (changed bool) // VerifyResponse verifies the response from the server. VerifyResponse(t *TC, r *http.Request, resp *http.Response) } // HttpErrorAction is an Action, which expect specific status code in response. type HttpErrorAction struct { Status int } func (h HttpErrorAction) VerifyBefore(t *TC, workdir string) { return } func (h HttpErrorAction) VerifyAfter(t *TC, workdir string) { return } func (h HttpErrorAction) ApplyEnv(t *TC, env *Env) bool { return false } func (h HttpErrorAction) VerifyResponse(t *TC, r *http.Request, resp *http.Response) { require.Equal(t, h.Status, resp.StatusCode, "expected http status code") require.Positive(t, resp.ContentLength, "error response must contain an explanation") } // GetFileAction is an Action, which returns file content in response. type GetFileAction struct { Path string File *EnvFile Compression bool VerifyHeaders bool } func (g GetFileAction) VerifyBefore(t *TC, workdir string) { RequireFileContent(t, workdir, g.Path, g.File) } func (g GetFileAction) VerifyAfter(t *TC, workdir string) { RequireFileContent(t, workdir, g.Path, g.File) } func (g GetFileAction) ApplyEnv(t *TC, env *Env) bool { return false } func (g GetFileAction) VerifyResponse(t *TC, r *http.Request, resp *http.Response) { require.Equal(t, 200, resp.StatusCode, "expected OK") var body io.Reader = resp.Body if g.Compression { require.Equal(t, "gzip", resp.Header.Get("Content-Encoding"), "expected gzip response") gz, err := gzip.NewReader(resp.Body) require.NoError(t, err, "expected valid gzip response") defer gz.Close() body = gz } else { require.Empty(t, resp.Header.Get("Content-Encoding"), "unexpected content encoding") require.Equal(t, g.File.Size, resp.ContentLength, "expected content length") } if g.VerifyHeaders && g.File.Size > 0 { _, err := parseContentType(resp.Header.Get("Content-Type")) require.NoError(t, err, "expected valid content type") } require.NoError(t, CompareFileContent(t, body, g.File), "file content mismatch") remaining, err := io.Copy(io.Discard, body) require.NoError(t, err, "failed to read remaining response body") require.Zero(t, remaining, "response contains extra file content") } // GetDirAction is an Action, which returns directory listing in response. type GetDirAction struct { Path string Dir *EnvDir Compression bool VerifyHeaders bool } func (g GetDirAction) VerifyBefore(t *TC, workdir string) { RequireDir(t, workdir, g.Path, g.Dir) } func (g GetDirAction) VerifyAfter(t *TC, workdir string) { RequireDir(t, workdir, g.Path, g.Dir) } func (g GetDirAction) ApplyEnv(t *TC, env *Env) bool { return false } func (g GetDirAction) VerifyResponse(t *TC, r *http.Request, resp *http.Response) { require.Equal(t, 200, resp.StatusCode, "expected OK") var body io.Reader = resp.Body if g.Compression { require.Equal(t, "gzip", resp.Header.Get("Content-Encoding"), "expected gzip response") gz, err := gzip.NewReader(resp.Body) require.NoError(t, err, "expected valid gzip response") defer gz.Close() body = gz } else { require.Empty(t, resp.Header.Get("Content-Encoding"), "unexpected content encoding") } content, err := io.ReadAll(body) require.NoError(t, err, "expected response to be read without errors") if g.VerifyHeaders && len(content) > 0 { mediaType, err := parseContentType(resp.Header.Get("Content-Type")) require.NoError(t, err, "expected valid content type") require.Contains(t, []string{"text/plain", "text/html"}, mediaType, "expected directory listing content type") } strlist := string(content) for name := range g.Dir.Listing { require.Contains(t, strlist, name, "expected dir listing to contain child %s", name) } } // CreateDirAction is an Action, which creates a directory. type CreateDirAction struct { Path string Name string Parent *EnvDir PathOnDisk string } func (c CreateDirAction) VerifyBefore(t *TC, workdir string) { RequireNotExists(t, workdir, c.Path) } func (c CreateDirAction) VerifyAfter(t *TC, workdir string) { RequireDir(t, workdir, c.Path, c.Parent) entries, err := os.ReadDir(c.PathOnDisk) require.NoError(t, err, "expected to read dir %s without errors", c.PathOnDisk) require.Empty(t, entries, "expected dir %s to be empty", c.PathOnDisk) } func (c CreateDirAction) ApplyEnv(t *TC, env *Env) bool { _, exist := c.Parent.CreateDir(c.Name) require.False(t, exist) return true } func (c CreateDirAction) VerifyResponse(t *TC, r *http.Request, resp *http.Response) { require.Contains(t, []int{http.StatusOK, http.StatusCreated}, resp.StatusCode, "expected successful creation") } // CreateFileAction is an Action, which creates a file. type CreateFileAction struct { Path string Name string Parent *EnvDir Content *EnvFile } func (c CreateFileAction) VerifyBefore(t *TC, workdir string) { RequireNotExists(t, workdir, c.Path) } func (c CreateFileAction) VerifyAfter(t *TC, workdir string) { RequireFileContent(t, workdir, c.Path, c.Content) } func (c CreateFileAction) ApplyEnv(t *TC, env *Env) bool { c.Parent.Listing[c.Name] = c.Content return true } func (c CreateFileAction) VerifyResponse(t *TC, r *http.Request, resp *http.Response) { require.Contains(t, []int{http.StatusOK, http.StatusCreated}, resp.StatusCode, "expected successful creation") } // ReplaceFileAction is an Action, which replaces file content. type ReplaceFileAction struct { Path string Name string Parent *EnvDir OldContent *EnvFile NewContent *EnvFile } func (r ReplaceFileAction) VerifyBefore(t *TC, workdir string) { RequireFileContent(t, workdir, r.Path, r.OldContent) } func (r ReplaceFileAction) VerifyAfter(t *TC, workdir string) { RequireFileContent(t, workdir, r.Path, r.NewContent) } func (r ReplaceFileAction) ApplyEnv(t *TC, env *Env) bool { r.Parent.Listing[r.Name] = r.NewContent return true } func (r ReplaceFileAction) VerifyResponse(t *TC, req *http.Request, resp *http.Response) { require.Contains(t, []int{http.StatusOK, http.StatusNoContent}, resp.StatusCode, "expected successful update") } // DeleteAction is an Action, which deletes a file or directory. type DeleteAction struct { Path string Parent *EnvDir Name string } func (d DeleteAction) VerifyBefore(t *TC, workdir string) { RequireExists(t, workdir, d.Path) } func (d DeleteAction) VerifyAfter(t *TC, workdir string) { RequireNotExists(t, workdir, d.Path) } func (d DeleteAction) ApplyEnv(t *TC, env *Env) bool { delete(d.Parent.Listing, d.Name) return true } func (d DeleteAction) VerifyResponse(t *TC, r *http.Request, resp *http.Response) { require.Equal(t, 200, resp.StatusCode, "expected OK") } // Action returns action for the query, or nil if query shouldn't do anything. func (q *Query) Action(env *Env, opts *RunOpts) Action { if !strings.EqualFold(q.HostHeader, opts.ServerDomain) { return HttpErrorAction{ Status: 400, } } parent, child := env.Lookup(q.Path) switch q.Method { case "GET": if child == nil { return HttpErrorAction{ Status: 404, } } if f, ok := child.(*EnvFile); ok { return GetFileAction{ Path: q.Path, File: f, Compression: q.Gzip, VerifyHeaders: q.VerifyHeaders, } } if d, ok := child.(*EnvDir); ok { return GetDirAction{ Path: q.Path, Dir: d, Compression: q.Gzip, VerifyHeaders: q.VerifyHeaders, } } // shouldn't get here return nil case "POST": if child != nil { return HttpErrorAction{ Status: 409, } } if parent == nil { return HttpErrorAction{Status: http.StatusNotFound} } _, name := path.Split(q.Path) if q.CreateDirectory { return CreateDirAction{ Path: q.Path, Name: name, Parent: parent, PathOnDisk: path.Join(opts.WorkingDirectory, q.Path), } } return CreateFileAction{ Path: q.Path, Name: name, Parent: parent, Content: q.FileContent, } case "PUT": if child == nil { return HttpErrorAction{Status: http.StatusNotFound} } f, ok := child.(*EnvFile) if !ok { return HttpErrorAction{ Status: 409, } } _, name := path.Split(q.Path) return ReplaceFileAction{ Path: q.Path, Name: name, Parent: parent, OldContent: f, NewContent: q.FileContent, } case "DELETE": if child == env.RootDir { return HttpErrorAction{Status: http.StatusForbidden} } if child == nil { return HttpErrorAction{Status: http.StatusNotFound} } _, ok := child.(*EnvDir) if ok && !q.RemoveDirectory { return HttpErrorAction{ Status: 406, } } _, name := path.Split(q.Path) return DeleteAction{ Path: q.Path, Parent: parent, Name: name, } } return nil }