Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions e2e/echoserver/imperative/assertions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package imperative

import (
"bytes"
"fmt"
"net/http"
)

func AssertRequest(req *http.Request, body []byte) error {
// parse imperative from request header
imp, err := ParseImperative(req.Header.Get(ImperativeHeader))
if err != nil {
return fmt.Errorf("assertRequest: parse imperative: %v", err)
}
if err := imp.Validate(); err != nil {
return fmt.Errorf("assertRequest: validate imperative: %v", err)
}

// check method
if req.Method != imp.Request.Method {
return fmt.Errorf("request method mismatch: expected %s, got %s", imp.Request.Method, req.Method)
}
// check path
if req.URL.Path != imp.Request.Path {
return fmt.Errorf("request path mismatch: expected %s, got %s", imp.Request.Path, req.URL.Path)
}
// check headers
for key, value := range imp.Request.Headers {
if req.Header.Get(key) != value {
return fmt.Errorf("request header mismatch: expected %s, got %s", value, req.Header.Get(key))
}
}
// calculate specified body
specifiedBody, err := imp.Request.Body.Build()
if err != nil {
return fmt.Errorf("assertRequest: build body: %v", err)
}
// compare against passed body
if !bytes.Equal(specifiedBody, body) {
return fmt.Errorf("request body mismatch: expected %d bytes, got %d bytes", len(specifiedBody), len(body))
}
// imperative ok, method ok, path ok, headers ok, body ok
return nil
}

func AssertResponse(resp *http.Response, body []byte) error {
// read imperative from response header
imp, err := ParseImperative(resp.Header.Get(ImperativeHeader))
if err != nil {
return fmt.Errorf("assertRequest: parse imperative: %v", err)
}
if err := imp.Validate(); err != nil {
return fmt.Errorf("assertResponse: validate imperative: %v", err)
}

// check status code
if resp.StatusCode != imp.Response.StatusCode {
return fmt.Errorf("response status code mismatch: expected %d, got %d", imp.Response.StatusCode, resp.StatusCode)
}
// check headers
for key, value := range imp.Response.Headers {
if resp.Header.Get(key) != value {
return fmt.Errorf("response header mismatch: expected %s, got %s", value, resp.Header.Get(key))
}
}
// return early if the imperative aborts the connection
if imp.Transport.Abort {
return nil
}
// calculate specified body
specifiedBody, err := imp.Response.Body.Build()
if err != nil {
return fmt.Errorf("assertResponse: build body: %v", err)
}
// compare against passed body
if !bytes.Equal(specifiedBody, body) {
return fmt.Errorf("response body mismatch: expected %d bytes, got %d bytes", len(specifiedBody), len(body))
}
// imperative ok, status code ok, headers ok, body ok
return nil
}
99 changes: 99 additions & 0 deletions e2e/echoserver/imperative/assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package imperative

import (
"io"
"testing"
)

