Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support to get headers from binary request #565

Merged
merged 7 commits into from
Nov 29, 2024
Merged
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
10 changes: 4 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ COPY --from=ui /workspace/dist/assets/*.css cmd/data/index.css
RUN CGO_ENABLED=0 go build -v -a -ldflags "-w -s -X github.com/linuxsuren/api-testing/pkg/version.version=${VERSION}\
-X github.com/linuxsuren/api-testing/pkg/version.date=$(date +%Y-%m-%d)" -o atest .

FROM docker.io/library/ubuntu:23.10
FROM docker.io/library/alpine:3.20.3

LABEL "com.github.actions.name"="API testing"
LABEL "com.github.actions.description"="API testing"
Expand All @@ -54,10 +54,8 @@ COPY --from=builder /workspace/atest /usr/local/bin/atest
COPY --from=builder /workspace/LICENSE /LICENSE
COPY --from=builder /workspace/README.md /README.md

RUN apt update -y && \
# required for atest-store-git
apt install -y --no-install-recommends ssh-client ca-certificates curl && \
rm -rf /var/lib/apt/lists/*

# required for atest-store-git
RUN apk add curl openssh-client bash openssl

EXPOSE 8080
CMD ["atest", "server", "--local-storage=/var/data/api-testing/*.yaml"]
4 changes: 2 additions & 2 deletions console/atest-ui/src/views/TestCase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1335,10 +1335,10 @@ const renameTestCase = (name: string) => {
<Codemirror v-if="!isResponseFile" v-model="testResult.bodyText"/>
<div v-if="isResponseFile" style="padding-top: 10px;">
<el-row>
<el-col :span="8">
<el-col :span="10">
<div>Response body is too large, please download to view.</div>
</el-col>
<el-col :span="4">
<el-col :span="2">
<el-button type="primary" @click="downloadResponseFile">Download</el-button>
</el-col>
</el-row>
Expand Down
1 change: 1 addition & 0 deletions e2e/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FROM ghcr.io/linuxsuren/api-testing:master
WORKDIR /workspace
COPY e2e/* .
COPY helm/api-testing api-testing
RUN apk add curl openssh-client bash openssl
RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
RUN chmod 700 get_helm.sh
RUN ./get_helm.sh
Expand Down
26 changes: 26 additions & 0 deletions e2e/test-suite-common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,32 @@ items:
}
}
}
- name: createTestCaseForBinary
request:
api: /suites/{{.param.suiteName}}/cases
method: POST
header:
X-Store-Name: "{{.param.store}}"
body: |
{
"suiteName": "{{.param.suiteName}}",
"data": {
"name": "binary",
"request": {
"api": "{{.param.server}}/get",
"method": "GET"
}
}
}
- name: runBinaryCase
request:
api: /suites/{{.param.suiteName}}/cases/binary/run
method: POST
header:
X-Store-Name: "{{.param.store}}"
expect:
verify:
- any(data.header, {.value == "application/octet-stream"})
- name: getTestCase
request:
api: /suites/{{.param.suiteName}}/cases/{{.param.caseName}}
Expand Down
55 changes: 49 additions & 6 deletions pkg/runner/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -206,10 +208,11 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte

respType := util.GetFirstHeaderValue(resp.Header, util.ContentType)

r.withSimpleResponseRecord(resp)
if isNonBinaryContent(respType) {
var responseBodyData []byte
var rErr error
if responseBodyData, rErr = r.withResponseRecord(resp); rErr != nil {
if responseBodyData, rErr = r.withResponseBodyRecord(resp); rErr != nil {
err = errors.Join(err, rErr)
return
}
Expand All @@ -224,13 +227,49 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte

err = errors.Join(err, jsonSchemaValidation(testcase.Expect.Schema, responseBodyData))
} else {
switch respType {
case util.OctetStream, util.Image:
var data []byte
if data, err = io.ReadAll(resp.Body); err == nil {
r.simpleResponse.RawBody = data
r.simpleResponse, err = HandleLargeResponseBody(r.simpleResponse, testcase.Group, testcase.Name)
}
}
r.log.Debug("skip to read the body due to it is not struct content: %q\n", respType)
}

r.cookies = append(r.cookies, resp.Cookies()...)
return
}

func HandleLargeResponseBody(resp SimpleResponse, suite string, caseName string) (SimpleResponse, error) {
const maxSize = 5120
prefix := "isFilePath-" + strings.Join([]string{suite, caseName}, "-")
if len(resp.Body) > 0 {
resp.RawBody = []byte(resp.Body)
}

if len(resp.RawBody) > maxSize {
fmt.Println("response body is too large, will be saved to file", "size", len(resp.RawBody))
tmpFile, err := os.CreateTemp("", prefix+"-")
defer tmpFile.Close()
if err != nil {
return resp, fmt.Errorf("failed to create file: %w", err)
}

if _, err = tmpFile.Write(resp.RawBody); err != nil {
return resp, fmt.Errorf("failed to write response body to file: %w", err)
}
absFilePath, err := filepath.Abs(tmpFile.Name())
if err != nil {
return resp, fmt.Errorf("failed to get absolute file path: %w", err)
}
resp.Body = filepath.Base(absFilePath)
return resp, nil
}
return resp, nil
}

func ammendHeaders(headers http.Header, body []byte) {
// add content-length if it's missing
if val := headers.Get(util.ContentLength); val == "" {
Expand Down Expand Up @@ -300,19 +339,23 @@ func generateRandomValue(param spec.Parameter) interface{} {
}
}

func (r *simpleTestCaseRunner) withResponseRecord(resp *http.Response) (responseBodyData []byte, err error) {
responseBodyData, err = io.ReadAll(resp.Body)
func (r *simpleTestCaseRunner) withSimpleResponseRecord(resp *http.Response) {
r.simpleResponse = SimpleResponse{
StatusCode: resp.StatusCode,
Header: make(map[string]string),
Body: string(responseBodyData),
}

// add some headers for convienience
ammendHeaders(resp.Header, responseBodyData)
for key := range resp.Header {
r.simpleResponse.Header[key] = resp.Header.Get(key)
}
}

func (r *simpleTestCaseRunner) withResponseBodyRecord(resp *http.Response) (responseBodyData []byte, err error) {
responseBodyData, err = io.ReadAll(resp.Body)
r.simpleResponse.Body = string(responseBodyData)

// add some headers for convenience
ammendHeaders(resp.Header, responseBodyData)
return
}

Expand Down
1 change: 1 addition & 0 deletions pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type ResponseRecord interface {
type SimpleResponse struct {
Header map[string]string
Body string
RawBody []byte
StatusCode int
}

Expand Down
28 changes: 1 addition & 27 deletions pkg/server/remote_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (s *server) Run(ctx context.Context, task *TestTask) (reply *TestResult, er
output, testErr := suiteRunner.RunTestCase(&testCase, dataContext, ctx)
if getter, ok := suiteRunner.(runner.ResponseRecord); ok {
resp := getter.GetResponseRecord()
resp, err = handleLargeResponseBody(resp, suite.Name, testCase.Name)
resp, err = runner.HandleLargeResponseBody(resp, suite.Name, testCase.Name)
reply.TestCaseResult = append(reply.TestCaseResult, &TestCaseResult{
StatusCode: int32(resp.StatusCode),
Body: resp.Body,
Expand Down Expand Up @@ -343,32 +343,6 @@ func (s *server) BatchRun(srv Runner_BatchRunServer) (err error) {
}
}
}
return
}

func handleLargeResponseBody(resp runner.SimpleResponse, suite string, caseName string) (reply runner.SimpleResponse, err error) {
const maxSize = 5120
prefix := "isFilePath-" + strings.Join([]string{suite, caseName}, "-")

if len(resp.Body) > maxSize {
remoteServerLogger.Logger.Info("response body is too large, will be saved to file", "size", len(resp.Body))
tmpFile, err := os.CreateTemp("", prefix+"-")
defer tmpFile.Close()
if err != nil {
return resp, fmt.Errorf("failed to create file: %w", err)
}

if _, err = tmpFile.Write([]byte(resp.Body)); err != nil {
return resp, fmt.Errorf("failed to write response body to file: %w", err)
}
absFilePath, err := filepath.Abs(tmpFile.Name())
if err != nil {
return resp, fmt.Errorf("failed to get absolute file path: %w", err)
}
resp.Body = filepath.Base(absFilePath)
return resp, nil
}
return resp, nil
}

func (s *server) DownloadResponseFile(ctx context.Context, in *TestCase) (reply *FileData, err error) {
Expand Down
1 change: 1 addition & 0 deletions pkg/util/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const (
YAML = "application/yaml"
ZIP = "application/zip"
OctetStream = "application/octet-stream"
Image = "image/jpeg"
Plain = "text/plain"
Authorization = "Authorization"
)
1 change: 0 additions & 1 deletion tools/make/run.mk
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ run-server: ## Run the API Testing server
run-server: build-ui run-backend
run-backend:
go run . server --local-storage 'bin/*.yaml' --console-path ${ATEST_UI}/dist \
--mock-config bin/mock.yaml \
LinuxSuRen marked this conversation as resolved.
Show resolved Hide resolved
--extension-registry ghcr.io --download-timeout 90s

.PHONY: run-console
Expand Down