425 lines
11 KiB
Go
425 lines
11 KiB
Go
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
|
|
}
|