func TestAssertRequest(t *testing.T) {
imp := Imperative{Response: Response{StatusCode: 200}, Request: Request{Method: "GET", Path: "/", Headers: map[string]string{"Content-Type": "application/json"}, Body: Body{Length: 1024, Encoding: EncodingUTF8, Seed: 42}}}
req, err := imp.BuildRequest("")
if err != nil {
t.Errorf("BuildRequest: %v", err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Errorf("ReadAll: %v", err)
}

if err := AssertRequest(req, body); err != nil {
t.Errorf("AssertRequest: %v", err)
}
}

func TestAssertRequest_InvalidBody(t *testing.T) {
imp := Imperative{Response: Response{StatusCode: 200}, Request: Request{Method: "GET", Path: "/", Headers: map[string]string{"Content-Type": "application/json"}, Body: Body{Length: 1024, Encoding: EncodingUTF8, Seed: 42}}}
req, err := imp.BuildRequest("")
if err != nil {
t.Errorf("BuildRequest: %v", err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Errorf("ReadAll: %v", err)
}
// flip a single bit in the body
body[512] ^= 1

if err := AssertRequest(req, body); err == nil {
t.Errorf("AssertRequest should have failed")
}
}

func TestAssertRequest_InvalidHeaders(t *testing.T) {
imp := Imperative{Response: Response{StatusCode: 200}, Request: Request{Method: "GET", Path: "/", Headers: map[string]string{"Content-Type": "application/json"}, Body: Body{Length: 1024, Encoding: EncodingUTF8, Seed: 42}}}
req, err := imp.BuildRequest("")
if err != nil {
t.Errorf("BuildRequest: %v", err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Errorf("ReadAll: %v", err)
}
// overwrite the content type
req.Header.Set("Content-Type", "application/octet-stream")

if err := AssertRequest(req, body); err == nil {
t.Errorf("AssertRequest should have failed")
}
}

func TestAssertRequest_InvalidPath(t *testing.T) {
imp := Imperative{Response: Response{StatusCode: 200}, Request: Request{Method: "GET", Path: "/", Headers: map[string]string{"Content-Type": "application/json"}, Body: Body{Length: 1024, Encoding: EncodingUTF8, Seed: 42}}}
req, err := imp.BuildRequest("")
if err != nil {
t.Errorf("BuildRequest: %v", err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Errorf("ReadAll: %v", err)
}
// overwrite the path
req.URL.Path = "/invalid"

if err := AssertRequest(req, body); err == nil {
t.Errorf("AssertRequest should have failed")
}
}

func TestAssertRequest_InvalidMethod(t *testing.T) {
imp := Imperative{Response: Response{StatusCode: 200}, Request: Request{Method: "POST", Path: "/", Headers: map[string]string{"Content-Type": "application/json"}, Body: Body{Length: 1024, Encoding: EncodingUTF8, Seed: 42}}}
req, err := imp.BuildRequest("")
if err != nil {
t.Errorf("BuildRequest: %v", err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Errorf("ReadAll: %v", err)
}
// overwrite the method
req.Method = "GET"

if err := AssertRequest(req, body); err == nil {
t.Errorf("AssertRequest should have failed")
}
}

// where are the tests for the response assertions?
// since it doesn't make sense to generate a http.Response from an Imperative
// because the http.Handler uses the http.ResponseWriter to write the response
// we test the response building logic in the ../server_test.go file :)
46 changes: 46 additions & 0 deletions e2e/echoserver/imperative/compression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package imperative

import (
"bytes"
"compress/gzip"
"compress/zlib"

"github.com/klauspost/compress/zstd"
)

// Compress compresses data using the named encoding.
func Compress(data []byte, encoding string) ([]byte, error) {
var buf bytes.Buffer
switch encoding {
case "gzip":
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
case "deflate":
w := zlib.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
case "zstd":
w, err := zstd.NewWriter(&buf)
if err != nil {
return nil, err
}
if _, err := w.Write(data); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
default:
return data, nil
}
return buf.Bytes(), nil
}
96 changes: 96 additions & 0 deletions e2e/echoserver/imperative/generation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package imperative

import (
"fmt"
"net/http"
"os"

"bytes"
)

func (bdy Body) Validate() error {
if bdy.Length != 0 && bdy.Filepath != "" {
return fmt.Errorf("imperative: body length and filepath cannot be set at the same time. Pick one or the other.")
}
if bdy.Encoding != "" && bdy.Encoding != EncodingUTF8 && bdy.Encoding != EncodingBinary {
return fmt.Errorf("imperative: invalid value for encoding, must be either empty, %s or %s", EncodingUTF8, EncodingBinary)
}
if bdy.Length > 0 && bdy.Seed == 0 {
return fmt.Errorf("imperative: set the seed explicitly to prevent duplicate payload digests")
}
return nil
}

// Builds a body from specification
// If neither Filepath nor Length is set, returns empty []byte, nil
func (bdy Body) Build() ([]byte, error) {
var body []byte
var err error

if bdy.Filepath != "" {
// should be page cached by kernel
body, err = os.ReadFile(bdy.Filepath)
if err != nil {
return nil, err
}
} else if bdy.Length > 0 {
if bdy.Encoding == EncodingBinary {
body = binaryBody(bdy.Length, bdy.Seed)
} else {
body = stringBody(bdy.Length, bdy.Seed)
}
}

if bdy.Compress != "" && len(body) > 0 {
compressed, err := Compress(body, bdy.Compress)
if err != nil {
return nil, fmt.Errorf("compress: %w", err)
}
body = compressed
}

return body, nil
}

// Returns deterministic binary slice
func binaryBody(n int, seed int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte((i + seed) % 256)
}
return b
}

// Returns deterministic string as []byte
func stringBody(n int, seed int) []byte {
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, n)
for i := range b {
b[i] = alphabet[(i+seed)%len(alphabet)]
}
return b
}

// Returns a http.Request from an Imperative
func (imp Imperative) BuildRequest(baseURL string) (*http.Request, error) {
if err := imp.Validate(); err != nil {
return nil, err
}
body, err := imp.Request.Body.Build()
if err != nil {
return nil, err
}
req, err := http.NewRequest(imp.Request.Method, fmt.Sprintf("%s%s", baseURL, imp.Request.Path), bytes.NewReader(body))
if err != nil {
return nil, err
}
// set headers
for key, value := range imp.Request.Headers {
req.Header.Set(key, value)
}
// set imperative header
req.Header.Set(ImperativeHeader, imp.Encode())

// imp validated, body built, headers set
return req, nil
}
Loading