mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 03:12:14 +01:00
test: add integration tests
This commit is contained in:
57
test/integration/scenario/compare.go
Normal file
57
test/integration/scenario/compare.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func compareJson(expected, actual interface{}) error {
|
||||
// Handle maps (JSON objects).
|
||||
expectedMap, ok := expected.(map[string]interface{})
|
||||
if ok {
|
||||
actualMap, ok := actual.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected an object, but actual is: %T", actual)
|
||||
}
|
||||
// For each key in expected, compare if the expected value is not
|
||||
// "ignore".
|
||||
for key, expVal := range expectedMap {
|
||||
if str, isStr := expVal.(string); isStr && str == "ignore" {
|
||||
continue // Skip.
|
||||
}
|
||||
actVal, exists := actualMap[key]
|
||||
if !exists {
|
||||
return fmt.Errorf("missing expected key %q", key)
|
||||
}
|
||||
if err := compareJson(expVal, actVal); err != nil {
|
||||
return fmt.Errorf("key %q: %w", key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle slices (JSON arrays).
|
||||
expectedSlice, ok := expected.([]interface{})
|
||||
if ok {
|
||||
actualSlice, ok := actual.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("expected an array, but actual is: %T", actual)
|
||||
}
|
||||
if len(expectedSlice) != len(actualSlice) {
|
||||
return fmt.Errorf("expected array length to be: %d, but actual is: %d", len(expectedSlice), len(actualSlice))
|
||||
}
|
||||
for i := range expectedSlice {
|
||||
if err := compareJson(expectedSlice[i], actualSlice[i]); err != nil {
|
||||
return fmt.Errorf("at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// For other types, compare directly.
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
return fmt.Errorf("expected %v (%T) but got %v (%T)", expected, expected, actual, actual)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
136
test/integration/scenario/containers.go
Normal file
136
test/integration/scenario/containers.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/network"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
var (
|
||||
GotenbergDockerRepository string
|
||||
GotenbergVersion string
|
||||
GotenbergContainerPlatform string
|
||||
)
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func (n *noopLogger) Printf(format string, v ...interface{}) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func startGotenbergContainer(ctx context.Context, env map[string]string) (*testcontainers.DockerNetwork, testcontainers.Container, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
n, err := network.New(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create Gotenberg container network: %w", err)
|
||||
}
|
||||
|
||||
heathPath := "/health"
|
||||
if env["API_ROOT_PATH"] != "" {
|
||||
heathPath = fmt.Sprintf("%shealth", env["API_ROOT_PATH"])
|
||||
}
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: fmt.Sprintf("gotenberg/%s:%s", GotenbergDockerRepository, GotenbergVersion),
|
||||
ImagePlatform: GotenbergContainerPlatform,
|
||||
ExposedPorts: []string{"3000/tcp"},
|
||||
HostConfigModifier: func(hostConfig *container.HostConfig) {
|
||||
hostConfig.ExtraHosts = []string{"host.docker.internal:host-gateway"}
|
||||
},
|
||||
Networks: []string{n.Name},
|
||||
WaitingFor: wait.ForHTTP(heathPath),
|
||||
Env: env,
|
||||
}
|
||||
|
||||
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
Logger: &noopLogger{},
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("start new Gotenberg container: %w", err)
|
||||
}
|
||||
|
||||
return n, c, err
|
||||
}
|
||||
|
||||
func execCommandInIntegrationToolsContainer(ctx context.Context, cmd []string, path string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: "gotenberg/integration-tools:latest",
|
||||
Files: []testcontainers.ContainerFile{
|
||||
{
|
||||
HostFilePath: path,
|
||||
ContainerFilePath: filepath.Base(path),
|
||||
FileMode: 0o700,
|
||||
},
|
||||
},
|
||||
Cmd: []string{"tail", "-f", "/dev/null"}, // Keeps container running indefinitely.
|
||||
}
|
||||
|
||||
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
Logger: &noopLogger{},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("start new Integration Tools container: %w", err)
|
||||
}
|
||||
defer func(c testcontainers.Container, ctx context.Context) {
|
||||
err := c.Terminate(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("terminate container: %v\n", err)
|
||||
}
|
||||
}(c, ctx)
|
||||
|
||||
_, output, err := c.Exec(ctx, cmd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(output)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read output: %w", err)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func containerHttpEndpoint(ctx context.Context, container testcontainers.Container, port nat.Port) (string, error) {
|
||||
ip, err := container.Host(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get container IP: %w", err)
|
||||
}
|
||||
mapped, err := container.MappedPort(ctx, port)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get container port: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("http://%s:%s", ip, mapped.Port()), nil
|
||||
}
|
||||
|
||||
func containerLogEntries(ctx context.Context, container testcontainers.Container) (string, error) {
|
||||
logReader, err := container.Logs(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get container log entries: %w", err)
|
||||
}
|
||||
defer logReader.Close()
|
||||
|
||||
logsBytes, err := io.ReadAll(logReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read container log entries: %w", err)
|
||||
}
|
||||
|
||||
return string(logsBytes), nil
|
||||
}
|
||||
2
test/integration/scenario/doc.go
Normal file
2
test/integration/scenario/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package scenario gathers all steps used in the features.
|
||||
package scenario
|
||||
83
test/integration/scenario/http.go
Normal file
83
test/integration/scenario/http.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func doRequest(method, url string, headers map[string]string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create a request: %w", err)
|
||||
}
|
||||
|
||||
for header, value := range headers {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send a request: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func doFormDataRequest(method, url string, fields map[string]string, files map[string][]string, headers map[string]string) (*http.Response, error) {
|
||||
var b bytes.Buffer
|
||||
writer := multipart.NewWriter(&b)
|
||||
|
||||
for name, value := range fields {
|
||||
err := writer.WriteField(name, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write field %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for name, paths := range files {
|
||||
for _, path := range paths {
|
||||
part, err := writer.CreateFormFile(name, filepath.Base(path))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create form file %q: %w", filepath.Base(path), err)
|
||||
}
|
||||
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open file %q: %w", path, err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
_, err = io.Copy(part, reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copy file %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("close writer: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create a request: %w", err)
|
||||
}
|
||||
|
||||
for header, value := range headers {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send a request: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
911
test/integration/scenario/scenario.go
Normal file
911
test/integration/scenario/scenario.go
Normal file
@@ -0,0 +1,911 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mholt/archives"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
)
|
||||
|
||||
type scenario struct {
|
||||
resp *httptest.ResponseRecorder
|
||||
workdir string
|
||||
gotenbergContainer testcontainers.Container
|
||||
gotenbergContainerNetwork *testcontainers.DockerNetwork
|
||||
server *server
|
||||
hostPort int
|
||||
}
|
||||
|
||||
func (s *scenario) reset(ctx context.Context) error {
|
||||
s.resp = httptest.NewRecorder()
|
||||
|
||||
err := os.RemoveAll(s.workdir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove workdir %q: %w", s.workdir, err)
|
||||
}
|
||||
s.workdir = ""
|
||||
|
||||
if s.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.server.stop(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop server: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iHaveADefaultGotenbergContainer(ctx context.Context) error {
|
||||
n, c, err := startGotenbergContainer(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create Gotenberg container: %s", err)
|
||||
}
|
||||
s.gotenbergContainerNetwork = n
|
||||
s.gotenbergContainer = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iHaveAGotenbergContainerWithTheFollowingEnvironmentVariables(ctx context.Context, envTable *godog.Table) error {
|
||||
env := make(map[string]string)
|
||||
for _, row := range envTable.Rows {
|
||||
env[row.Cells[0].Value] = row.Cells[1].Value
|
||||
}
|
||||
n, c, err := startGotenbergContainer(ctx, env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create Gotenberg container: %s", err)
|
||||
}
|
||||
s.gotenbergContainerNetwork = n
|
||||
s.gotenbergContainer = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iHaveAServer(ctx context.Context) error {
|
||||
srv, err := newServer(ctx, s.workdir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create server: %s", err)
|
||||
}
|
||||
s.server = srv
|
||||
port, err := s.server.start(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start server: %s", err)
|
||||
}
|
||||
s.hostPort = port
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iMakeARequestToGotenberg(ctx context.Context, method, endpoint string) error {
|
||||
return s.iMakeARequestToGotenbergWithTheFollowingHeaders(ctx, method, endpoint, nil)
|
||||
}
|
||||
|
||||
func (s *scenario) iMakeARequestToGotenbergWithTheFollowingHeaders(ctx context.Context, method, endpoint string, headersTable *godog.Table) error {
|
||||
if s.gotenbergContainer == nil {
|
||||
return errors.New("no Gotenberg container")
|
||||
}
|
||||
|
||||
base, err := containerHttpEndpoint(ctx, s.gotenbergContainer, "3000")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get container HTTP endpoint: %w", err)
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
if headersTable != nil {
|
||||
for _, row := range headersTable.Rows {
|
||||
headers[row.Cells[0].Value] = row.Cells[1].Value
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := doRequest(method, fmt.Sprintf("%s%s", base, endpoint), headers, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("do request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read response body: %w", err)
|
||||
}
|
||||
|
||||
s.resp = httptest.NewRecorder()
|
||||
s.resp.Code = resp.StatusCode
|
||||
for key, values := range resp.Header {
|
||||
for _, v := range values {
|
||||
s.resp.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
_, err = s.resp.Body.Write(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write response body: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ctx context.Context, method, endpoint string, dataTable *godog.Table) error {
|
||||
if s.gotenbergContainer == nil {
|
||||
return errors.New("no Gotenberg container")
|
||||
}
|
||||
|
||||
fields := make(map[string]string)
|
||||
files := make(map[string][]string)
|
||||
headers := make(map[string]string)
|
||||
|
||||
for _, row := range dataTable.Rows {
|
||||
name := row.Cells[0].Value
|
||||
value := row.Cells[1].Value
|
||||
kind := row.Cells[2].Value
|
||||
|
||||
switch kind {
|
||||
case "field":
|
||||
if name == "downloadFrom" || name == "url" || name == "cookies" {
|
||||
fields[name] = strings.ReplaceAll(value, "%d", fmt.Sprintf("%d", s.hostPort))
|
||||
continue
|
||||
}
|
||||
fields[name] = value
|
||||
case "file":
|
||||
if strings.Contains(value, "teststore") {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
||||
}
|
||||
value = strings.ReplaceAll(value, "teststore", dirPath)
|
||||
} else {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current directory: %w", err)
|
||||
}
|
||||
value = fmt.Sprintf("%s/%s", wd, value)
|
||||
}
|
||||
files[name] = append(files[name], value)
|
||||
case "header":
|
||||
if name == "Gotenberg-Webhook-Url" || name == "Gotenberg-Webhook-Error-Url" {
|
||||
headers[name] = fmt.Sprintf(value, s.hostPort)
|
||||
continue
|
||||
}
|
||||
headers[name] = value
|
||||
default:
|
||||
return fmt.Errorf("unexpected %q %q", kind, value)
|
||||
}
|
||||
}
|
||||
|
||||
base, err := containerHttpEndpoint(ctx, s.gotenbergContainer, "3000")
|
||||
if err != nil {
|
||||
return fmt.Errorf("get container HTTP endpoint: %w", err)
|
||||
}
|
||||
|
||||
resp, err := doFormDataRequest(method, fmt.Sprintf("%s%s", base, endpoint), fields, files, headers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("do request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read response body: %w", err)
|
||||
}
|
||||
|
||||
s.resp = httptest.NewRecorder()
|
||||
s.resp.Code = resp.StatusCode
|
||||
for key, values := range resp.Header {
|
||||
for _, v := range values {
|
||||
s.resp.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
_, err = s.resp.Body.Write(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
cd := resp.Header.Get("Content-Disposition")
|
||||
if cd == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, params, err := mime.ParseMediaType(cd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Content-Disposition header: %w", err)
|
||||
}
|
||||
|
||||
filename, ok := params["filename"]
|
||||
if !ok {
|
||||
return errors.New("no filename in Content-Disposition header")
|
||||
}
|
||||
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
err = os.MkdirAll(dirPath, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create working directory: %w", err)
|
||||
}
|
||||
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
file, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create file %q: %w", fpath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write file %q: %w", fpath, err)
|
||||
}
|
||||
|
||||
if resp.Header.Get("Content-Type") == "application/zip" {
|
||||
var format archives.Zip
|
||||
err = format.Extract(ctx, file, func(ctx context.Context, f archives.FileInfo) error {
|
||||
source, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file %q: %w", f.Name(), err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
targetPath := fmt.Sprintf("%s/%s", dirPath, f.Name())
|
||||
target, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create file %q: %w", targetPath, err)
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
_, err = io.Copy(target, source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy file %q: %w", targetPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) iWaitForTheAsynchronousRequestToWebhook(ctx context.Context) error {
|
||||
if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
}
|
||||
if s.server.req != nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-s.server.errChan:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scenario) theGotenbergContainerShouldLogTheFollowingEntries(ctx context.Context, should string, entriesTable *godog.Table) error {
|
||||
if s.gotenbergContainer == nil {
|
||||
return errors.New("no Gotenberg container")
|
||||
}
|
||||
|
||||
expected := make([]string, len(entriesTable.Rows))
|
||||
for i, row := range entriesTable.Rows {
|
||||
expected[i] = row.Cells[0].Value
|
||||
}
|
||||
|
||||
invert := should == "should NOT"
|
||||
check := func() error {
|
||||
logs, err := containerLogEntries(ctx, s.gotenbergContainer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get log entries: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range expected {
|
||||
if !invert && !strings.Contains(logs, entry) {
|
||||
return fmt.Errorf("expected log entry %q not found in %q", expected, logs)
|
||||
}
|
||||
|
||||
if invert && strings.Contains(logs, entry) {
|
||||
return fmt.Errorf("log entry %q NOT expected", expected)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
for i := 0; i < 3; i++ {
|
||||
err = check()
|
||||
if err != nil && !invert {
|
||||
// We have to retry as not all logs may have been produced.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scenario) theResponseStatusCodeShouldBe(expected int) error {
|
||||
if expected != s.resp.Code {
|
||||
return fmt.Errorf("expected response status code to be: %d, but actual is: %d %q", expected, s.resp.Code, s.resp.Body.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theHeaderValueShouldBe(kind, name string, expected string) error {
|
||||
var actual string
|
||||
if kind == "response" {
|
||||
actual = s.resp.Header().Get(name)
|
||||
} else if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
} else if s.server.req == nil {
|
||||
return errors.New("no webhook request found")
|
||||
} else {
|
||||
actual = s.server.req.Header.Get(name)
|
||||
}
|
||||
|
||||
if expected != actual {
|
||||
return fmt.Errorf("expected %s header %q to be: %q, but actual is: %q", kind, name, expected, actual)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theCookieValueShouldBe(kind, name, expected string) error {
|
||||
var cookies []*http.Cookie
|
||||
if kind == "response" {
|
||||
cookies = s.resp.Result().Cookies()
|
||||
} else if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
} else if s.server.req == nil {
|
||||
return errors.New("no webhook request found")
|
||||
} else {
|
||||
cookies = s.server.req.Cookies()
|
||||
}
|
||||
|
||||
var actual *http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == name {
|
||||
actual = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if actual == nil {
|
||||
if expected != "" {
|
||||
return fmt.Errorf("expected %s cookie %q not found", kind, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if expected != actual.Value {
|
||||
return fmt.Errorf("expected %s cookie %q to be: %q, but actual is: %q", kind, name, expected, actual.Value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theBodyShouldMatchString(kind string, expectedDoc *godog.DocString) error {
|
||||
var actual string
|
||||
if kind == "response" {
|
||||
actual = s.resp.Body.String()
|
||||
} else if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
} else if s.server.req == nil {
|
||||
return errors.New("no webhook request found")
|
||||
} else {
|
||||
body, err := io.ReadAll(s.server.req.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read request body: %w", err)
|
||||
}
|
||||
actual = string(body)
|
||||
}
|
||||
|
||||
expected := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion)
|
||||
|
||||
if actual != expected {
|
||||
return fmt.Errorf("expected %q body to be: %q, but actual is: %q", kind, expected, actual)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theBodyShouldContainString(kind string, expectedDoc *godog.DocString) error {
|
||||
var actual string
|
||||
if kind == "response" {
|
||||
actual = s.resp.Body.String()
|
||||
} else if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
} else if s.server.req == nil {
|
||||
return errors.New("no webhook request found")
|
||||
} else {
|
||||
body, err := io.ReadAll(s.server.req.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read request body: %w", err)
|
||||
}
|
||||
actual = string(body)
|
||||
}
|
||||
|
||||
expected := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion)
|
||||
|
||||
if !strings.Contains(actual, expected) {
|
||||
return fmt.Errorf("expected %q body to contain: %q, but actual is: %q", kind, expected, actual)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *godog.DocString) error {
|
||||
var body []byte
|
||||
if kind == "response" {
|
||||
body = s.resp.Body.Bytes()
|
||||
} else if s.server == nil {
|
||||
return errors.New("server not initialized")
|
||||
} else if s.server.req == nil {
|
||||
return errors.New("no webhook request found")
|
||||
} else {
|
||||
b, err := io.ReadAll(s.server.req.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read request body: %w", err)
|
||||
}
|
||||
body = b
|
||||
}
|
||||
|
||||
var expected, actual interface{}
|
||||
|
||||
content := strings.ReplaceAll(expectedDoc.Content, "{version}", GotenbergVersion)
|
||||
err := json.Unmarshal([]byte(content), &expected)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal expected JSON: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &actual)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal actual JSON: %w", err)
|
||||
}
|
||||
|
||||
err = compareJson(expected, actual)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expected matching JSON: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
||||
}
|
||||
|
||||
var paths []string
|
||||
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(info.Name()), ".pdf") {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
|
||||
if len(paths) != expected {
|
||||
return fmt.Errorf("expected %d PDF(s), but actual is %d", expected, len(paths))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTable *godog.Table) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
||||
}
|
||||
|
||||
var filenames []string
|
||||
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if !info.IsDir() {
|
||||
filenames = append(filenames, info.Name())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
|
||||
for _, row := range filesTable.Rows {
|
||||
found := false
|
||||
expected := row.Cells[0].Value
|
||||
for _, filename := range filenames {
|
||||
if strings.HasPrefix(expected, "*_") && strings.Contains(filename, strings.ReplaceAll(expected, "*_", "")) {
|
||||
found = true
|
||||
}
|
||||
if strings.EqualFold(expected, filename) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("expected file %q not found among %q", expected, filenames)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Context, kind, validate string, tolerance int) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
||||
}
|
||||
|
||||
var paths []string
|
||||
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(info.Name()), ".pdf") {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
|
||||
var flavor string
|
||||
switch validate {
|
||||
case "PDF/A-1b":
|
||||
flavor = "1b"
|
||||
case "PDF/A-2b":
|
||||
flavor = "2b"
|
||||
case "PDF/A-3b":
|
||||
flavor = "3b"
|
||||
case "PDF/UA-1":
|
||||
flavor = "ua1"
|
||||
case "PDF/UA-2":
|
||||
flavor = "ua2"
|
||||
default:
|
||||
return fmt.Errorf("unknown %q", validate)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
cmd := []string{
|
||||
"verapdf",
|
||||
"-f",
|
||||
flavor,
|
||||
filepath.Base(path),
|
||||
}
|
||||
|
||||
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`failedRules="(\d+)"`)
|
||||
matches := re.FindStringSubmatch(output)
|
||||
|
||||
if len(matches) < 2 {
|
||||
return errors.New("expected failed rules")
|
||||
}
|
||||
|
||||
failedRules, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert failed rules value %q to integer: %w", matches[1], err)
|
||||
}
|
||||
|
||||
if tolerance < failedRules {
|
||||
return fmt.Errorf("expected failed rules to be inferior or equal to: %d, but actual is %d", tolerance, failedRules)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfShouldHavePages(ctx context.Context, name string, pages int) error {
|
||||
var path string
|
||||
if !strings.HasPrefix(name, "*_") {
|
||||
path = fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("PDF %q does not exist", path)
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.Contains(info.Name(), substr) {
|
||||
path = currentPath
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := []string{
|
||||
"pdfinfo",
|
||||
filepath.Base(path),
|
||||
}
|
||||
|
||||
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
output = strings.ReplaceAll(output, " ", "")
|
||||
re := regexp.MustCompile(`Pages:(\d+)`)
|
||||
matches := re.FindStringSubmatch(output)
|
||||
|
||||
if len(matches) < 2 {
|
||||
return errors.New("expected pages")
|
||||
}
|
||||
|
||||
actual, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert pages value %q to integer: %w", matches[1], err)
|
||||
}
|
||||
|
||||
if actual != pages {
|
||||
return fmt.Errorf("expected %d pages, but actual is %d", pages, actual)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context, name string, kind string) error {
|
||||
var path string
|
||||
if !strings.HasPrefix(name, "*_") {
|
||||
path = fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("PDF %q does not exist", path)
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.Contains(info.Name(), substr) {
|
||||
path = currentPath
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := []string{
|
||||
"pdfinfo",
|
||||
filepath.Base(path),
|
||||
}
|
||||
|
||||
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
output = strings.ReplaceAll(output, " ", "")
|
||||
re := regexp.MustCompile(`Pagesize:(\d+)x(\d+).*`)
|
||||
matches := re.FindStringSubmatch(output)
|
||||
|
||||
if len(matches) < 3 {
|
||||
return errors.New("expected page size")
|
||||
}
|
||||
|
||||
invert := kind == "should NOT"
|
||||
|
||||
width, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert width value %q to integer: %w", matches[1], err)
|
||||
}
|
||||
|
||||
height, err := strconv.Atoi(matches[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert height value %q to integer: %w", matches[2], err)
|
||||
}
|
||||
|
||||
if invert && height < width {
|
||||
return fmt.Errorf("expected height %d to be greater than width %d", height, width)
|
||||
}
|
||||
|
||||
if !invert && width < height {
|
||||
return fmt.Errorf("expected width %d to be greater than height %d", width, height)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfShouldHaveTheFollowingContentAtPage(ctx context.Context, name, kind string, page int, expected *godog.DocString) error {
|
||||
var path string
|
||||
if !strings.HasPrefix(name, "*_") {
|
||||
path = fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("PDF %q does not exist", path)
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.Contains(info.Name(), substr) {
|
||||
path = currentPath
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := []string{
|
||||
"pdftotext",
|
||||
"-f",
|
||||
fmt.Sprintf("%d", page),
|
||||
"-l",
|
||||
fmt.Sprintf("%d", page),
|
||||
filepath.Base(path),
|
||||
"-",
|
||||
}
|
||||
|
||||
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
invert := kind == "should NOT"
|
||||
|
||||
if !invert && !strings.Contains(output, expected.Content) {
|
||||
return fmt.Errorf("expected %q not found in %q", expected.Content, output)
|
||||
}
|
||||
|
||||
if invert && strings.Contains(output, expected.Content) {
|
||||
return fmt.Errorf("%q found in %q", expected.Content, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should string) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
||||
}
|
||||
|
||||
var paths []string
|
||||
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(info.Name()), ".pdf") {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk %q: %w", s.workdir, err)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
cmd := []string{
|
||||
"verapdf",
|
||||
"-off",
|
||||
"--extract",
|
||||
"annotations",
|
||||
filepath.Base(path),
|
||||
}
|
||||
|
||||
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
invert := should == "should NOT"
|
||||
if invert && strings.Contains(output, "<featuresReport></featuresReport>") {
|
||||
return fmt.Errorf("PDF %q is flatten", path)
|
||||
}
|
||||
|
||||
if !invert && !strings.Contains(output, "<featuresReport></featuresReport>") {
|
||||
return fmt.Errorf("PDF %q is not flatten", path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||
s := &scenario{}
|
||||
ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("get current directory: %w", err)
|
||||
}
|
||||
s.workdir = fmt.Sprintf("%s/teststore/%s", wd, uuid.NewString())
|
||||
err = os.MkdirAll(s.workdir, 0o755)
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("create working directory: %w", err)
|
||||
}
|
||||
return ctx, nil
|
||||
})
|
||||
ctx.Given(`^I have a default Gotenberg container$`, s.iHaveADefaultGotenbergContainer)
|
||||
ctx.Given(`^I have a Gotenberg container with the following environment variable\(s\):$`, s.iHaveAGotenbergContainerWithTheFollowingEnvironmentVariables)
|
||||
ctx.Given(`^I have a (webhook|static) server$`, s.iHaveAServer)
|
||||
ctx.When(`^I make a "(GET|HEAD)" request to Gotenberg at the "([^"]*)" endpoint$`, s.iMakeARequestToGotenberg)
|
||||
ctx.When(`^I make a "(GET|HEAD)" request to Gotenberg at the "([^"]*)" endpoint with the following header\(s\):$`, s.iMakeARequestToGotenbergWithTheFollowingHeaders)
|
||||
ctx.When(`^I make a "(POST)" request to Gotenberg at the "([^"]*)" endpoint with the following form data and header\(s\):$`, s.iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders)
|
||||
ctx.When(`^I wait for the asynchronous request to the webhook$`, s.iWaitForTheAsynchronousRequestToWebhook)
|
||||
ctx.Then(`^the Gotenberg container (should|should NOT) log the following entries:$`, s.theGotenbergContainerShouldLogTheFollowingEntries)
|
||||
ctx.Then(`^the response status code should be (\d+)$`, s.theResponseStatusCodeShouldBe)
|
||||
ctx.Then(`^the (response|webhook request|file request|server request) header "([^"]*)" should be "([^"]*)"$`, s.theHeaderValueShouldBe)
|
||||
ctx.Then(`^the (response|webhook request|file request|server request) cookie "([^"]*)" should be "([^"]*)"$`, s.theCookieValueShouldBe)
|
||||
ctx.Then(`^the (response|webhook request) body should match string:$`, s.theBodyShouldMatchString)
|
||||
ctx.Then(`^the (response|webhook request) body should contain string:$`, s.theBodyShouldContainString)
|
||||
ctx.Then(`^the (response|webhook request) body should match JSON:$`, s.theBodyShouldMatchJSON)
|
||||
ctx.Then(`^there should be (\d+) PDF\(s\) in the (response|webhook request)$`, s.thereShouldBePdfs)
|
||||
ctx.Then(`^there should be the following file\(s\) in the (response|webhook request):$`, s.thereShouldBeTheFollowingFiles)
|
||||
ctx.Then(`^the (response|webhook request) PDF\(s\) should be valid "([^"]*)" with a tolerance of (\d+) failed rule\(s\)$`, s.thePdfsShouldBeValidWithAToleranceOf)
|
||||
ctx.Then(`^the (response|webhook request) PDF\(s\) (should|should NOT) be flatten$`, s.thePdfsShouldBeFlatten)
|
||||
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
|
||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
|
||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
|
||||
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
|
||||
if s.gotenbergContainer != nil {
|
||||
errTerminate := s.gotenbergContainer.Terminate(ctx, testcontainers.StopTimeout(0))
|
||||
if errTerminate != nil {
|
||||
return ctx, fmt.Errorf("terminate Gotenberg container: %w", errTerminate)
|
||||
}
|
||||
}
|
||||
if s.gotenbergContainerNetwork != nil {
|
||||
errRemove := s.gotenbergContainerNetwork.Remove(ctx)
|
||||
if errRemove != nil {
|
||||
return ctx, fmt.Errorf("remove Gotenberg container network: %w", errRemove)
|
||||
}
|
||||
}
|
||||
return ctx, nil
|
||||
})
|
||||
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
|
||||
errReset := s.reset(ctx)
|
||||
if errReset != nil {
|
||||
return ctx, fmt.Errorf("reset scenario: %w", errReset)
|
||||
}
|
||||
return ctx, nil
|
||||
})
|
||||
}
|
||||
176
test/integration/scenario/server.go
Normal file
176
test/integration/scenario/server.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package scenario
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mholt/archives"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
srv *echo.Echo
|
||||
req *http.Request
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func newServer(ctx context.Context, workdir string) (*server, error) {
|
||||
srv := echo.New()
|
||||
srv.HideBanner = true
|
||||
srv.HidePort = true
|
||||
s := &server{
|
||||
srv: srv,
|
||||
errChan: make(chan error, 1),
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get current directory: %w", err)
|
||||
}
|
||||
|
||||
webhookErr := func(err error) error {
|
||||
s.errChan <- err
|
||||
return err
|
||||
}
|
||||
|
||||
webhookHandler := func(c echo.Context) error {
|
||||
s.req = c.Request()
|
||||
|
||||
body, err := io.ReadAll(s.req.Body)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("read request body: %w", err))
|
||||
}
|
||||
|
||||
cd := s.req.Header.Get("Content-Disposition")
|
||||
if cd == "" {
|
||||
return webhookErr(fmt.Errorf("no Content-Disposition header"))
|
||||
}
|
||||
|
||||
_, params, err := mime.ParseMediaType(cd)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("parse Content-Disposition header: %w", err))
|
||||
}
|
||||
|
||||
filename, ok := params["filename"]
|
||||
if !ok {
|
||||
return webhookErr(errors.New("no filename in Content-Disposition header"))
|
||||
}
|
||||
|
||||
dirPath := fmt.Sprintf("%s/%s", workdir, s.req.Header.Get("Gotenberg-Trace"))
|
||||
err = os.MkdirAll(dirPath, 0o755)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("create working directory: %w", err))
|
||||
}
|
||||
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
file, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("create file %q: %w", fpath, err))
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(body)
|
||||
if err != nil {
|
||||
return webhookErr(fmt.Errorf("write file %q: %w", fpath, err))
|
||||
}
|
||||
|
||||
if s.req.Header.Get("Content-Type") == "application/zip" {
|
||||
var format archives.Zip
|
||||
err = format.Extract(ctx, file, func(ctx context.Context, f archives.FileInfo) error {
|
||||
source, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file %q: %w", f.Name(), err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
targetPath := fmt.Sprintf("%s/%s", dirPath, f.Name())
|
||||
target, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create file %q: %w", targetPath, err)
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
_, err = io.Copy(target, source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy file %q: %w", targetPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return webhookErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return webhookErr(c.String(http.StatusOK, http.StatusText(http.StatusOK)))
|
||||
}
|
||||
webhookErrorHandler := func(c echo.Context) error {
|
||||
s.req = c.Request()
|
||||
return webhookErr(c.String(http.StatusOK, http.StatusText(http.StatusOK)))
|
||||
}
|
||||
|
||||
srv.POST("/webhook", webhookHandler)
|
||||
srv.PATCH("/webhook", webhookHandler)
|
||||
srv.PUT("/webhook", webhookHandler)
|
||||
srv.POST("/webhook/error", webhookErrorHandler)
|
||||
srv.PATCH("/webhook/error", webhookErrorHandler)
|
||||
srv.PUT("/webhook/error", webhookErrorHandler)
|
||||
srv.GET("/static/:path", func(c echo.Context) error {
|
||||
s.req = c.Request()
|
||||
path := c.Param("path")
|
||||
if strings.Contains(path, "teststore") {
|
||||
return c.Attachment(fmt.Sprintf("%s/%s/%s", workdir, s.req.Header.Get("Gotenberg-Trace"), filepath.Base(path)), filepath.Base(path))
|
||||
}
|
||||
return c.Attachment(fmt.Sprintf("%s/%s", wd, path), filepath.Base(path))
|
||||
})
|
||||
srv.GET("/html/:path", func(c echo.Context) error {
|
||||
s.req = c.Request()
|
||||
path := fmt.Sprintf("%s/%s", wd, c.Param("path"))
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, fmt.Sprintf("open file %q: %s", path, err))
|
||||
}
|
||||
defer f.Close()
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return c.String(http.StatusInternalServerError, fmt.Sprintf("read file %q: %s", path, err))
|
||||
}
|
||||
return c.HTML(http.StatusOK, string(b))
|
||||
})
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *server) start(ctx context.Context) (int, error) {
|
||||
// #nosec
|
||||
ln, err := net.Listen("tcp", "0.0.0.0:0")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create listener: %w", err)
|
||||
}
|
||||
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
go func() {
|
||||
s.srv.Listener = ln
|
||||
err = s.srv.Start("")
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
godog.Log(ctx, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func (s *server) stop(ctx context.Context) error {
|
||||
close(s.errChan)
|
||||
return s.srv.Shutdown(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user