mirror of
https://github.com/versity/versitygw.git
synced 2026-03-27 18:05:00 +00:00
Merge pull request #1841 from versity/test/templates
test: test for not-implemented content-type, add first template test
This commit is contained in:
1
.github/workflows/system.yml
vendored
1
.github/workflows/system.yml
vendored
@@ -105,6 +105,7 @@ jobs:
|
||||
USER_AUTOGENERATION_PREFIX: github-actions-test-
|
||||
AWS_REGION: ${{ matrix.AWS_REGION }}
|
||||
COVERAGE_LOG: coverage.log
|
||||
TEMPLATE_MATRIX_FILE: ${{ github.workspace }}/tests/templates/matrix.yaml
|
||||
run: |
|
||||
make testbin
|
||||
export AWS_ACCESS_KEY_ID=ABCDEFGHIJKLMNOPQRST
|
||||
|
||||
368
tests/checker/main.go
Normal file
368
tests/checker/main.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const TemplateIdDefault = "default"
|
||||
|
||||
var dataFile *string
|
||||
var batsTestFileName *string
|
||||
var batsTestName *string
|
||||
var templateId *string
|
||||
var serverName *string
|
||||
var matrixFile *string
|
||||
|
||||
type Templates map[string]string
|
||||
|
||||
type BatsTest map[string]Templates
|
||||
|
||||
type BatsFile map[string]BatsTest
|
||||
|
||||
type BatsFiles map[string]BatsFile
|
||||
|
||||
type S3ErrorTemplate struct {
|
||||
Error ErrorInner `yaml:"Error"`
|
||||
}
|
||||
|
||||
type S3ErrorXML struct {
|
||||
XMLName xml.Name `xml:"Error"`
|
||||
ErrorInner
|
||||
}
|
||||
|
||||
type TemplateBody struct {
|
||||
Type string `yaml:"type"`
|
||||
Data yaml.Node `yaml:"data"`
|
||||
}
|
||||
|
||||
type ExpectTemplate struct {
|
||||
Status int `yaml:"status"`
|
||||
Headers map[string]string `yaml:"headers"`
|
||||
Body TemplateBody `yaml:"body"`
|
||||
}
|
||||
|
||||
type ErrorInner struct {
|
||||
Code string `yaml:"Code" xml:"Code"`
|
||||
Message string `yaml:"Message" xml:"Message"`
|
||||
}
|
||||
|
||||
type ListAnalyticsConfigurationsResultTemplate struct {
|
||||
IsTruncated bool `yaml:"IsTruncated"`
|
||||
}
|
||||
|
||||
type ListAnalyticsConfigurationsResultXML struct {
|
||||
XMLName xml.Name `xml:"ListBucketAnalyticsConfigurationsResult"`
|
||||
IsTruncated bool `xml:"IsTruncated"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := checkFlags(); err != nil {
|
||||
fmt.Println("Error checking command flags", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 1) Read the YAML template file
|
||||
b, err := loadTemplate()
|
||||
if err != nil {
|
||||
fmt.Println("Error loading template", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
expected, decodedBody, err := loadExpectedValues(b)
|
||||
if err != nil {
|
||||
fmt.Println("Error converting template data into struct", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("type of decoded body: %v\n", reflect.TypeOf(decodedBody))
|
||||
|
||||
actual, err := loadResponseFromFile(*dataFile, reflect.TypeOf(decodedBody))
|
||||
if err != nil {
|
||||
fmt.Printf("error loading response from %s: %v\n", *dataFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = compare(expected, decodedBody, actual); err != nil {
|
||||
fmt.Printf("comparison error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func checkFlags() error {
|
||||
dataFile = flag.String("dataFile", "", "String containing CURL or OPENSSL response")
|
||||
batsTestFileName = flag.String("batsTestFileName", "", "Name of bats test file")
|
||||
batsTestName = flag.String("batsTestName", "", "Name of bats test")
|
||||
templateId = flag.String("templateId", "", "Specific ID within test, if any")
|
||||
serverName = flag.String("serverName", "", "Name of S3 gateway server being tested against")
|
||||
matrixFile = flag.String("matrixFile", "", "File which maps test calls to templates")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *dataFile == "" {
|
||||
return errors.New("'dataFile' parameter cannot be blank")
|
||||
}
|
||||
if *batsTestFileName == "" {
|
||||
return errors.New("'batsTestFileName' parameter cannot be blank")
|
||||
}
|
||||
if *batsTestName == "" {
|
||||
return errors.New("'batsTestName' parameter cannot be blank")
|
||||
}
|
||||
if *serverName == "" {
|
||||
return errors.New("'serverName' parameter cannot be blank")
|
||||
}
|
||||
if *matrixFile == "" {
|
||||
return errors.New("'matrixFile' parameter cannot be blank")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadTemplate() ([]byte, error) {
|
||||
matrixFileData, err := os.ReadFile(*matrixFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading matrix file: %w", err)
|
||||
}
|
||||
|
||||
batsFiles := &BatsFiles{}
|
||||
if err = yaml.Unmarshal([]byte(matrixFileData), batsFiles); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling matrix YAML data: %w", err)
|
||||
}
|
||||
|
||||
batsTestFile := filepath.Base(*batsTestFileName)
|
||||
batsFileYaml, ok := (*batsFiles)[filepath.Base(*batsTestFileName)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot find bats file name of '%s' in matrix", batsTestFile)
|
||||
}
|
||||
batsTestYaml, ok := batsFileYaml[*batsTestName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot find bats test name of '%s' in matrix under file name '%s'", *batsTestName, batsTestFile)
|
||||
}
|
||||
testTemplateId := getTestTemplateId()
|
||||
batsCallIdYaml, ok := batsTestYaml[testTemplateId]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot find bats template ID of '%s' in matrix under file name '%s', test name '%s'",
|
||||
testTemplateId, batsTestFile, *batsTestName)
|
||||
}
|
||||
templateName, ok := batsCallIdYaml[*serverName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot find bats server name of '%s' in matrix under file name '%s', test name '%s', template ID '%s'",
|
||||
*serverName, batsTestFile, *batsTestName, testTemplateId)
|
||||
}
|
||||
|
||||
templateFileBytes, err := os.ReadFile("tests/templates/" + *serverName + "/" + templateName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading template file bytes: %w", err)
|
||||
}
|
||||
return templateFileBytes, nil
|
||||
}
|
||||
|
||||
func getTestTemplateId() string {
|
||||
if *templateId != "" {
|
||||
return *templateId
|
||||
}
|
||||
return TemplateIdDefault
|
||||
}
|
||||
|
||||
func loadExpectedValues(b []byte) (*ExpectTemplate, any, error) {
|
||||
var exp ExpectTemplate
|
||||
if err := yaml.Unmarshal(b, &exp); err != nil {
|
||||
return nil, nil, fmt.Errorf("error unmarshaling template YAML: %w", err)
|
||||
}
|
||||
|
||||
decoded, err := exp.Body.Decode()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error decoding data part of template: %w", err)
|
||||
}
|
||||
return &exp, decoded, nil
|
||||
}
|
||||
|
||||
func (b TemplateBody) Decode() (any, error) {
|
||||
switch b.Type {
|
||||
case "s3_error":
|
||||
var t S3ErrorTemplate
|
||||
if err := b.Data.Decode(&t); err != nil {
|
||||
return nil, fmt.Errorf("decode s3_error body: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
case "list_bucket_analytics_configuration_result":
|
||||
var t ListAnalyticsConfigurationsResultTemplate
|
||||
if err := b.Data.Decode(&t); err != nil {
|
||||
return nil, fmt.Errorf("decode s3_error body: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
|
||||
// add more kinds here:
|
||||
// case "list_bucket_result": ...
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown body kind: %q", b.Type)
|
||||
}
|
||||
}
|
||||
|
||||
type Actual struct {
|
||||
Status int
|
||||
Headers map[string][]string // multi-value headers
|
||||
Body any
|
||||
}
|
||||
|
||||
func loadResponseFromFile(fileName string, bodyType reflect.Type) (*Actual, error) {
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var headerPart, bodyPart []byte
|
||||
|
||||
if bytes.Contains(data, []byte("\r\n\r\n")) {
|
||||
parts := bytes.SplitN(data, []byte("\r\n\r\n"), 2)
|
||||
headerPart = parts[0]
|
||||
bodyPart = parts[1]
|
||||
} else {
|
||||
parts := bytes.SplitN(data, []byte("\n\n"), 2)
|
||||
headerPart = parts[0]
|
||||
bodyPart = parts[1]
|
||||
}
|
||||
|
||||
lines := bytes.Split(headerPart, []byte("\n"))
|
||||
statusLine := strings.TrimSpace(string(lines[0]))
|
||||
|
||||
return readActualFromStrings(statusLine, lines, bodyPart, bodyType)
|
||||
}
|
||||
|
||||
func readActualFromStrings(statusData string, headerData [][]byte, body []byte, bodyType reflect.Type) (*Actual, error) {
|
||||
var status int
|
||||
var err error
|
||||
statusLineValues := strings.Split(statusData, " ")
|
||||
if len(statusLineValues) > 1 {
|
||||
if status, err = strconv.Atoi(statusLineValues[1]); err != nil {
|
||||
return nil, fmt.Errorf("error converting status value '%s' to integer: %w", statusLineValues[1], err)
|
||||
}
|
||||
}
|
||||
|
||||
headers := addHeadersToMap(headerData)
|
||||
|
||||
var actualBody any
|
||||
switch bodyType.String() {
|
||||
case "main.S3ErrorTemplate":
|
||||
actualBody = &S3ErrorXML{}
|
||||
case "main.ListAnalyticsConfigurationsResultTemplate":
|
||||
actualBody = &ListAnalyticsConfigurationsResultXML{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled body type: %s", bodyType.String())
|
||||
}
|
||||
|
||||
switch ty := actualBody.(type) {
|
||||
case *S3ErrorXML, *ListAnalyticsConfigurationsResultXML:
|
||||
if err = xml.Unmarshal(body, ty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported actual body type: %T", actualBody)
|
||||
}
|
||||
|
||||
return &Actual{Status: status, Headers: headers, Body: actualBody}, nil
|
||||
}
|
||||
|
||||
func addHeadersToMap(headerData [][]byte) map[string][]string {
|
||||
headers := map[string][]string{}
|
||||
for _, header := range headerData {
|
||||
line := strings.TrimRight(string(header), "\r\n")
|
||||
if line == "" {
|
||||
break // end of headers
|
||||
}
|
||||
// Skip status line like: HTTP/1.1 501 Not Implemented
|
||||
if strings.HasPrefix(strings.ToUpper(line), "HTTP/") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(k))
|
||||
val := strings.TrimSpace(v)
|
||||
headers[key] = append(headers[key], val)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func compare(expected *ExpectTemplate, expectedBody any, actual *Actual) error {
|
||||
if expected.Status != actual.Status {
|
||||
return fmt.Errorf("status: expected %d, got %d", expected.Status, actual.Status)
|
||||
}
|
||||
|
||||
if err := headerCompare(expected.Headers, actual.Headers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if expectedError, ok := expectedBody.(S3ErrorTemplate); ok {
|
||||
actualError, ok := actual.Body.(*S3ErrorXML)
|
||||
if !ok {
|
||||
return fmt.Errorf("error casting actual response body to S3 error xml")
|
||||
}
|
||||
if actualError.Code != expectedError.Error.Code {
|
||||
return fmt.Errorf("expected error code of '%s', was '%s'", expectedError.Error.Code, actualError.Code)
|
||||
}
|
||||
if actualError.Message != expectedError.Error.Message {
|
||||
return fmt.Errorf("expected error message of '%s', was '%s'", expectedError.Error.Message, actualError.Message)
|
||||
}
|
||||
fmt.Println("comparison success")
|
||||
} else if expectedError, ok := expectedBody.(ListAnalyticsConfigurationsResultTemplate); ok {
|
||||
actualError, ok := actual.Body.(*ListAnalyticsConfigurationsResultXML)
|
||||
if !ok {
|
||||
return fmt.Errorf("error casting actual response body to list bucket analytics config xml")
|
||||
}
|
||||
if actualError.IsTruncated != expectedError.IsTruncated {
|
||||
return fmt.Errorf("expected IsTruncated value of '%t', was '%t'", expectedError.IsTruncated, actualError.IsTruncated)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unrecognized type: %s", reflect.TypeOf(expectedBody).String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func headerCompare(expectedHeaders map[string]string, actualHeaders map[string][]string) error {
|
||||
var problems []string
|
||||
for k, want := range expectedHeaders {
|
||||
key := strings.ToLower(k)
|
||||
gotVals := actualHeaders[key]
|
||||
got := ""
|
||||
if len(gotVals) > 0 {
|
||||
got = gotVals[0]
|
||||
}
|
||||
|
||||
// missing header
|
||||
if got == "" {
|
||||
problems = append(problems, fmt.Sprintf("header %q missing (expected %q)", k, want))
|
||||
continue
|
||||
}
|
||||
|
||||
// regex match
|
||||
if strings.HasPrefix(want, "re:") {
|
||||
pat := strings.TrimPrefix(want, "re:")
|
||||
re, err := regexp.Compile(pat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad regex for header %q: %w", k, err)
|
||||
}
|
||||
if !re.MatchString(got) {
|
||||
problems = append(problems, fmt.Sprintf("header %q: expected /%s/, got %q", k, pat, got))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// exact match
|
||||
if want != got {
|
||||
problems = append(problems, fmt.Sprintf("header %q: expected %q, got %q", k, want, got))
|
||||
}
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
return errors.New(strings.Join(problems, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -397,3 +397,15 @@ send_rest_go_command_check_header_key_and_value() {
|
||||
return 0
|
||||
}
|
||||
|
||||
send_rest_go_command_write_response_to_file() {
|
||||
if ! check_param_count_gt "file, params" 2 $#; then
|
||||
return 1
|
||||
fi
|
||||
if ! rest_go_command_perform_send "${@:2}"; then
|
||||
log 2 "error sending rest go command"
|
||||
return 1
|
||||
fi
|
||||
echo -n "$result" > "$1"
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
10
tests/env.sh
10
tests/env.sh
@@ -101,6 +101,16 @@ check_aws_vars() {
|
||||
log 1 "AWS_ENDPOINT_URL missing"
|
||||
exit 1
|
||||
fi
|
||||
export SERVER_NAME="VERSITYGW"
|
||||
else
|
||||
if [ -z "$SERVER_NAME" ]; then
|
||||
export SERVER_NAME="AMAZONS3"
|
||||
else
|
||||
export SERVER_NAME
|
||||
fi
|
||||
fi
|
||||
if [ -n "$TEMPLATE_MATRIX_FILE" ]; then
|
||||
export TEMPLATE_MATRIX_FILE
|
||||
fi
|
||||
# exporting these since they're needed for subshells
|
||||
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_REGION AWS_PROFILE AWS_ENDPOINT_URL
|
||||
|
||||
8
tests/templates/amazons3/get-bucket-analytics.yaml.tmpl
Normal file
8
tests/templates/amazons3/get-bucket-analytics.yaml.tmpl
Normal file
@@ -0,0 +1,8 @@
|
||||
status: 200
|
||||
headers:
|
||||
Server: AmazonS3
|
||||
body:
|
||||
type: list_bucket_analytics_configuration_result
|
||||
data:
|
||||
ListBucketAnalyticsConfigurationsResult:
|
||||
IsTruncated: false
|
||||
7
tests/templates/matrix.yaml
Normal file
7
tests/templates/matrix.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
test_rest_not_implemented.sh:
|
||||
test_REST_-2d_GetBucketAnalyticsConfiguration_-2d_with_template:
|
||||
default:
|
||||
VERSITYGW:
|
||||
not-implemented.yaml.tmpl
|
||||
AMAZONS3:
|
||||
get-bucket-analytics.yaml.tmpl
|
||||
10
tests/templates/versitygw/not-implemented.yaml.tmpl
Normal file
10
tests/templates/versitygw/not-implemented.yaml.tmpl
Normal file
@@ -0,0 +1,10 @@
|
||||
status: 501
|
||||
headers:
|
||||
Content-Type: "application/xml"
|
||||
Server: VERSITYGW
|
||||
body:
|
||||
type: s3_error
|
||||
data:
|
||||
Error:
|
||||
Code: NotImplemented
|
||||
Message: "A header you provided implies functionality that is not implemented."
|
||||
@@ -26,6 +26,45 @@ source ./tests/setup.sh
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - GetBucketAnalyticsConfiguration - with template" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1821"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run get_file_name
|
||||
assert_success
|
||||
file_name=$output
|
||||
|
||||
run send_rest_go_command_write_response_to_file "$TEST_FILE_FOLDER/$file_name" "-bucketName" "$bucket_name" "-query" "analytics="
|
||||
assert_success
|
||||
|
||||
run bash -c "go run ./tests/checker/main.go -dataFile $TEST_FILE_FOLDER/$file_name -batsTestFileName $BATS_TEST_FILENAME \
|
||||
-batsTestName $BATS_TEST_NAME -serverName $SERVER_NAME -matrixFile $TEMPLATE_MATRIX_FILE"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - NotImplemented - correct Content-Type header" {
|
||||
if [ "$DIRECT" != "true" ]; then
|
||||
skip "https://github.com/versity/versitygw/issues/1821"
|
||||
fi
|
||||
run get_bucket_name "$BUCKET_ONE_NAME"
|
||||
assert_success
|
||||
bucket_name=$output
|
||||
|
||||
run setup_bucket_v2 "$bucket_name"
|
||||
assert_success
|
||||
|
||||
run send_rest_go_command_check_header_key_and_value "501" "Content-Type" "application/xml" "-bucketName" "$bucket_name" \
|
||||
"-query" "analytics"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "REST - Get/ListBucketAnalyticsConfiguration(s)" {
|
||||
run test_not_implemented_expect_failure "$BUCKET_ONE_NAME" "analytics=" "GET"
|
||||
assert_success
|
||||
|
||||
Reference in New Issue
Block a user