diff --git a/.github/actions/build-push/action.yml b/.github/actions/build-test-push/action.yml similarity index 56% rename from .github/actions/build-push/action.yml rename to .github/actions/build-test-push/action.yml index af94b040..019dc911 100644 --- a/.github/actions/build-push/action.yml +++ b/.github/actions/build-test-push/action.yml @@ -1,5 +1,5 @@ -name: Build and Push -description: Build and push Docker images +name: Build Test Push +description: Build, test and push Docker images for a given platform author: Julien Neuhart inputs: @@ -19,6 +19,9 @@ inputs: version: description: Gotenberg version required: true + skip_integrations_tests: + description: Define whether to skip integration testing + default: false alternate_repository: description: Alternate repository to push the tags to dry_run: @@ -27,10 +30,10 @@ inputs: outputs: tags: description: Comma separated list of tag - value: ${{ steps.build_push.outputs.tags }} + value: ${{ steps.build.outputs.tags }} tags_cloud_run: description: Comma separated list of Cloud Run tags (linux/amd64 only) - value: ${{ steps.build_push.outputs.tags_cloud_run }} + value: ${{ steps.build.outputs.tags_cloud_run }} runs: using: composite @@ -50,18 +53,35 @@ runs: username: ${{ inputs.docker_hub_username }} password: ${{ inputs.docker_hub_password }} - - name: Build and push ${{ inputs.platform }} - id: build_push + - name: Build ${{ inputs.platform }} + id: build shell: bash run: | - .github/actions/build-push/build-push.sh \ + .github/actions/build-test-push/build.sh \ --version "${{ inputs.version }}" \ --platform "${{ inputs.platform }}" \ --alternate-repository "${{ inputs.alternate_repository }}" \ --dry-run "${{ inputs.dry_run }}" + - name: Run integration tests + if: ${{ inputs.skip_integrations_tests != 'true' }} + shell: bash + run: | + .github/actions/build-test-push/test.sh \ + --version "${{ inputs.version }}" \ + --platform "${{ inputs.platform }}" \ + --alternate-repository "${{ inputs.alternate_repository }}" \ + --dry-run "${{ inputs.dry_run }}" + + - name: Push + shell: bash + run: | + .github/actions/build-test-push/push.sh \ + --tags "${{ steps.build.outputs.tags }}" \ + --dry-run "${{ inputs.dry_run }}" + - name: Outputs shell: bash run: | - echo "tags=${{ steps.build_push.outputs.tags }}" - echo "tags_cloud_run=${{ steps.build_push.outputs.tags_cloud_run }}" + echo "tags=${{ steps.build.outputs.tags }}" + echo "tags_cloud_run=${{ steps.build.outputs.tags_cloud_run }}" diff --git a/.github/actions/build-push/build-push.sh b/.github/actions/build-test-push/build.sh similarity index 91% rename from .github/actions/build-push/build-push.sh rename to .github/actions/build-test-push/build.sh index c83bb028..410d3abd 100755 --- a/.github/actions/build-push/build-push.sh +++ b/.github/actions/build-test-push/build.sh @@ -92,7 +92,7 @@ fi tags_flags=() tags_cloud_run_flags=() -echo "Will push the following tags:" +echo "Will use the following tags:" for tag in "${tags[@]}"; do tags_flags+=("-t" "$tag") echo "- $tag" @@ -103,7 +103,7 @@ for tag in "${tags_cloud_run[@]}"; do done echo -# Build and push images. +# Build images. run_cmd() { local cmd="$1" @@ -126,6 +126,8 @@ join() { echo "$*" } +no_arch_tag="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version" + cmd="docker buildx build \ --build-arg GOLANG_VERSION=$GOLANG_VERSION \ --build-arg GOTENBERG_VERSION=$version \ @@ -134,9 +136,10 @@ cmd="docker buildx build \ --build-arg NOTO_COLOR_EMOJI_VERSION=$NOTO_COLOR_EMOJI_VERSION \ --build-arg PDFTK_VERSION=$PDFTK_VERSION \ --build-arg PDFCPU_VERSION=$PDFCPU_VERSION \ - --push \ --platform $platform \ + --load \ ${tags_flags[*]} \ + -t $no_arch_tag \ -f $DOCKERFILE $DOCKER_BUILD_CONTEXT " run_cmd "$cmd" @@ -149,19 +152,10 @@ if [ "$platform" != "linux/amd64" ]; then exit 0 fi -source_tag_cloud_run="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-${arch[1]}" -cmd="docker pull $source_tag_cloud_run" -run_cmd "$cmd" - -target_tag_cloud_run="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version" -cmd="docker image tag $source_tag_cloud_run $target_tag_cloud_run" -run_cmd "$cmd" - cmd="docker build \ --build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \ --build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \ --build-arg GOTENBERG_VERSION=$version \ - --push \ ${tags_cloud_run_flags[*]} \ -f $DOCKERFILE_CLOUDRUN $DOCKER_BUILD_CONTEXT " diff --git a/.github/actions/build-test-push/push.sh b/.github/actions/build-test-push/push.sh new file mode 100755 index 00000000..599346f1 --- /dev/null +++ b/.github/actions/build-test-push/push.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Exit early. +# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin. +set -e + +# Source dot env file. +source .env + +# Arguments. +tags="" +dry_run="" + +while [[ $# -gt 0 ]]; do + case $1 in + --tags) + tags="$2" + shift 2 + ;; + --dry-run) + dry_run=$2 + shift 2 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +echo "Push tag(s) ๐Ÿ“ฆ" +echo + +echo "Tag(s) to push:" +IFS=',' read -ra tags_to_push <<< "$tags" +for tag in "${tags_to_push[@]}"; do + echo "- $tag" +done + +if [ "$dry_run" = "true" ]; then + echo "๐Ÿšง Dry run" +fi +echo + +# Push tags. +run_cmd() { + local cmd="$1" + + if [ "$dry_run" = "true" ]; then + echo "๐Ÿšง Dry run - would run the following command:" + echo "$cmd" + echo + else + echo "โš™๏ธ Running command:" + echo "$cmd" + eval "$cmd" + echo + fi +} + +for tag in "${tags_to_push[@]}"; do + cmd="docker push $tag" + run_cmd "$cmd" + + echo "โžก๏ธ $tag pushed" + echo +done + +echo "โœ… Done!" +exit 0 diff --git a/.github/actions/build-test-push/test.sh b/.github/actions/build-test-push/test.sh new file mode 100755 index 00000000..ee5eb197 --- /dev/null +++ b/.github/actions/build-test-push/test.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Exit early. +# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin. +set -e + +# Source dot env file. +source .env + +# Arguments. +version="" +platform="" +alternate_repository="" +dry_run="" + +while [[ $# -gt 0 ]]; do + case $1 in + --version) + version="${2//v/}" + shift 2 + ;; + --platform) + platform="$2" + shift 2 + ;; + --alternate-repository) + alternate_repository="$2" + shift 2 + ;; + --dry-run) + dry_run="$2" + shift 2 + ;; + *) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +echo "Integration testing ๐Ÿงช" +echo + +echo "Gotenberg version: $version" +echo "Target platform: $platform" + +repository=$DOCKER_REPOSITORY + +if [ -n "$alternate_repository" ]; then + echo "โš ๏ธ Using $alternate_repository for DOCKER_REPOSITORY" + repository=$alternate_repository +fi + +if [ "$dry_run" = "true" ]; then + echo "๐Ÿšง Dry run" +fi +echo + +# Test image. +run_cmd() { + local cmd="$1" + + if [ "$dry_run" = "true" ]; then + echo "๐Ÿšง Dry run - would run the following command:" + echo "$cmd" + echo + else + echo "โš™๏ธ Running command:" + echo "$cmd" + eval "$cmd" + echo + fi +} + +cmd="make test-integration DOCKER_REPOSITORY=$repository GOTENBERG_VERSION=$version PLATFORM=$platform" +run_cmd "$cmd" + +echo "โœ… Done!" +exit 0 diff --git a/.github/actions/merge/merge.sh b/.github/actions/merge/merge.sh index 951bba22..ef4c0afe 100755 --- a/.github/actions/merge/merge.sh +++ b/.github/actions/merge/merge.sh @@ -49,7 +49,6 @@ fi if [ "$dry_run" = "true" ]; then echo "๐Ÿšง Dry run" fi - echo # Build merge map. diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index ab40aa6e..1ada08b3 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -20,12 +20,13 @@ jobs: - name: Build and push id: build_push - uses: ./.github/actions/build-push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: ${{ github.event.release.tag_name }} + version: pr-${{ github.event.pull_request.number }} platform: linux/amd64 + skip_integrations_tests: true release_386: name: Release linux/386 @@ -39,12 +40,13 @@ jobs: - name: Build and push id: build_push - uses: ./.github/actions/build-push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: ${{ github.event.release.tag_name }} + version: pr-${{ github.event.pull_request.number }} platform: linux/386 + skip_integrations_tests: true release_arm64: name: Release linux/arm64 @@ -58,12 +60,13 @@ jobs: - name: Build and push id: build_push - uses: ./.github/actions/build-push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: ${{ github.event.release.tag_name }} + version: pr-${{ github.event.pull_request.number }} platform: linux/arm64 + skip_integrations_tests: true release_arm_v7: name: Release linux/arm/v7 @@ -77,12 +80,13 @@ jobs: - name: Build and push id: build_push - uses: ./.github/actions/build-push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: ${{ github.event.release.tag_name }} + version: pr-${{ github.event.pull_request.number }} platform: linux/arm/v7 + skip_integrations_tests: true merge_clean_release_tags: needs: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 75980b6b..ab8dc1c5 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,66 +17,74 @@ permissions: jobs: lint: - name: Lint + name: Lint Golang codebase runs-on: ubuntu-latest steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "1.23" - cache: false - - - name: Checkout source code - uses: actions/checkout@v4 + go-version-file: go.mod - name: Run linters uses: golangci/golangci-lint-action@v6 with: version: v1.64.2 - tests: - needs: - - lint - name: Tests + lint-prettier: + name: Lint non-Golang codebase runs-on: ubuntu-latest steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Checkout source code uses: actions/checkout@v4 - - name: Build testing environment - run: make build build-tests + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Install Dependencies + run: npm i + + - name: Run linters + run: make lint-prettier + + test-unit: + needs: + - lint + - lint-prettier + name: Run unit tests + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod - name: Run tests - run: make tests-once - - - name: Upload to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true + run: make test-unit snapshot_amd64: if: github.event_name == 'pull_request' needs: - - tests + - test-unit name: Snapshot linux/amd64 runs-on: ubuntu-latest outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -87,19 +95,19 @@ jobs: snapshot_386: if: github.event_name == 'pull_request' needs: - - tests + - test-unit name: Snapshot linux/386 runs-on: ubuntu-latest outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -110,19 +118,19 @@ jobs: snapshot_arm64: if: github.event_name == 'pull_request' needs: - - tests + - test-unit name: Snapshot linux/arm64 runs-on: ubuntu-24.04-arm outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -133,19 +141,19 @@ jobs: snapshot_arm_v7: if: github.event_name == 'pull_request' needs: - - tests + - test-unit name: Snapshot linux/arm/v7 runs-on: ubuntu-24.04-arm outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -182,90 +190,94 @@ jobs: edge_amd64: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: - - tests + - test-unit name: Edge linux/amd64 runs-on: ubuntu-latest outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: edge + version: pr-${{ github.event.pull_request.number }} platform: linux/amd64 + alternate_repository: snapshot edge_386: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: - - tests + - test-unit name: Edge linux/386 runs-on: ubuntu-latest outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: edge + version: pr-${{ github.event.pull_request.number }} platform: linux/386 + alternate_repository: snapshot edge_arm64: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: - - tests + - test-unit name: Edge linux/arm64 runs-on: ubuntu-24.04-arm outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: edge + version: pr-${{ github.event.pull_request.number }} platform: linux/arm64 + alternate_repository: snapshot edge_arm_v7: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: - - tests + - test-unit name: Edge linux/arm/v7 runs-on: ubuntu-24.04-arm outputs: - tags: ${{ steps.build_push.outputs.tags }} - tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }} + tags: ${{ steps.build_test_push.outputs.tags }} + tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Build and push - id: build_push - uses: ./.github/actions/build-push + - name: Build, test and push + id: build_test_push + uses: ./.github/actions/build-test-push with: docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }} - version: edge + version: pr-${{ github.event.pull_request.number }} platform: linux/arm/v7 + alternate_repository: snapshot merge_clean_edge_tags: needs: diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..ca8f932a --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +23.9.0 diff --git a/.prettierrc b/.prettierrc index 6d1814a3..18106bad 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,4 @@ { - "plugins": ["prettier-plugin-gherkin", "prettier-plugin-sh"] + "plugins": ["prettier-plugin-gherkin", "prettier-plugin-sh"], + "escapeBackslashes": true } diff --git a/Makefile b/Makefile index c3b3576f..e997b419 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,7 @@ include .env .PHONY: help help: ## Show the help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' - -.PHONY: it -it: build build-tests ## Initialize the development environment + @grep -hE '^[A-Za-z0-9_ \-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .PHONY: build build: ## Build the Gotenberg's Docker image @@ -158,52 +155,42 @@ run: ## Start a Gotenberg container --webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \ --webhook-disable=$(WEBHOOK_DISABLE) -.PHONY: build-tests -build-tests: ## Build the tests' Docker image - docker build \ - --build-arg GOLANG_VERSION=$(GOLANG_VERSION) \ - --build-arg DOCKER_REGISTRY=$(DOCKER_REGISTRY) \ - --build-arg DOCKER_REPOSITORY=$(DOCKER_REPOSITORY) \ - --build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \ - --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) \ - -t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \ - -f test/Dockerfile . +.PHONY: test-unit +test-unit: ## Run unit tests + go test -race ./... -.PHONY: tests -tests: ## Start the testing environment - docker run --rm -it \ - -v $(PWD):/tests \ - $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \ - bash +PLATFORM= -.PHONY: tests-once -tests-once: ## Run the tests once (prefer the "tests" command while developing) - docker run --rm \ - -v $(PWD):/tests \ - $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \ - gotest - -.PHONY: tests-integration -tests-integration: ## Run integration tests - go test -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration +.PHONY: test-integration +test-integration: ## Run integration tests + go test -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \ + --gotenberg-docker-repository=$(DOCKER_REPOSITORY) \ + --gotenberg-version=$(GOTENBERG_VERSION) \ + --gotenberg-container-platform=$(PLATFORM) .PHONY: lint -lint: ## Lint codebase. - #golangci-lint run +lint: ## Lint Golang codebase + golangci-lint run + +.PHONY: lint-prettier +lint-prettier: ## Lint non-Golang codebase npx prettier --check . +.PHONY: lint-todo +lint-todo: ## Find TODOs in Golang codebase + golangci-lint run --no-config --disable-all --enable godox + # go install mvdan.cc/gofumpt@latest # go install github.com/daixiang0/gci@latest .PHONY: fmt -fmt: ## Format codebase and "optimize" the dependencies +fmt: ## Format Golang codebase and "optimize" the dependencies gofumpt -l -w . gci write -s standard -s default -s "prefix(github.com/gotenberg/gotenberg/v8)" --skip-generated --skip-vendor --custom-order . - npx prettier --write . go mod tidy -.PHONY: todo -todo: ## Find TODOs in codebase. - golangci-lint run --no-config --disable-all --enable godox +.PHONY: prettify +prettify: ## Format non-Golang codebase + npx prettier --write . # go install golang.org/x/tools/cmd/godoc@latest .PHONY: godoc diff --git a/README.md b/README.md index 2ae1eb06..8c70fda2 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,8 @@

Total downloads (gotenberg/gotenberg) Total downloads (thecodingmachine/gotenberg) -
Continuous Integration Go Reference - Code coverage

gotenberg%2Fgotenberg | Trendshift diff --git a/go.mod b/go.mod index 687ccd1e..e9157c5e 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/alexliesenfeld/health v0.8.0 github.com/andybalholm/brotli v1.1.1 // indirect github.com/barasher/go-exiftool v1.10.0 - github.com/chromedp/cdproto v0.0.0-20250311215558-29dfcc2791de - github.com/chromedp/chromedp v0.13.1 + github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 + github.com/chromedp/chromedp v0.13.3 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 @@ -33,47 +33,97 @@ require ( ) require ( + github.com/cucumber/godog v0.15.0 github.com/dlclark/regexp2 v1.11.5 + github.com/docker/docker v28.0.2+incompatible + github.com/docker/go-connections v0.5.0 github.com/mholt/archives v0.1.0 github.com/shirou/gopsutil/v4 v4.25.2 + github.com/testcontainers/testcontainers-go v0.35.0 ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/STARRY-S/zip v0.2.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.0 // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chromedp/sysutil v1.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect + github.com/cucumber/messages/go/v21 v21.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/ebitengine/purego v0.8.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.5 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d // indirect + github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/magiconair/properties v1.8.9 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nwaples/rardecode/v2 v2.1.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/procfs v0.16.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 237ed873..3b96234c 100644 --- a/go.sum +++ b/go.sum @@ -14,9 +14,17 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/STARRY-S/zip v0.2.2 h1:8QeCbIi1Z9U5MgoDARJR1ClbBo9RD46SmVy+dl0woCk= github.com/STARRY-S/zip v0.2.2/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk= @@ -35,24 +43,50 @@ github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chromedp/cdproto v0.0.0-20250311215558-29dfcc2791de h1:tOKSCbB420VENW1Wz10EnSXr4jLnWoq25vnBW4ScmF0= -github.com/chromedp/cdproto v0.0.0-20250311215558-29dfcc2791de/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= -github.com/chromedp/chromedp v0.13.1 h1:FDh9CfaAt0w70gl69Hb69M/xgZrWuppH9AW22aGa+iU= -github.com/chromedp/chromedp v0.13.1/go.mod h1:O3nO4Lno7iLoVX+7GdqQkehhKG7DtLf/zFRyJo0AhXY= +github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs= +github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0= +github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= +github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= +github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -62,10 +96,17 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -75,6 +116,12 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -107,6 +154,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -114,17 +163,31 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= +github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -133,8 +196,13 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= @@ -143,8 +211,10 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d h1:fjMbDVUGsMQiVZnSQsmouYJvMdwsGiDipOZoN66v844= -github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= +github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -153,14 +223,34 @@ github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew= github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= @@ -172,29 +262,46 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= +github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= @@ -210,6 +317,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -217,6 +326,26 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -229,6 +358,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= @@ -257,6 +387,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -271,6 +402,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -287,6 +420,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= @@ -303,10 +437,13 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -355,10 +492,13 @@ golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -385,6 +525,10 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -392,15 +536,21 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/gotenberg/cmd_test.go b/pkg/gotenberg/cmd_test.go deleted file mode 100644 index 1687387c..00000000 --- a/pkg/gotenberg/cmd_test.go +++ /dev/null @@ -1,306 +0,0 @@ -package gotenberg - -import ( - "context" - "testing" - "time" - - "go.uber.org/zap" -) - -func TestCommand(t *testing.T) { - cmd := Command(zap.NewNop(), "foo") - if !cmd.process.SysProcAttr.Setpgid { - t.Error("expected cmd.process.SysProcAttr.Setpgid to be true") - } -} - -func TestCommandContext(t *testing.T) { - tests := []struct { - scenario string - ctx context.Context - expectCommandContextError bool - }{ - { - scenario: "nominal behavior", - ctx: context.Background(), - expectCommandContextError: false, - }, - { - scenario: "nil context", - ctx: nil, - expectCommandContextError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.scenario, func(t *testing.T) { - cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo") - - if err == nil && !cmd.process.SysProcAttr.Setpgid { - t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true") - } - - if !tc.expectCommandContextError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectCommandContextError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestCmd_Start(t *testing.T) { - tests := []struct { - scenario string - cmd *Cmd - expectStartError bool - }{ - { - scenario: "nominal behavior", - cmd: Command(zap.NewNop(), "echo", "Hello", "World"), - expectStartError: false, - }, - { - scenario: "start error", - cmd: Command(zap.NewNop(), "foo"), - expectStartError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.scenario, func(t *testing.T) { - err := tc.cmd.Start() - - if !tc.expectStartError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectStartError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestCmd_Wait(t *testing.T) { - tests := []struct { - scenario string - cmd *Cmd - expectWaitError bool - }{ - { - scenario: "nominal behavior", - cmd: func() *Cmd { - cmd := Command(zap.NewNop(), "echo", "Hello", "World") - err := cmd.Start() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return cmd - }(), - expectWaitError: false, - }, - { - scenario: "wait error", - cmd: Command(zap.NewNop(), "echo", "Hello", "World"), - expectWaitError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.scenario, func(t *testing.T) { - err := tc.cmd.Wait() - - if !tc.expectWaitError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectWaitError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestCmd_Exec(t *testing.T) { - tests := []struct { - scenario string - cmd *Cmd - timeout time.Duration - expectExecError bool - }{ - { - scenario: "nominal behavior", - cmd: func() *Cmd { - cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World") - if err != nil { - t.Fatalf("expected no error from CommandContext(), but got: %v", err) - } - return cmd - }(), - expectExecError: false, - }, - { - scenario: "nil context", - cmd: Command(zap.NewNop(), "echo", "Hello", "World"), - expectExecError: true, - }, - { - scenario: "start error", - cmd: func() *Cmd { - cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return cmd - }(), - expectExecError: true, - }, - { - scenario: "context done", - cmd: Command(zap.NewNop(), "sleep", "2"), - timeout: time.Duration(1) * time.Second, - expectExecError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.scenario, func(t *testing.T) { - if tc.timeout > 0 { - ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout) - defer cancel() - - tc.cmd.ctx = ctx - } - - _, err := tc.cmd.Exec() - - if !tc.expectExecError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectExecError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestCmd_pipeOutput(t *testing.T) { - tests := []struct { - scenario string - cmd *Cmd - run bool - expectPipeOutputError bool - }{ - { - scenario: "nominal behavior", - cmd: Command(zap.NewExample(), "echo", "Hello", "World"), - run: true, - expectPipeOutputError: false, - }, - { - scenario: "no debug, no pipe", - cmd: Command(zap.NewNop(), "echo", "Hello", "World"), - run: false, - expectPipeOutputError: false, - }, - { - scenario: "stdout already piped", - cmd: func() *Cmd { - cmd := Command(zap.NewExample(), "echo", "Hello", "World") - _, err := cmd.process.StdoutPipe() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return cmd - }(), - run: false, - expectPipeOutputError: true, - }, - { - scenario: "stderr already piped", - cmd: func() *Cmd { - cmd := Command(zap.NewExample(), "echo", "Hello", "World") - _, err := cmd.process.StderrPipe() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return cmd - }(), - run: false, - expectPipeOutputError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.scenario, func(t *testing.T) { - err := tc.cmd.pipeOutput() - - if tc.run { - errStart := tc.cmd.process.Start() - if errStart != nil { - t.Fatalf("expected no error but got: %v", err) - } - } - - if !tc.expectPipeOutputError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectPipeOutputError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestCmd_Kill(t *testing.T) { - tests := []struct { - scenario string - cmd *Cmd - }{ - { - scenario: "nominal behavior", - cmd: func() *Cmd { - cmd := Command(zap.NewNop(), "sleep", "60") - err := cmd.process.Start() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return cmd - }(), - }, - { - scenario: "no process", - cmd: &Cmd{logger: zap.NewNop()}, - }, - { - scenario: "process already killed", - cmd: func() *Cmd { - cmd := Command(zap.NewNop(), "sleep", "60") - err := cmd.process.Start() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - err = cmd.Kill() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return cmd - }(), - }, - } - - for _, tc := range tests { - t.Run(tc.scenario, func(t *testing.T) { - err := tc.cmd.Kill() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} diff --git a/pkg/gotenberg/context_test.go b/pkg/gotenberg/context_test.go index 9b48cfd2..f8bc4d6f 100644 --- a/pkg/gotenberg/context_test.go +++ b/pkg/gotenberg/context_test.go @@ -5,23 +5,6 @@ import ( "testing" ) -func TestNewContext(t *testing.T) { - if NewContext(ParsedFlags{}, nil) == nil { - t.Error("expected a non-nil value") - } -} - -func TestContext_ParsedFlags(t *testing.T) { - ctx := NewContext(ParsedFlags{}, nil) - - actual := ctx.ParsedFlags() - expect := ParsedFlags{} - - if actual != expect { - t.Errorf("expected %v but got %v", expect, actual) - } -} - func TestContext_Module(t *testing.T) { for _, tc := range []struct { scenario string diff --git a/pkg/gotenberg/fs_test.go b/pkg/gotenberg/fs_test.go deleted file mode 100644 index ee57d1b9..00000000 --- a/pkg/gotenberg/fs_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package gotenberg - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/google/uuid" -) - -func TestOsMkdirAll_MkdirAll(t *testing.T) { - dirPath, err := NewFileSystem(new(OsMkdirAll)).MkdirAll() - if err != nil { - t.Fatalf("create working directory: %v", err) - } - - err = os.RemoveAll(dirPath) - if err != nil { - t.Fatalf("remove working directory: %v", err) - } -} - -func TestOsPathRename_Rename(t *testing.T) { - dirPath, err := NewFileSystem(new(OsMkdirAll)).MkdirAll() - if err != nil { - t.Fatalf("create working directory: %v", err) - } - - path := "/tests/test/testdata/api/sample1.txt" - copyPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString())) - - in, err := os.Open(path) - if err != nil { - t.Fatalf("open file: %v", err) - } - - defer func() { - err := in.Close() - if err != nil { - t.Fatalf("close file: %v", err) - } - }() - - out, err := os.Create(copyPath) - if err != nil { - t.Fatalf("create new file: %v", err) - } - - defer func() { - err := out.Close() - if err != nil { - t.Fatalf("close new file: %v", err) - } - }() - - _, err = io.Copy(out, in) - if err != nil { - t.Fatalf("copy file to new file: %v", err) - } - - rename := new(OsPathRename) - newPath := filepath.Join(dirPath, fmt.Sprintf("%s.txt", uuid.NewString())) - - err = rename.Rename(copyPath, newPath) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } - - err = os.RemoveAll(dirPath) - if err != nil { - t.Fatalf("remove working directory: %v", err) - } -} - -func TestFileSystem_WorkingDir(t *testing.T) { - fs := NewFileSystem(new(MkdirAllMock)) - dirName := fs.WorkingDir() - - if dirName == "" { - t.Error("expected directory name but got empty string") - } -} - -func TestFileSystem_WorkingDirPath(t *testing.T) { - fs := NewFileSystem(new(MkdirAllMock)) - expectedPath := fmt.Sprintf("%s/%s", os.TempDir(), fs.WorkingDir()) - - if fs.WorkingDirPath() != expectedPath { - t.Errorf("expected path '%s' but got '%s'", expectedPath, fs.WorkingDirPath()) - } -} - -func TestFileSystem_NewDirPath(t *testing.T) { - fs := NewFileSystem(new(MkdirAllMock)) - newDir := fs.NewDirPath() - expectedPrefix := fs.WorkingDirPath() - - if !strings.HasPrefix(newDir, expectedPrefix) { - t.Errorf("expected new directory to start with '%s' but got '%s'", expectedPrefix, newDir) - } -} - -func TestFileSystem_MkdirAll(t *testing.T) { - for _, tc := range []struct { - scenario string - mkdirAll MkdirAll - expectError bool - }{ - { - scenario: "error", - mkdirAll: &MkdirAllMock{ - MkdirAllMock: func(path string, perm os.FileMode) error { - return errors.New("foo") - }, - }, - expectError: true, - }, - { - scenario: "success", - mkdirAll: &MkdirAllMock{ - MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - fs := NewFileSystem(tc.mkdirAll) - - _, err := fs.MkdirAll() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} diff --git a/pkg/gotenberg/gc_test.go b/pkg/gotenberg/gc_test.go index c8a4c09e..2124b0dd 100644 --- a/pkg/gotenberg/gc_test.go +++ b/pkg/gotenberg/gc_test.go @@ -3,6 +3,7 @@ package gotenberg import ( "fmt" "os" + "path" "testing" "time" @@ -51,7 +52,7 @@ func TestGarbageCollect(t *testing.T) { return path }(), - includeSubstr: []string{"foo", fmt.Sprintf("%s/a_directory/a_bar_file", os.TempDir())}, + includeSubstr: []string{"foo", path.Join(os.TempDir(), "/a_directory/a_bar_file")}, expectError: false, expectExists: []string{"a_baz_file"}, expectNotExists: []string{"a_foo_file", "a_bar_file"}, diff --git a/pkg/gotenberg/logging_test.go b/pkg/gotenberg/logging_test.go deleted file mode 100644 index cc8f203d..00000000 --- a/pkg/gotenberg/logging_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package gotenberg - -import ( - "testing" - - "go.uber.org/zap" -) - -func TestLeveledLogger_Error(t *testing.T) { - NewLeveledLogger(zap.NewNop()).Error("foo") -} - -func TestLeveledLogger_Warn(t *testing.T) { - NewLeveledLogger(zap.NewNop()).Warn("foo") -} - -func TestLeveledLogger_Info(t *testing.T) { - NewLeveledLogger(zap.NewNop()).Info("foo") -} - -func TestLeveledLogger_Debug(t *testing.T) { - NewLeveledLogger(zap.NewNop()).Debug("foo") -} diff --git a/pkg/gotenberg/mocks_test.go b/pkg/gotenberg/mocks_test.go deleted file mode 100644 index d813499a..00000000 --- a/pkg/gotenberg/mocks_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package gotenberg - -import ( - "context" - "os" - "testing" - - "go.uber.org/zap" -) - -func TestModuleMock(t *testing.T) { - mock := &ModuleMock{ - DescriptorMock: func() ModuleDescriptor { - return ModuleDescriptor{ID: "foo", New: func() Module { - return nil - }} - }, - } - - if mock.Descriptor().ID != "foo" { - t.Errorf("expected ID '%s' from ModuleMock.Descriptor, but got '%s'", "foo", mock.Descriptor().ID) - } -} - -func TestProvisionerMock(t *testing.T) { - mock := &ProvisionerMock{ - ProvisionMock: func(*Context) error { - return nil - }, - } - - err := mock.Provision(&Context{}) - if err != nil { - t.Errorf("expected no error from ProvisionerMock.Provision, but got: %v", err) - } -} - -func TestValidatorMock(t *testing.T) { - mock := &ValidatorMock{ - ValidateMock: func() error { - return nil - }, - } - - err := mock.Validate() - if err != nil { - t.Errorf("expected no error from ValidatorMock.Validate, but got: %v", err) - } -} - -func TestDebuggableMock(t *testing.T) { - mock := &DebuggableMock{ - DebugMock: func() map[string]interface{} { - return map[string]interface{}{ - "foo": "bar", - } - }, - } - - d := mock.Debug() - if d == nil { - t.Errorf("expected debug data, but got nil") - } -} - -func TestPDFEngineMock(t *testing.T) { - mock := &PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - SplitMock: func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) { - return nil, nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error { - return nil - }, - ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) { - return nil, nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - } - - err := mock.Merge(context.Background(), zap.NewNop(), nil, "") - if err != nil { - t.Errorf("expected no error from PdfEngineMock.Merge, but got: %v", err) - } - - _, err = mock.Split(context.Background(), zap.NewNop(), SplitMode{}, "", "") - if err != nil { - t.Errorf("expected no error from PdfEngineMock.Split, but got: %v", err) - } - - err = mock.Flatten(context.Background(), zap.NewNop(), "") - if err != nil { - t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err) - } - - err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "") - if err != nil { - t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err) - } - - _, err = mock.ReadMetadata(context.Background(), zap.NewNop(), "") - if err != nil { - t.Errorf("expected no error from PdfEngineMock.ReadMetadata, but got: %v", err) - } - - err = mock.WriteMetadata(context.Background(), zap.NewNop(), map[string]interface{}{}, "") - if err != nil { - t.Errorf("expected no error from PdfEngineMock.WriteMetadata but got: %v", err) - } -} - -func TestPDFEngineProviderMock(t *testing.T) { - mock := &PdfEngineProviderMock{ - PdfEngineMock: func() (PdfEngine, error) { - return new(PdfEngineMock), nil - }, - } - - _, err := mock.PdfEngine() - if err != nil { - t.Errorf("expected no error from PdfEngineProviderMock.PdfEngine, but got: %v", err) - } -} - -func TestProcessMock(t *testing.T) { - mock := &ProcessMock{ - StartMock: func(logger *zap.Logger) error { - return nil - }, - StopMock: func(logger *zap.Logger) error { - return nil - }, - HealthyMock: func(logger *zap.Logger) bool { - return true - }, - } - - err := mock.Start(zap.NewNop()) - if err != nil { - t.Errorf("expected no error from ProcessMock.Start, but got: %v", err) - } - - err = mock.Stop(zap.NewNop()) - if err != nil { - t.Errorf("expected no error from ProcessMock.Stop, but got: %v", err) - } - - healthy := mock.Healthy(zap.NewNop()) - if !healthy { - t.Error("expected true from ProcessMock.Healthy, but got false") - } -} - -func TestProcessSupervisorMock(t *testing.T) { - mock := &ProcessSupervisorMock{ - LaunchMock: func() error { - return nil - }, - ShutdownMock: func() error { - return nil - }, - HealthyMock: func() bool { - return true - }, - RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error { - return nil - }, - ReqQueueSizeMock: func() int64 { - return 0 - }, - RestartsCountMock: func() int64 { - return 0 - }, - } - - err := mock.Launch() - if err != nil { - t.Errorf("expected no error from ProcessSupervisorMock.Launch, but got: %v", err) - } - - err = mock.Shutdown() - if err != nil { - t.Errorf("expected no error from ProcessSupervisorMock.Shutdown, but got: %v", err) - } - - healthy := mock.Healthy() - if !healthy { - t.Error("expected true from ProcessSupervisorMock.Healthy, but got false") - } - - err = mock.Run(context.TODO(), zap.NewNop(), nil) - if err != nil { - t.Errorf("expected no error from ProcessSupervisorMock.Run, but got: %v", err) - } - - size := mock.ReqQueueSize() - if size != 0 { - t.Errorf("expected 0 from ProcessSupervisorMock.ReqQueueSize, but got: %d", size) - } - - restarts := mock.RestartsCount() - if restarts != 0 { - t.Errorf("expected 0 from ProcessSupervisorMock.RestartsCount, but got: %d", restarts) - } -} - -func TestLoggerProviderMock(t *testing.T) { - mock := &LoggerProviderMock{ - LoggerMock: func(mod Module) (*zap.Logger, error) { - return nil, nil - }, - } - - _, err := mock.Logger(new(ModuleMock)) - if err != nil { - t.Errorf("expected no error from LoggerProviderMock.Logger, but got: %v", err) - } -} - -func TestMetricsProviderMock(t *testing.T) { - mock := &MetricsProviderMock{ - MetricsMock: func() ([]Metric, error) { - return nil, nil - }, - } - - _, err := mock.Metrics() - if err != nil { - t.Errorf("expected no error from MetricsProviderMock.Metrics, but got: %v", err) - } -} - -func TestMkdirAllMock(t *testing.T) { - mock := &MkdirAllMock{ - MkdirAllMock: func(dir string, perm os.FileMode) error { - return nil - }, - } - - err := mock.MkdirAll("/foo", 0o755) - if err != nil { - t.Errorf("expected no error from MkdirAllMock.MkdirAll, but got: %v", err) - } -} - -func TestPathRenameMock(t *testing.T) { - mock := &PathRenameMock{ - RenameMock: func(oldpath, newpath string) error { - return nil - }, - } - - err := mock.Rename("", "") - if err != nil { - t.Errorf("expected no error from PathRenameMock.Rename, but got: %v", err) - } -} diff --git a/pkg/modules/api/api_test.go b/pkg/modules/api/api_test.go deleted file mode 100644 index 465adce3..00000000 --- a/pkg/modules/api/api_test.go +++ /dev/null @@ -1,1033 +0,0 @@ -package api - -import ( - "bytes" - "context" - "errors" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "reflect" - "testing" - "time" - - "github.com/alexliesenfeld/health" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestApi_Descriptor(t *testing.T) { - descriptor := new(Api).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(Api)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestApi_Provision(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - setEnv func() - expectPort int - expectMiddlewares []Middleware - expectError bool - }{ - { - scenario: "port from env: non-existing environment variable", - ctx: func() *gotenberg.Context { - fs := new(Api).Descriptor().FlagSet - err := fs.Parse([]string{"--api-port-from-env=FOO"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - nil, - ) - }(), - expectError: true, - }, - { - scenario: "port from env: invalid environment variable value", - ctx: func() *gotenberg.Context { - fs := new(Api).Descriptor().FlagSet - err := fs.Parse([]string{"--api-port-from-env=PORT"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - nil, - ) - }(), - setEnv: func() { - err := os.Setenv("PORT", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }, - expectError: true, - }, - { - scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_USERNAME environment variable", - ctx: func() *gotenberg.Context { - fs := new(Api).Descriptor().FlagSet - err := fs.Parse([]string{"--api-enable-basic-auth=true"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - nil, - ) - }(), - expectError: true, - }, - { - scenario: "basic auth: non-existing GOTENBERG_API_BASIC_AUTH_PASSWORD environment variable", - ctx: func() *gotenberg.Context { - fs := new(Api).Descriptor().FlagSet - err := fs.Parse([]string{"--api-enable-basic-auth=true"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - nil, - ) - }(), - setEnv: func() { - err := os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }, - expectError: true, - }, - { - scenario: "no valid routers", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - RouterMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ValidateMock = func() error { - return errors.New("foo") - } - mod.RoutesMock = func() ([]Route, error) { - return nil, nil - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "cannot retrieve routes from router", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - RouterMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.RoutesMock = func() ([]Route, error) { - return nil, errors.New("foo") - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no valid middleware providers", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - MiddlewareProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ValidateMock = func() error { - return errors.New("foo") - } - mod.MiddlewaresMock = func() ([]Middleware, error) { - return nil, nil - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "cannot retrieve middlewares from middleware provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - MiddlewareProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.MiddlewaresMock = func() ([]Middleware, error) { - return nil, errors.New("foo") - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no valid health checkers", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - HealthCheckerMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ValidateMock = func() error { - return errors.New("foo") - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "cannot retrieve health checks from health checker", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - HealthCheckerMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ChecksMock = func() ([]health.CheckerOption, error) { - return nil, errors.New("foo") - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no logger provider", - ctx: func() *gotenberg.Context { - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{}, - ) - }(), - expectError: true, - }, - { - scenario: "no logger from logger provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return nil, errors.New("foo") - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "success", - ctx: func() *gotenberg.Context { - mod1 := &struct { - gotenberg.ModuleMock - RouterMock - }{} - mod1.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }} - } - mod1.RoutesMock = func() ([]Route, error) { - return []Route{{}}, nil - } - - mod2 := &struct { - gotenberg.ModuleMock - MiddlewareProviderMock - }{} - mod2.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }} - } - mod2.MiddlewaresMock = func() ([]Middleware, error) { - return []Middleware{ - { - Priority: VeryLowPriority, - }, - { - Priority: LowPriority, - }, - { - Priority: MediumPriority, - }, - { - Priority: HighPriority, - }, - { - Priority: VeryHighPriority, - }, - }, nil - } - - mod3 := &struct { - gotenberg.ModuleMock - HealthCheckerMock - }{} - mod3.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return mod3 }} - } - mod3.ChecksMock = func() ([]health.CheckerOption, error) { - return []health.CheckerOption{health.WithDisabledAutostart()}, nil - } - mod3.ReadyMock = func() error { - return nil - } - - mod4 := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - }{} - mod4.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "qux", New: func() gotenberg.Module { return mod4 }} - } - mod4.LoggerMock = func(_ gotenberg.Module) (*zap.Logger, error) { - return zap.NewNop(), nil - } - - fs := new(Api).Descriptor().FlagSet - err := fs.Parse([]string{"--api-port-from-env=PORT", "--api-enable-basic-auth=true"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - []gotenberg.ModuleDescriptor{ - mod1.Descriptor(), - mod2.Descriptor(), - mod3.Descriptor(), - mod4.Descriptor(), - }, - ) - }(), - setEnv: func() { - err := os.Setenv("PORT", "1337") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - err = os.Setenv("GOTENBERG_API_BASIC_AUTH_USERNAME", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - err = os.Setenv("GOTENBERG_API_BASIC_AUTH_PASSWORD", "bar") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }, - expectPort: 1337, - expectMiddlewares: []Middleware{ - { - Priority: VeryHighPriority, - }, - { - Priority: HighPriority, - }, - { - Priority: MediumPriority, - }, - { - Priority: LowPriority, - }, - { - Priority: VeryLowPriority, - }, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - if tc.setEnv != nil { - tc.setEnv() - } - - mod := new(Api) - err := mod.Provision(tc.ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectPort != 0 && mod.port != tc.expectPort { - t.Errorf("expected port %d but got %d", tc.expectPort, mod.port) - } - - if !reflect.DeepEqual(mod.externalMiddlewares, tc.expectMiddlewares) { - t.Errorf("expected %+v, but got: %+v", tc.expectMiddlewares, mod.externalMiddlewares) - } - }) - } -} - -func TestApi_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - port int - bindIp string - tlsCertFile string - tlsKeyFile string - rootPath string - traceHeader string - routes []Route - middlewares []Middleware - expectError bool - }{ - { - scenario: "invalid port (< 1)", - port: 0, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid port (> 65535)", - port: 65536, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid IP", - port: 10, - bindIp: "foo", - rootPath: "/foo/", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid TLS files: only cert file provided", - port: 10, - bindIp: "127.0.0.1", - tlsCertFile: "cert.pem", - rootPath: "/foo/", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid TLS files: only key file provided", - port: 10, - bindIp: "127.0.0.1", - tlsKeyFile: "key.pem", - rootPath: "/foo/", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid root path: missing / prefix", - port: 10, - bindIp: "127.0.0.1", - rootPath: "foo/", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid root path: missing / suffix", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo", - traceHeader: "foo", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid trace header", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "", - routes: nil, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid route: empty path", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Path: "", - }, - }, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid route: missing / prefix in path", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Path: "foo", - }, - }, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid multipart route: no /forms prefix in path", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Path: "/foo", - IsMultipart: true, - }, - }, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid route: no method", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Path: "/foo", - Method: "", - }, - }, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid route: nil handler", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Method: http.MethodPost, - Path: "/foo", - Handler: nil, - }, - }, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid route: path already existing", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Method: http.MethodPost, - Path: "/foo", - Handler: func(_ echo.Context) error { return nil }, - }, - { - Method: http.MethodPost, - Path: "/foo", - Handler: func(_ echo.Context) error { return nil }, - }, - }, - middlewares: nil, - expectError: true, - }, - { - scenario: "invalid middleware: nil handler", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: nil, - middlewares: []Middleware{ - { - Priority: HighPriority, - Handler: nil, - }, - }, - expectError: true, - }, - { - scenario: "success", - port: 10, - bindIp: "127.0.0.1", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Method: http.MethodGet, - Path: "/foo", - Handler: func(_ echo.Context) error { return nil }, - }, - { - Method: http.MethodGet, - Path: "/forms/foo", - Handler: func(_ echo.Context) error { return nil }, - IsMultipart: true, - }, - }, - middlewares: []Middleware{ - { - Priority: HighPriority, - Handler: func() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - return next(c) - } - } - }(), - }, - }, - }, - { - scenario: "success with TLS", - port: 10, - tlsCertFile: "cert.pem", - tlsKeyFile: "key.pem", - rootPath: "/foo/", - traceHeader: "foo", - routes: []Route{ - { - Method: http.MethodGet, - Path: "/foo", - Handler: func(_ echo.Context) error { return nil }, - }, - { - Method: http.MethodGet, - Path: "/forms/foo", - Handler: func(_ echo.Context) error { return nil }, - IsMultipart: true, - }, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := Api{ - port: tc.port, - bindIp: tc.bindIp, - tlsCertFile: tc.tlsCertFile, - tlsKeyFile: tc.tlsKeyFile, - rootPath: tc.rootPath, - traceHeader: tc.traceHeader, - routes: tc.routes, - externalMiddlewares: tc.middlewares, - } - - err := mod.Validate() - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestApi_Start(t *testing.T) { - for _, tc := range []struct { - scenario string - readyFn []func() error - tlsCertFile string - tlsKeyFile string - expectError bool - }{ - { - scenario: "at least one module not ready", - readyFn: []func() error{ - func() error { return nil }, - func() error { return errors.New("not ready") }, - }, - expectError: true, - }, - { - scenario: "success", - readyFn: []func() error{ - func() error { return nil }, - func() error { return nil }, - }, - expectError: false, - }, - { - scenario: "success with TLS", - readyFn: []func() error{ - func() error { return nil }, - func() error { return nil }, - }, - tlsCertFile: "/tests/test/testdata/api/cert.pem", - tlsKeyFile: "/tests/test/testdata/api/key.pem", - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Api) - mod.port = 3000 - mod.tlsCertFile = tc.tlsCertFile - mod.tlsKeyFile = tc.tlsKeyFile - mod.startTimeout = time.Duration(30) * time.Second - mod.rootPath = "/" - mod.basicAuthUsername = "foo" - mod.basicAuthPassword = "bar" - mod.disableHealthCheckLogging = true - mod.enableDebugRoute = true - mod.routes = []Route{ - { - Method: http.MethodPost, - Path: "/forms/foo", - IsMultipart: true, - DisableLogging: true, - Handler: func(c echo.Context) error { - ctx := c.Get("context").(*Context) - ctx.outputPaths = []string{ - "/tests/test/testdata/api/sample1.txt", - } - - return nil - }, - }, - { - Method: http.MethodPost, - Path: "/forms/bar", - IsMultipart: true, - Handler: func(_ echo.Context) error { return errors.New("foo") }, - }, - } - mod.externalMiddlewares = []Middleware{ - { - Stack: PreRouterStack, - Handler: func() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - return next(c) - } - } - }(), - }, - { - Stack: MultipartStack, - Handler: func() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - return next(c) - } - } - }(), - }, - { - Stack: DefaultStack, - Handler: func() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - return next(c) - } - } - }(), - }, - { - Handler: func() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - return next(c) - } - } - }(), - }, - } - mod.readyFn = tc.readyFn - mod.fs = gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - mod.logger = zap.NewNop() - - err := mod.Start() - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectError { - return - } - - // root request. - recorder := httptest.NewRecorder() - rootRequest := httptest.NewRequest(http.MethodGet, "/", nil) - rootRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword) - mod.srv.ServeHTTP(recorder, rootRequest) - if recorder.Code != http.StatusOK { - t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) - } - - // favicon request. - recorder = httptest.NewRecorder() - faviconRequest := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil) - faviconRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword) - mod.srv.ServeHTTP(recorder, faviconRequest) - if recorder.Code != http.StatusNoContent { - t.Errorf("expected %d status code but got %d", http.StatusNoContent, recorder.Code) - } - - // health requests. - recorder = httptest.NewRecorder() - healthGetRequest := httptest.NewRequest(http.MethodGet, "/health", nil) - mod.srv.ServeHTTP(recorder, healthGetRequest) - if recorder.Code != http.StatusOK { - t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) - } - - recorder = httptest.NewRecorder() - healthHeadRequest := httptest.NewRequest(http.MethodHead, "/health", nil) - mod.srv.ServeHTTP(recorder, healthHeadRequest) - if recorder.Code != http.StatusOK { - t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) - } - - // version request. - recorder = httptest.NewRecorder() - versionRequest := httptest.NewRequest(http.MethodGet, "/version", nil) - versionRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword) - mod.srv.ServeHTTP(recorder, versionRequest) - if recorder.Code != http.StatusOK { - t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) - } - - // debug request. - recorder = httptest.NewRecorder() - debugRequest := httptest.NewRequest(http.MethodGet, "/debug", nil) - debugRequest.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword) - mod.srv.ServeHTTP(recorder, debugRequest) - if recorder.Code != http.StatusOK { - t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) - } - - // "multipart/form-data" request. - multipartRequest := func(url string) *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - - err := writer.WriteField("foo", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - part, err := writer.CreateFormFile("foo.txt", "foo.txt") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - _, err = part.Write([]byte("foo")) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - req := httptest.NewRequest(http.MethodPost, url, body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - req.SetBasicAuth(mod.basicAuthUsername, mod.basicAuthPassword) - - return req - } - - recorder = httptest.NewRecorder() - mod.srv.ServeHTTP(recorder, multipartRequest("/forms/foo")) - - if recorder.Code != http.StatusOK { - t.Errorf("expected %d status code but got %d", http.StatusOK, recorder.Code) - } - - recorder = httptest.NewRecorder() - mod.srv.ServeHTTP(recorder, multipartRequest("/forms/bar")) - - if recorder.Code != http.StatusInternalServerError { - t.Errorf("expected %d status code but got %d", http.StatusInternalServerError, recorder.Code) - } - - err = mod.srv.Shutdown(context.TODO()) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } - }) - } -} - -func TestApi_StartupMessage(t *testing.T) { - for _, tc := range []struct { - scenario string - port int - bindIp string - expectMessage string - }{ - { - scenario: "no custom IP", - port: 3000, - bindIp: "", - expectMessage: "server started on [::]:3000", - }, - { - scenario: "custom IP", - port: 3000, - bindIp: "127.0.0.1", - expectMessage: "server started on 127.0.0.1:3000", - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := Api{ - port: tc.port, - bindIp: tc.bindIp, - } - - actual := mod.StartupMessage() - if actual != tc.expectMessage { - t.Errorf("expected '%s' but got '%s'", tc.expectMessage, actual) - } - }) - } -} - -func TestApi_Stop(t *testing.T) { - mod := &Api{ - port: 3000, - routes: []Route{ - { - Method: http.MethodGet, - Path: "/foo", - Handler: func(_ echo.Context) error { return nil }, - }, - }, - logger: zap.NewNop(), - } - - err := mod.Start() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = mod.Stop(context.TODO()) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} diff --git a/pkg/modules/api/context_test.go b/pkg/modules/api/context_test.go deleted file mode 100644 index b69bc0a5..00000000 --- a/pkg/modules/api/context_test.go +++ /dev/null @@ -1,837 +0,0 @@ -package api - -import ( - "bytes" - "context" - "errors" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "reflect" - "strings" - "testing" - "time" - - "github.com/dlclark/regexp2" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestNewContext(t *testing.T) { - defaultAllowList, err := regexp2.Compile("", 0) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - defaultDenyList, err := regexp2.Compile("", 0) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - defaultDownloadFromCfg := downloadFromConfig{ - allowList: defaultAllowList, - denyList: defaultDenyList, - maxRetry: 1, - disable: false, - } - - for _, tc := range []struct { - scenario string - request *http.Request - bodyLimit int64 - downloadFromCfg downloadFromConfig - downloadFromSrv *echo.Echo - expectContext *Context - expectError bool - expectHttpError bool - expectHttpStatus int - }{ - { - scenario: "http.ErrNotMultipart", - request: httptest.NewRequest(http.MethodPost, "/", nil), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusUnsupportedMediaType, - }, - { - scenario: "http.ErrMissingBoundary", - request: func() *http.Request { - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set(echo.HeaderContentType, echo.MIMEMultipartForm) - return req - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusUnsupportedMediaType, - }, - { - scenario: "malformed body", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("foo", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "request entity too large: form values", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("key", "value") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - bodyLimit: 1, - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusRequestEntityTooLarge, - }, - { - scenario: "request entity too large: downloadFrom", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - bodyLimit: 45, // form values = 44 bytes. - downloadFromSrv: func() *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.GET("/", func(c echo.Context) error { - c.Response().Header().Set(echo.HeaderContentDisposition, `attachment; filename="bar.txt"`) - c.Response().Header().Set(echo.HeaderContentType, "text/plain") - return c.String(http.StatusOK, http.StatusText(http.StatusOK)) - }) - return srv - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusRequestEntityTooLarge, - }, - { - scenario: "request entity too large: form files", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - part, err := writer.CreateFormFile("foo.txt", "foo.txt") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - _, err = part.Write([]byte("foo")) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - bodyLimit: 1, - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusRequestEntityTooLarge, - }, - { - scenario: "invalid downloadFrom form field: cannot unmarshal", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid downloadFrom form field: no URL", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid downloadFrom form field: filtered URL", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"https://foo.bar"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromCfg: func() downloadFromConfig { - denyList, err := regexp2.Compile("https://foo.bar", 0) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return downloadFromConfig{allowList: defaultAllowList, denyList: denyList, maxRetry: 1, disable: false} - }(), - expectError: true, - }, - { - scenario: "invalid downloadFrom form field: unreachable URL", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid downloadFrom form field: invalid status code", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromSrv: func() *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.GET("/", func(c echo.Context) error { - return c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) - }) - return srv - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid downloadFrom form field: no 'Content-Disposition' header", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromSrv: func() *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.GET("/", func(c echo.Context) error { - return c.String(http.StatusOK, http.StatusText(http.StatusOK)) - }) - return srv - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid downloadFrom form field: malformed 'Content-Disposition' header", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromSrv: func() *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.GET("/", func(c echo.Context) error { - c.Response().Header().Set(echo.HeaderContentDisposition, ";;") - return c.String(http.StatusOK, http.StatusText(http.StatusOK)) - }) - return srv - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid downloadFrom form field: no filename parameter in 'Content-Disposition' header", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/"}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromSrv: func() *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.GET("/", func(c echo.Context) error { - c.Response().Header().Set(echo.HeaderContentDisposition, "inline;") - return c.String(http.StatusOK, http.StatusText(http.StatusOK)) - }) - return srv - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "success", - request: func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - err := writer.WriteField("foo", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - part, err := writer.CreateFormFile("foo.txt", "foo.txt") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - _, err = part.Write([]byte("foo")) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - err = writer.WriteField("downloadFrom", `[{"url":"http://localhost:80/","extraHttpHeaders":{"X-Foo":"Bar"}}]`) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - return req - }(), - downloadFromSrv: func() *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.GET("/", func(c echo.Context) error { - if c.Request().Header.Get("User-Agent") != "Gotenberg" { - t.Fatalf("expected 'Gotenberg' from header 'User-Agent', but got '%s'", c.Request().Header.Get("User-Agent")) - } - if c.Request().Header.Get("X-Foo") != "Bar" { - t.Fatalf("expected 'Bar' from header 'X-Foo', but got '%s'", c.Request().Header.Get("X-Foo")) - } - if c.Request().Header.Get("Gotenberg-Trace") != "123" { - t.Fatalf("expected '123' from header 'Gotenberg-Trace', but got '%s'", c.Request().Header.Get("Gotenberg-Trace")) - } - c.Response().Header().Set(echo.HeaderContentDisposition, `attachment; filename="bar.txt"`) - c.Response().Header().Set(echo.HeaderContentType, "text/plain") - return c.String(http.StatusOK, http.StatusText(http.StatusOK)) - }) - return srv - }(), - downloadFromCfg: defaultDownloadFromCfg, - expectContext: &Context{ - values: map[string][]string{ - "foo": {"foo"}, - "downloadFrom": { - `[{"url":"http://localhost:80/","extraHttpHeaders":{"X-Foo":"Bar"}}]`, - }, - }, - files: map[string]string{ - "foo.txt": "foo.txt", - "bar.txt": "bar.txt", // downloadFrom. - }, - }, - expectError: false, - expectHttpError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - if tc.downloadFromSrv != nil { - go func() { - err := tc.downloadFromSrv.Start(":80") - if !errors.Is(err, http.ErrServerClosed) { - t.Error(err) - return - } - }() - defer func() { - err := tc.downloadFromSrv.Shutdown(context.TODO()) - if err != nil { - t.Error(err) - } - }() - } - - handler := func(c echo.Context) error { - ctx, cancel, err := newContext(c, zap.NewNop(), gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, tc.bodyLimit, tc.downloadFromCfg, "Gotenberg-Trace", "123") - defer cancel() - // Context already cancelled. - defer cancel() - - if err != nil { - return err - } - - if tc.expectContext != nil { - if !reflect.DeepEqual(tc.expectContext.values, ctx.values) { - t.Fatalf("expected context.values to be %v but got %v", tc.expectContext.values, ctx.values) - } - if len(tc.expectContext.files) != len(ctx.files) { - t.Fatalf("expected context.files to contain %d items but got %d", len(tc.expectContext.files), len(ctx.files)) - } - for key, value := range tc.expectContext.files { - if !strings.HasSuffix(ctx.files[key], value) { - t.Fatalf("expected context.files to contain '%s' but got '%s'", value, ctx.files[key]) - } - } - } - - return nil - } - - recorder := httptest.NewRecorder() - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(tc.request, recorder) - err := handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - }) - } -} - -func TestContext_Request(t *testing.T) { - request := httptest.NewRequest(http.MethodPost, "/", nil) - recorder := httptest.NewRecorder() - c := echo.New().NewContext(request, recorder) - - ctx := &Context{ - echoCtx: c, - } - - if !reflect.DeepEqual(ctx.Request(), c.Request()) { - t.Errorf("expected %v but got %v", ctx.Request(), c.Request()) - } -} - -func TestContext_FormData(t *testing.T) { - ctx := &Context{ - values: map[string][]string{ - "foo": {"foo"}, - }, - files: map[string]string{ - "foo.txt": "/foo.txt", - }, - } - - actual := ctx.FormData() - expect := &FormData{ - values: ctx.values, - files: ctx.files, - } - - if !reflect.DeepEqual(actual, expect) { - t.Errorf("expected %+v but got %+v", expect, actual) - } -} - -func TestContext_CreateSubDirectory(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *Context - expectError bool - }{ - { - scenario: "failure", - ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return errors.New("cannot rename") - }}}, - expectError: true, - }, - { - scenario: "success", - ctx: &Context{mkdirAll: &gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.logger = zap.NewNop() - _, err := tc.ctx.CreateSubDirectory("foo") - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestContext_GeneratePath(t *testing.T) { - ctx := &Context{ - dirPath: "/foo", - } - - path := ctx.GeneratePath(".pdf") - if !strings.HasPrefix(path, ctx.dirPath) { - t.Errorf("expected '%s' to start with '%s'", path, ctx.dirPath) - } -} - -func TestContext_Rename(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *Context - expectError bool - }{ - { - scenario: "failure", - ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return errors.New("cannot rename") - }}}, - expectError: true, - }, - { - scenario: "success", - ctx: &Context{pathRename: &gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.logger = zap.NewNop() - err := tc.ctx.Rename("", "") - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestContext_AddOutputPaths(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *Context - path string - expectCount int - expectError bool - }{ - { - scenario: "ErrContextAlreadyClosed", - ctx: &Context{cancelled: true}, - expectCount: 0, - expectError: true, - }, - { - scenario: "ErrOutOfBoundsOutputPath", - ctx: &Context{dirPath: "/foo"}, - path: "/bar/foo.txt", - expectCount: 0, - expectError: true, - }, - { - scenario: "success", - ctx: &Context{dirPath: "/foo"}, - path: "/foo/foo.txt", - expectCount: 1, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - err := tc.ctx.AddOutputPaths(tc.path) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if len(tc.ctx.outputPaths) != tc.expectCount { - t.Errorf("expected %d output paths but got %d", tc.expectCount, len(tc.ctx.outputPaths)) - } - }) - } -} - -func TestContext_Log(t *testing.T) { - expect := zap.NewNop() - ctx := Context{logger: expect} - actual := ctx.Log() - - if !reflect.DeepEqual(actual, expect) { - t.Errorf("expected %v but got %v", expect, actual) - } -} - -func TestContext_BuildOutputFile(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *Context - expectError bool - }{ - { - scenario: "ErrContextAlreadyClosed", - ctx: &Context{cancelled: true}, - expectError: true, - }, - { - scenario: "no output path", - ctx: &Context{}, - expectError: true, - }, - { - scenario: "success: one output path", - ctx: &Context{outputPaths: []string{"foo.txt"}}, - expectError: false, - }, - { - scenario: "cannot archive: invalid output paths", - ctx: &Context{outputPaths: []string{"foo.txt", "foo.pdf"}}, - expectError: true, - }, - { - scenario: "success: many output paths", - ctx: &Context{ - outputPaths: []string{ - "/tests/test/testdata/api/sample1.txt", - "/tests/test/testdata/api/sample1.txt", - }, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - dirPath, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected no erro but got: %v", err) - } - - defer func() { - err := os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - tc.ctx.dirPath = dirPath - tc.ctx.Context = context.Background() - tc.ctx.logger = zap.NewNop() - - _, err = tc.ctx.BuildOutputFile() - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestContext_OutputFilename(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *Context - outputPath string - expectOutputFilename string - }{ - { - scenario: "with Gotenberg-Output-Filename header", - ctx: func() *Context { - c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil) - c.Request().Header.Set("Gotenberg-Output-Filename", "foo") - return &Context{echoCtx: c} - }(), - outputPath: "/foo/bar.txt", - expectOutputFilename: "foo.txt", - }, - { - scenario: "without custom filename", - ctx: func() *Context { - c := echo.New().NewContext(httptest.NewRequest(http.MethodGet, "/foo", nil), nil) - return &Context{echoCtx: c} - }(), - outputPath: "/foo/foo.txt", - expectOutputFilename: "foo.txt", - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - actual := tc.ctx.OutputFilename(tc.outputPath) - - if actual != tc.expectOutputFilename { - t.Errorf("expected '%s' but got '%s'", tc.expectOutputFilename, actual) - } - }) - } -} diff --git a/pkg/modules/api/formdata_test.go b/pkg/modules/api/formdata_test.go index 5ebd0b2f..e3ce8f41 100644 --- a/pkg/modules/api/formdata_test.go +++ b/pkg/modules/api/formdata_test.go @@ -1425,36 +1425,36 @@ func TestFormData_Content(t *testing.T) { scenario: "file does exist without file extension", form: &FormData{ files: map[string]string{ - "foo": "/tests/test/testdata/api/sample1.txt", + "foo": "testdata/sample.txt", }, }, filename: "foo", defaultValue: "", - expect: "foo", + expect: "This is a text from a text file.", expectError: false, }, { scenario: "file does exist with an uppercase file extension", form: &FormData{ files: map[string]string{ - "foo.TXT": "/tests/test/testdata/api/sample1.txt", + "foo.TXT": "testdata/sample.txt", }, }, filename: "foo.txt", defaultValue: "", - expect: "foo", + expect: "This is a text from a text file.", expectError: false, }, { scenario: "file does exist without a lowercase file extension", form: &FormData{ files: map[string]string{ - "foo.txt": "/tests/test/testdata/api/sample1.txt", + "foo.txt": "testdata/sample.txt", }, }, filename: "foo.txt", defaultValue: "", - expect: "foo", + expect: "This is a text from a text file.", expectError: false, }, } { @@ -1519,33 +1519,33 @@ func TestFormData_MandatoryContent(t *testing.T) { scenario: "mandatory file does exist without file extension", form: &FormData{ files: map[string]string{ - "foo": "/tests/test/testdata/api/sample1.txt", + "foo": "testdata/sample.txt", }, }, filename: "foo", - expect: "foo", + expect: "This is a text from a text file.", expectError: false, }, { scenario: "mandatory file does exist with an uppercase file extension", form: &FormData{ files: map[string]string{ - "foo.TXT": "/tests/test/testdata/api/sample1.txt", + "foo.TXT": "testdata/sample.txt", }, }, filename: "foo.txt", - expect: "foo", + expect: "This is a text from a text file.", expectError: false, }, { scenario: "mandatory file does exist without a lowercase file extension", form: &FormData{ files: map[string]string{ - "foo.txt": "/tests/test/testdata/api/sample1.txt", + "foo.txt": "testdata/sample.txt", }, }, filename: "foo.txt", - expect: "foo", + expect: "This is a text from a text file.", expectError: false, }, } { diff --git a/pkg/modules/api/middlewares_test.go b/pkg/modules/api/middlewares_test.go deleted file mode 100644 index 8bef1268..00000000 --- a/pkg/modules/api/middlewares_test.go +++ /dev/null @@ -1,584 +0,0 @@ -package api - -import ( - "bytes" - "context" - "errors" - "mime/multipart" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestParseError(t *testing.T) { - for i, tc := range []struct { - err error - expectStatus int - expectMessage string - }{ - { - err: echo.ErrInternalServerError, - expectStatus: http.StatusInternalServerError, - expectMessage: http.StatusText(http.StatusInternalServerError), - }, - { - err: gotenberg.ErrFiltered, - expectStatus: http.StatusForbidden, - expectMessage: http.StatusText(http.StatusForbidden), - }, - { - err: gotenberg.ErrMaximumQueueSizeExceeded, - expectStatus: http.StatusTooManyRequests, - expectMessage: http.StatusText(http.StatusTooManyRequests), - }, - { - err: gotenberg.ErrPdfSplitModeNotSupported, - expectStatus: http.StatusBadRequest, - expectMessage: "At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues", - }, - { - err: gotenberg.ErrPdfFormatNotSupported, - expectStatus: http.StatusBadRequest, - expectMessage: "At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues", - }, - { - err: gotenberg.ErrPdfEngineMetadataValueNotSupported, - expectStatus: http.StatusBadRequest, - expectMessage: "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues", - }, - { - err: WrapError( - errors.New("foo"), - NewSentinelHttpError(http.StatusBadRequest, "foo"), - ), - expectStatus: http.StatusBadRequest, - expectMessage: "foo", - }, - } { - actualStatus, actualMessage := ParseError(tc.err) - - if actualStatus != tc.expectStatus { - t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, actualStatus) - } - - if actualMessage != tc.expectMessage { - t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, actualMessage) - } - } -} - -func TestHttpErrorHandler(t *testing.T) { - for i, tc := range []struct { - err error - expectStatus int - expectMessage string - }{ - { - err: echo.ErrInternalServerError, - expectStatus: http.StatusInternalServerError, - expectMessage: http.StatusText(http.StatusInternalServerError), - }, - { - err: context.DeadlineExceeded, - expectStatus: http.StatusServiceUnavailable, - expectMessage: http.StatusText(http.StatusServiceUnavailable), - }, - { - err: WrapError( - errors.New("foo"), - NewSentinelHttpError(http.StatusBadRequest, "foo"), - ), - expectStatus: http.StatusBadRequest, - expectMessage: "foo", - }, - } { - recorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/foo", nil) - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(request, recorder) - c.Set("logger", zap.NewNop()) - - handler := httpErrorHandler() - handler(tc.err, c) - - contentType := recorder.Header().Get(echo.HeaderContentType) - if contentType != echo.MIMETextPlainCharsetUTF8 { - t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, echo.MIMETextPlainCharsetUTF8, contentType) - } - - // Note: we cannot test the trace header in the response here, as it is set in the trace middleware. - - if recorder.Code != tc.expectStatus { - t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code) - } - - if recorder.Body.String() != tc.expectMessage { - t.Errorf("test %d: expected message '%s' but got '%s'", i, tc.expectMessage, recorder.Body.String()) - } - } -} - -func TestLatencyMiddleware(t *testing.T) { - recorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/foo", nil) - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(request, recorder) - - err := latencyMiddleware()( - func(c echo.Context) error { - return nil - }, - )(c) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - startTime := c.Get("startTime").(time.Time) - now := time.Now() - - if now.Before(startTime) { - t.Errorf("expected start time %s to be < %s", startTime, now) - } -} - -func TestRootPathMiddleware(t *testing.T) { - recorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/foo", nil) - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(request, recorder) - - err := rootPathMiddleware("foo")( - func(c echo.Context) error { - return nil - }, - )(c) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - rootPath := c.Get("rootPath").(string) - - if rootPath != "foo" { - t.Errorf("expected '%s' but got '%s", "foo", rootPath) - } -} - -func TestTraceMiddleware(t *testing.T) { - for i, tc := range []struct { - trace string - }{ - { - trace: "foo", - }, - { - trace: "", - }, - } { - recorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/foo", nil) - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(request, recorder) - - if tc.trace != "" { - c.Request().Header.Set("Gotenberg-Trace", tc.trace) - } - - err := traceMiddleware("Gotenberg-Trace")( - func(c echo.Context) error { - return nil - }, - )(c) - if err != nil { - t.Fatalf("test %d: expected no error but got: %v", i, err) - } - - trace := c.Get("trace").(string) - - if trace == "" { - t.Errorf("test %d: expected non empty trace in context", i) - } - - if tc.trace != "" && trace != tc.trace { - t.Errorf("test %d: expected context trace '%s' but got '%s'", i, tc.trace, trace) - } - - if tc.trace == "" && trace == tc.trace { - t.Errorf("test %d: expected context trace different from '%s' but got '%s'", i, tc.trace, trace) - } - - responseTrace := recorder.Header().Get("Gotenberg-Trace") - - if tc.trace != "" && responseTrace != tc.trace { - t.Errorf("test %d: expected header trace '%s' but got '%s'", i, tc.trace, responseTrace) - } - - if tc.trace == "" && responseTrace == tc.trace { - t.Errorf("test %d: expected header trace different from '%s' but got '%s'", i, tc.trace, responseTrace) - } - } -} - -func TestBasicAuthMiddleware(t *testing.T) { - for _, tc := range []struct { - scenario string - request *http.Request - username string - password string - expectError bool - }{ - { - scenario: "invalid basic auth", - request: func() *http.Request { - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.SetBasicAuth("invalid", "invalid") - return req - }(), - username: "foo", - password: "bar", - expectError: true, - }, - { - scenario: "valid basic auth", - request: func() *http.Request { - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.SetBasicAuth("foo", "bar") - return req - }(), - username: "foo", - password: "bar", - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - recorder := httptest.NewRecorder() - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - c := srv.NewContext(tc.request, recorder) - err := basicAuthMiddleware(tc.username, tc.password)(func(c echo.Context) error { - return nil - })(c) - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestLoggerMiddleware(t *testing.T) { - for i, tc := range []struct { - request *http.Request - next echo.HandlerFunc - skipLogging bool - }{ - { - request: httptest.NewRequest(http.MethodGet, "/", nil), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return errors.New("foo") - } - }(), - }, - { - request: httptest.NewRequest(http.MethodGet, "/health", nil), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return nil - } - }(), - skipLogging: true, - }, - { - request: httptest.NewRequest(http.MethodGet, "/health", nil), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return nil - } - }(), - }, - } { - recorder := httptest.NewRecorder() - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(tc.request, recorder) - c.Set("startTime", time.Now()) - c.Set("trace", "foo") - c.Set("rootPath", "/") - - var disableLoggingForPaths []string - if tc.skipLogging { - disableLoggingForPaths = append(disableLoggingForPaths, tc.request.RequestURI) - } - - err := loggerMiddleware(zap.NewNop(), disableLoggingForPaths)(tc.next)(c) - if err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) - } - } -} - -func TestContextMiddleware(t *testing.T) { - buildMultipartFormDataRequest := func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - - err := writer.WriteField("foo", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - - return req - } - - for i, tc := range []struct { - request *http.Request - next echo.HandlerFunc - expectErr bool - expectStatus int - expectContentType string - expectFilename string - }{ - { - request: httptest.NewRequest(http.MethodGet, "/", nil), - expectErr: true, - }, - { - request: buildMultipartFormDataRequest(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return ErrAsyncProcess - } - }(), - expectStatus: http.StatusNoContent, - }, - { - request: buildMultipartFormDataRequest(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return ErrNoOutputFile - } - }(), - expectStatus: http.StatusOK, - }, - { - request: buildMultipartFormDataRequest(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return errors.New("foo") - } - }(), - expectErr: true, - }, - { - request: buildMultipartFormDataRequest(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return nil - } - }(), - expectErr: true, - }, - { - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Output-Filename", "foo") - - return req - }(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - ctx := c.Get("context").(*Context) - ctx.outputPaths = []string{ - "/tests/test/testdata/api/sample2.pdf", - } - - return nil - } - }(), - expectStatus: http.StatusOK, - expectContentType: "application/pdf", - expectFilename: "foo.pdf", - }, - { - request: buildMultipartFormDataRequest(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - ctx := c.Get("context").(*Context) - ctx.outputPaths = []string{ - "/tests/test/testdata/api/sample1.txt", - "/tests/test/testdata/api/sample2.pdf", - } - - return nil - } - }(), - expectStatus: http.StatusOK, - expectContentType: "application/zip", - }, - } { - recorder := httptest.NewRecorder() - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(tc.request, recorder) - c.Set("logger", zap.NewNop()) - c.Set("traceHeader", "Gotenberg-Trace") - c.Set("trace", "foo") - c.Set("startTime", time.Now()) - - err := contextMiddleware(gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), time.Duration(10)*time.Second, 0, downloadFromConfig{})(tc.next)(c) - - if tc.expectErr && err == nil { - t.Errorf("test %d: expected error but got: %v", i, err) - } - - if !tc.expectErr && err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) - } - - if err != nil { - continue - } - - if recorder.Code != tc.expectStatus { - t.Errorf("test %d: expected HTTP status code %d but got %d", i, tc.expectStatus, recorder.Code) - } - - if tc.expectStatus == http.StatusNoContent { - continue - } - - contentType := recorder.Header().Get(echo.HeaderContentType) - if contentType != tc.expectContentType { - t.Errorf("test %d: expected %s '%s' but got '%s'", i, echo.HeaderContentType, tc.expectContentType, contentType) - } - - contentDisposition := recorder.Header().Get(echo.HeaderContentDisposition) - if !strings.Contains(contentDisposition, tc.expectFilename) { - t.Errorf("test %d: expected %s '%s' to contain '%s'", i, echo.HeaderContentDisposition, contentDisposition, tc.expectFilename) - } - } -} - -func TestHardTimeoutMiddleware(t *testing.T) { - for i, tc := range []struct { - next echo.HandlerFunc - timeout time.Duration - expectErr bool - expectHardTimeout bool - }{ - { - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return nil - } - }(), - timeout: time.Duration(100) * time.Millisecond, - }, - { - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - panic("foo") - } - }(), - timeout: time.Duration(100) * time.Millisecond, - expectErr: true, - expectHardTimeout: true, - }, - { - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return errors.New("foo") - } - }(), - timeout: time.Duration(100) * time.Millisecond, - expectErr: true, - }, - { - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - time.Sleep(time.Duration(200) * time.Millisecond) - - return nil - } - }(), - timeout: time.Duration(100) * time.Millisecond, - expectErr: true, - expectHardTimeout: true, - }, - } { - recorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/foo", nil) - - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(request, recorder) - c.Set("logger", zap.NewNop()) - - err := hardTimeoutMiddleware(tc.timeout)(tc.next)(c) - - if tc.expectErr && err == nil { - t.Errorf("test %d: expected error but got: %v", i, err) - } - - if !tc.expectErr && err != nil { - t.Errorf("test %d: expected no error but got: %v", i, err) - } - - var isHardTimeout bool - if err != nil { - isHardTimeout = strings.Contains(err.Error(), "hard timeout") - } - - if tc.expectHardTimeout && !isHardTimeout { - t.Errorf("test %d: expected hard timeout error but got: %v", i, err) - } - - if !tc.expectHardTimeout && isHardTimeout { - t.Errorf("test %d: expected no hard timeout error but got one: %v", i, err) - } - } -} diff --git a/pkg/modules/api/mocks_test.go b/pkg/modules/api/mocks_test.go deleted file mode 100644 index ecc2a19c..00000000 --- a/pkg/modules/api/mocks_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package api - -import ( - "reflect" - "testing" - - "github.com/alexliesenfeld/health" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestContextMock_SetDirPath(t *testing.T) { - mock := &ContextMock{&Context{}} - mock.SetDirPath("/foo") - - actual := mock.dirPath - expect := "/foo" - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestContextMock_DirPath(t *testing.T) { - mock := &ContextMock{&Context{}} - mock.SetDirPath("/foo") - - actual := mock.DirPath() - expect := "/foo" - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestContextMock_SetValues(t *testing.T) { - mock := &ContextMock{&Context{}} - mock.SetValues(map[string][]string{ - "foo": {"foo"}, - }) - - actual := mock.values - expect := map[string][]string{ - "foo": {"foo"}, - } - - if !reflect.DeepEqual(actual, expect) { - t.Errorf("expected %+v but got: %+v", expect, actual) - } -} - -func TestContextMock_SetFiles(t *testing.T) { - mock := &ContextMock{&Context{}} - mock.SetFiles(map[string]string{ - "foo": "/foo", - }) - - actual := mock.files - expect := map[string]string{ - "foo": "/foo", - } - - if !reflect.DeepEqual(actual, expect) { - t.Errorf("expected %+v but got: %+v", expect, actual) - } -} - -func TestContextMock_SetCancelled(t *testing.T) { - mock := &ContextMock{&Context{}} - mock.SetCancelled(true) - - actual := mock.cancelled - - if !actual { - t.Errorf("expected %t but got %t", true, actual) - } -} - -func TestContextMock_OutputPaths(t *testing.T) { - mock := ContextMock{ - &Context{ - outputPaths: []string{"/foo"}, - }, - } - - actual := mock.OutputPaths() - expect := []string{"/foo"} - - if !reflect.DeepEqual(actual, expect) { - t.Errorf("expected %+v but got: %+v", expect, actual) - } -} - -func TestContextMock_SetLogger(t *testing.T) { - mock := ContextMock{&Context{}} - - expect := zap.NewNop() - mock.SetLogger(expect) - - actual := mock.logger - - if actual != expect { - t.Errorf("expected %v but got %v", expect, actual) - } -} - -func TestContextMock_SetEchoContext(t *testing.T) { - mock := ContextMock{&Context{}} - - expect := echo.New().NewContext(nil, nil) - mock.SetEchoContext(expect) - - actual := mock.echoCtx - - if actual != expect { - t.Errorf("expected %v but got %v", expect, actual) - } -} - -func TestContextMock_SetMkdirAll(t *testing.T) { - mock := ContextMock{&Context{}} - - expect := new(gotenberg.OsMkdirAll) - mock.SetMkdirAll(expect) - - actual := mock.mkdirAll - - if actual != expect { - t.Errorf("expected %v but got %v", expect, actual) - } -} - -func TestContextMock_SetPathRename(t *testing.T) { - mock := ContextMock{&Context{}} - - expect := new(gotenberg.OsPathRename) - mock.SetPathRename(expect) - - actual := mock.pathRename - - if actual != expect { - t.Errorf("expected %v but got %v", expect, actual) - } -} - -func TestRouterMock(t *testing.T) { - mock := &RouterMock{ - RoutesMock: func() ([]Route, error) { - return nil, nil - }, - } - - _, err := mock.Routes() - if err != nil { - t.Errorf("expected no error from RouterMock.Routes, but got: %v", err) - } -} - -func TestMiddlewareProviderMock(t *testing.T) { - mock := &MiddlewareProviderMock{ - MiddlewaresMock: func() ([]Middleware, error) { - return nil, nil - }, - } - - _, err := mock.Middlewares() - if err != nil { - t.Errorf("expected no error from MiddlewareProviderMock.Middlewares, but got: %v", err) - } -} - -func TestHealthCheckerMock(t *testing.T) { - mock := &HealthCheckerMock{ - ChecksMock: func() ([]health.CheckerOption, error) { - return nil, nil - }, - ReadyMock: func() error { - return nil - }, - } - - _, err := mock.Checks() - if err != nil { - t.Errorf("expected no error from HealthCheckerMock.Checks, but got: %v", err) - } - - err = mock.Ready() - if err != nil { - t.Errorf("expected no error from HealthCheckerMock.Ready, but got: %v", err) - } -} diff --git a/test/testdata/libreoffice/document.txt b/pkg/modules/api/testdata/sample.txt similarity index 100% rename from test/testdata/libreoffice/document.txt rename to pkg/modules/api/testdata/sample.txt diff --git a/pkg/modules/chromium/browser_test.go b/pkg/modules/chromium/browser_test.go deleted file mode 100644 index a4636474..00000000 --- a/pkg/modules/chromium/browser_test.go +++ /dev/null @@ -1,2484 +0,0 @@ -package chromium - -import ( - "context" - "errors" - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/dlclark/regexp2" - "github.com/google/uuid" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "go.uber.org/zap/zaptest/observer" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestChromiumBrowser_Start(t *testing.T) { - for _, tc := range []struct { - scenario string - browser browser - expectError bool - cleanup bool - }{ - { - scenario: "successful start", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - }, - ), - expectError: false, - cleanup: true, - }, - { - scenario: "all browser arguments", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - incognito: true, - allowInsecureLocalhost: true, - ignoreCertificateErrors: true, - disableWebSecurity: true, - allowFileAccessFromFiles: true, - hostResolverRules: "MAP forgery.docker.localhost traefik", - proxyServer: "1.2.3.4", - }, - ), - expectError: false, - cleanup: true, - }, - { - scenario: "browser already started", - browser: func() browser { - b := new(chromiumBrowser) - b.isStarted.Store(true) - return b - }(), - expectError: true, - cleanup: false, - }, - { - scenario: "browser start error", - browser: func() browser { - b := newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - }, - ).(*chromiumBrowser) - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - b.initialCtx = ctx - - return b - }(), - expectError: true, - cleanup: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - logger := zap.NewNop() - err := tc.browser.Start(logger) - - if tc.cleanup { - defer func(b browser, logger *zap.Logger) { - err = b.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.browser, logger) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromiumBrowser_Stop(t *testing.T) { - for _, tc := range []struct { - scenario string - browser browser - setup func(browser browser, logger *zap.Logger) error - expectError bool - }{ - { - scenario: "successful stop", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - }, - ), - setup: func(b browser, logger *zap.Logger) error { - return b.Start(logger) - }, - expectError: false, - }, - { - scenario: "browser already stopped", - browser: func() browser { - b := new(chromiumBrowser) - b.isStarted.Store(false) - return b - }(), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - logger := zap.NewNop() - - if tc.setup != nil { - err := tc.setup(tc.browser, logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - } - - err := tc.browser.Stop(logger) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromiumBrowser_Healthy(t *testing.T) { - for _, tc := range []struct { - scenario string - browser browser - setup func(browser browser, logger *zap.Logger) error - expectHealthy bool - cleanup bool - }{ - { - scenario: "healthy browser", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - }, - ), - setup: func(b browser, logger *zap.Logger) error { - return b.Start(logger) - }, - expectHealthy: true, - cleanup: true, - }, - { - scenario: "browser not started", - browser: func() browser { - b := new(chromiumBrowser) - b.isStarted.Store(false) - return b - }(), - expectHealthy: false, - cleanup: false, - }, - { - scenario: "unhealthy browser", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - }, - ), - setup: func(b browser, logger *zap.Logger) error { - _ = b.Start(logger) - b.(*chromiumBrowser).cancelFunc() - - return nil - }, - expectHealthy: false, - cleanup: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - logger := zap.NewNop() - - if tc.setup != nil { - err := tc.setup(tc.browser, logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - } - - if tc.cleanup { - defer func(b browser, logger *zap.Logger) { - err := b.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.browser, logger) - } - - healthy := tc.browser.Healthy(logger) - - if !tc.expectHealthy && healthy { - t.Fatal("expected unhealthy browser but got an healthy one") - } - - if tc.expectHealthy && !healthy { - t.Fatal("expected a healthy browser but got an unhealthy one") - } - }) - } -} - -func TestChromiumBrowser_pdf(t *testing.T) { - for _, tc := range []struct { - scenario string - browser browser - fs *gotenberg.FileSystem - options PdfOptions - url string - noDeadline bool - start bool - expectError bool - expectedError error - expectedLogEntries []string - }{ - { - scenario: "browser not started", - browser: func() browser { - b := new(chromiumBrowser) - b.isStarted.Store(false) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: false, - start: false, - expectError: true, - }, - { - scenario: "context has no deadline", - browser: func() browser { - b := new(chromiumBrowser) - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: true, - start: false, - expectError: true, - }, - { - scenario: "ErrFiltered: main URL does not match the allowed list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), - denyList: regexp2.MustCompile("", 0), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: false, - start: false, - expectError: true, - expectedError: gotenberg.ErrFiltered, - }, - { - scenario: "ErrFiltered: main URL does match the denied list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("^file:///tmp.*", 0), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: false, - start: false, - expectError: true, - expectedError: gotenberg.ErrFiltered, - }, - { - scenario: "a request does not match the allowed list", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("^file:///tmp.*", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "'file:///etc/passwd' does not match the expression from the allowed list", - }, - }, - { - scenario: "a request does match the denied list", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "'file:///etc/passwd' matches the expression from the denied list", - }, - }, - { - scenario: "do not skip networkIdle event", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Skip networkIdle event

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{SkipNetworkIdleEvent: false}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "event networkIdle fired", - }, - }, - { - scenario: "ErrInvalidHttpStatusCode", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidHttpStatusCode

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{FailOnHttpStatusCodes: []int64{299}}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidHttpStatusCode, - }, - { - scenario: "ErrInvalidResourceHttpStatusCode", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/style.css", fs.WorkingDirPath()), []byte("body{font-family: Arial, Helvetica, sans-serif;}"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidResourceHttpStatusCode

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{FailOnResourceHttpStatusCodes: []int64{200}}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidResourceHttpStatusCode, - }, - { - scenario: "ErrConsoleExceptions", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{FailOnConsoleExceptions: true}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrConsoleExceptions, - }, - { - scenario: "ErrLoadingFailed", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - url: "http://localhost:100", - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrLoadingFailed, - }, - { - scenario: "ErrResourceLoadingFailed", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrResourceLoadingFailed

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{FailOnResourceLoadingFailed: true}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrResourceLoadingFailed, - }, - { - scenario: "clear cache", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - clearCache: true, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Clear cache

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "clear cache", - }, - }, - { - scenario: "clear cookies", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - clearCookies: true, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Clear cookies

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "clear cookies", - }, - }, - { - scenario: "disable JavaScript", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - disableJavaScript: true, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "disable JavaScript", - "JavaScript disabled, skipping wait delay", - "JavaScript disabled, skipping wait expression", - }, - }, - { - scenario: "set cookies", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Set cookies

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{Cookies: []Cookie{{Name: "foo", Value: "bar", Domain: ".foo.bar"}}}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "set cookie", - }, - }, - { - scenario: "user agent override", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

User-Agent override

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{UserAgent: "foo"}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - fmt.Sprintf("user agent override: foo"), - }, - }, - { - scenario: "extra HTTP headers", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Extra HTTP headers

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{ - { - Name: "X-Foo", - Value: "foo", - }, - { - Name: "X-Bar", - Value: "bar", - Scope: regexp2.MustCompile(`.*index\.html.*`, 0), - }, - { - Name: "X-Baz", - Value: "baz", - Scope: regexp2.MustCompile(`.*another\.html.*`, 0), - }, - }}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "extra HTTP headers:", - "extra HTTP header 'X-Foo' will be set for request URL", - "extra HTTP header 'X-Bar' (scoped) will be set for request URL", - "extra HTTP header 'X-Baz' (scoped) will not be set for request URL", - "setting extra HTTP headers for request URL", - }, - }, - { - scenario: "ErrOmitBackgroundWithoutPrintBackground", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrOmitBackgroundWithoutPrintBackground

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{OmitBackground: true}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrOmitBackgroundWithoutPrintBackground, - }, - { - scenario: "hide default white background", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Hide default white background

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{OmitBackground: true}, - PrintBackground: true, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "hide default white background", - }, - }, - { - scenario: "ErrInvalidEmulatedMediaType", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidEmulatedMediaType

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{EmulatedMediaType: "foo"}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidEmulatedMediaType, - }, - { - scenario: "emulate a media type", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Screen media type

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{EmulatedMediaType: "screen"}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "emulate media type 'screen'", - }, - }, - { - scenario: "wait delay: context done", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{WaitDelay: time.Duration(10) * time.Second}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedLogEntries: []string{ - "wait '10s' before print", - }, - }, - { - scenario: "wait delay", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{WaitDelay: time.Duration(1) * time.Millisecond}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "wait '1ms' before print", - }, - }, - { - scenario: "wait for expression: context done", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - html := ` - -` - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(html), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{WaitForExpression: "window.status === 'ready'"}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedLogEntries: []string{ - "wait until 'window.status === 'ready'' is true before print", - }, - }, - { - scenario: "ErrInvalidEvaluationExpression", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidEvaluationExpression

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{WaitForExpression: "return undefined"}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedLogEntries: []string{ - "wait until 'return undefined' is true before print", - }, - }, - { - scenario: "wait for expression", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - html := ` - -` - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(html), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - Options: Options{WaitForExpression: "window.globalVar === 'ready'"}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "wait until 'window.globalVar === 'ready'' is true before print", - }, - }, - { - scenario: "single page", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Custom header and footer

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - SinglePage: true, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "single page PDF", - }, - }, - { - scenario: "custom header and footer", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Custom header and footer

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - HeaderTemplate: "

Header

", - FooterTemplate: "

Footer

", - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "with custom header and/or footer", - }, - }, - { - scenario: "ErrInvalidPrinterSettings", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidPrinterSettings

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - PaperWidth: 0, - PaperHeight: 0, - MarginTop: 1000000, - MarginBottom: 1000000, - MarginLeft: 1000000, - MarginRight: 1000000, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidPrinterSettings, - }, - { - scenario: "ErrPageRangesSyntaxError", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrPageRangesSyntaxError

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: PdfOptions{ - PageRanges: "foo", - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrPageRangesSyntaxError, - }, - { - scenario: "success (default options)", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Default options

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: DefaultPdfOptions(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "cache not cleared", - "cookies not cleared", - "JavaScript not disabled", - "no cookies to set", - "no extra HTTP headers", - "navigate to", - "skipping network idle event", - "default white background not hidden", - "no emulated media type", - "no wait delay", - "no wait expression", - "no custom header nor footer", - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - core, recorded := observer.New(zapcore.DebugLevel) - logger := zap.New(core) - - defer func() { - err := os.RemoveAll(tc.fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }() - - if tc.start { - err := tc.browser.Start(logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - - defer func(b browser, logger *zap.Logger) { - err = b.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.browser, logger) - } - - var ( - ctx context.Context - cancel context.CancelFunc - ) - - if tc.noDeadline { - ctx = context.Background() - } else { - ctx, cancel = context.WithTimeout(context.Background(), time.Duration(5)*time.Second) - defer cancel() - } - - url := fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()) - if tc.url != "" { - url = tc.url - } - - err := tc.browser.pdf( - ctx, - logger, - url, - fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()), - tc.options, - ) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - for _, entry := range tc.expectedLogEntries { - doExist := true - for _, log := range recorded.All() { - doExist = strings.Contains(log.Message, entry) - if doExist { - break - } - } - - if !doExist { - t.Errorf("expected '%s' to exist as log entry", entry) - } - } - }) - } -} - -func TestChromiumBrowser_screenshot(t *testing.T) { - for _, tc := range []struct { - scenario string - browser browser - fs *gotenberg.FileSystem - options ScreenshotOptions - url string - noDeadline bool - start bool - expectError bool - expectedError error - expectedLogEntries []string - }{ - { - scenario: "browser not started", - browser: func() browser { - b := new(chromiumBrowser) - b.isStarted.Store(false) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: false, - start: false, - expectError: true, - }, - { - scenario: "context has not deadline", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: true, - start: false, - expectError: true, - }, - { - scenario: "ErrFiltered: main URL does not match the allowed list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), - denyList: regexp2.MustCompile("", 0), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: false, - start: false, - expectError: true, - expectedError: gotenberg.ErrFiltered, - }, - { - scenario: "ErrFiltered: main URL does match the denied list", - browser: func() browser { - b := new(chromiumBrowser) - b.arguments = browserArguments{ - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("^file:///tmp.*", 0), - } - b.isStarted.Store(true) - return b - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - noDeadline: false, - start: false, - expectError: true, - expectedError: gotenberg.ErrFiltered, - }, - { - scenario: "a request does not match the allowed list", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("^file:///tmp.*", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "'file:///etc/passwd' does not match the expression from the allowed list", - }, - }, - { - scenario: "a request does match the denied list", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "'file:///etc/passwd' matches the expression from the denied list", - }, - }, - { - scenario: "do not skip networkIdle event", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Skip networkIdle event

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{SkipNetworkIdleEvent: false}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "event networkIdle fired", - }, - }, - { - scenario: "ErrInvalidHttpStatusCode", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidHttpStatusCode

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{FailOnHttpStatusCodes: []int64{299}}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidHttpStatusCode, - }, - { - scenario: "ErrInvalidResourceHttpStatusCode", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/style.css", fs.WorkingDirPath()), []byte("body{font-family: Arial, Helvetica, sans-serif;}"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidResourceHttpStatusCode

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{FailOnResourceHttpStatusCodes: []int64{299}}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidResourceHttpStatusCode, - }, - { - scenario: "ErrConsoleExceptions", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{FailOnConsoleExceptions: true}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrConsoleExceptions, - }, - { - scenario: "ErrLoadingFailed", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - url: "http://localhost:100", - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrLoadingFailed, - }, - { - scenario: "ErrResourceLoadingFailed", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrResourceLoadingFailed

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{FailOnResourceLoadingFailed: true}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrResourceLoadingFailed, - }, - { - scenario: "clear cache", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - clearCache: true, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Clear cache

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "clear cache", - }, - }, - { - scenario: "clear cookies", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - clearCookies: true, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Clear cookies

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "clear cookies", - }, - }, - { - scenario: "disable JavaScript", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - disableJavaScript: true, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "disable JavaScript", - "JavaScript disabled, skipping wait delay", - "JavaScript disabled, skipping wait expression", - }, - }, - { - scenario: "set cookies", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Set cookies

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{Cookies: []Cookie{{Name: "fpp", Value: "bar", Domain: ".foo.bar"}}}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "set cookie", - }, - }, - { - scenario: "user agent override", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

User-Agent override

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{UserAgent: "foo"}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - fmt.Sprintf("user agent override: foo"), - }, - }, - { - scenario: "extra HTTP headers", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Extra HTTP headers

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{ExtraHttpHeaders: []ExtraHttpHeader{ - { - Name: "X-Foo", - Value: "foo", - }, - { - Name: "X-Bar", - Value: "bar", - Scope: regexp2.MustCompile(`.*index\.html.*`, 0), - }, - { - Name: "X-Baz", - Value: "baz", - Scope: regexp2.MustCompile(`.*another\.html.*`, 0), - }, - }}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "extra HTTP headers:", - "extra HTTP header 'X-Foo' will be set for request URL", - "extra HTTP header 'X-Bar' (scoped) will be set for request URL", - "extra HTTP header 'X-Baz' (scoped) will not be set for request URL", - "setting extra HTTP headers for request URL", - }, - }, - { - scenario: "ErrOmitBackgroundWithoutPrintBackground", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrOmitBackgroundWithoutPrintBackground

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{OmitBackground: true}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "hide default white background", - }, - }, - { - scenario: "ErrInvalidEmulatedMediaType", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidEmulatedMediaType

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{EmulatedMediaType: "foo"}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedError: ErrInvalidEmulatedMediaType, - }, - { - scenario: "emulate a media type", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Screen media type

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{EmulatedMediaType: "screen"}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "emulate media type 'screen'", - }, - }, - { - scenario: "wait delay: context done", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{WaitDelay: time.Duration(10) * time.Second}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedLogEntries: []string{ - "wait '10s' before print", - }, - }, - { - scenario: "wait delay", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(""), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{WaitDelay: time.Duration(1) * time.Millisecond}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "wait '1ms' before print", - }, - }, - { - scenario: "wait for expression: context done", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - html := ` - -` - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(html), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{WaitForExpression: "window.status === 'ready'"}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedLogEntries: []string{ - "wait until 'window.status === 'ready'' is true before print", - }, - }, - { - scenario: "ErrInvalidEvaluationExpression", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

ErrInvalidEvaluationExpression

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{WaitForExpression: "return undefined"}, - }, - noDeadline: false, - start: true, - expectError: true, - expectedLogEntries: []string{ - "wait until 'return undefined' is true before print", - }, - }, - { - scenario: "wait for expression", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - html := ` - -` - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte(html), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: ScreenshotOptions{ - Options: Options{WaitForExpression: "window.globalVar === 'ready'"}, - }, - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "wait until 'window.globalVar === 'ready'' is true before print", - }, - }, - { - scenario: "success (default options)", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Default options

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: DefaultScreenshotOptions(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "cache not cleared", - "cookies not cleared", - "JavaScript not disabled", - "no user agent override", - "no extra HTTP headers", - "navigate to", - "default white background not hidden", - "no emulated media type", - "no wait delay", - "no wait expression", - "set device metrics override", - }, - }, - { - scenario: "success (clip)", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Default options

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Clip = true - return options - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "cache not cleared", - "cookies not cleared", - "JavaScript not disabled", - "no cookies to set", - "no user agent override", - "no extra HTTP headers", - "navigate to", - "default white background not hidden", - "no emulated media type", - "no wait delay", - "no wait expression", - "set device metrics override", - }, - }, - { - scenario: "success (jpeg)", - browser: newChromiumBrowser( - browserArguments{ - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - wsUrlReadTimeout: 5 * time.Second, - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", fs.WorkingDirPath()), []byte("

Default options

"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Format = "jpeg" - return options - }(), - noDeadline: false, - start: true, - expectError: false, - expectedLogEntries: []string{ - "cache not cleared", - "cookies not cleared", - "JavaScript not disabled", - "no cookies to set", - "no user agent override", - "no extra HTTP headers", - "navigate to", - "skipping network idle event", - "default white background not hidden", - "no emulated media type", - "no wait delay", - "no wait expression", - "set device metrics override", - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - core, recorded := observer.New(zapcore.DebugLevel) - logger := zap.New(core) - - defer func() { - err := os.RemoveAll(tc.fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }() - - if tc.start { - err := tc.browser.Start(logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - - defer func(b browser, logger *zap.Logger) { - err = b.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.browser, logger) - } - - var ( - ctx context.Context - cancel context.CancelFunc - ) - - if tc.noDeadline { - ctx = context.Background() - } else { - ctx, cancel = context.WithTimeout(context.Background(), time.Duration(5)*time.Second) - defer cancel() - } - - url := fmt.Sprintf("file://%s/index.html", tc.fs.WorkingDirPath()) - if tc.url != "" { - url = tc.url - } - - err := tc.browser.screenshot( - ctx, - logger, - url, - fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()), - tc.options, - ) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - for _, entry := range tc.expectedLogEntries { - doExist := true - for _, log := range recorded.All() { - doExist = strings.Contains(log.Message, entry) - if doExist { - break - } - } - - if !doExist { - t.Errorf("expected '%s' to exist as log entry", entry) - } - } - }) - } -} diff --git a/pkg/modules/chromium/chromium_test.go b/pkg/modules/chromium/chromium_test.go deleted file mode 100644 index 720bc943..00000000 --- a/pkg/modules/chromium/chromium_test.go +++ /dev/null @@ -1,637 +0,0 @@ -package chromium - -import ( - "context" - "errors" - "os" - "reflect" - "testing" - "time" - - "github.com/alexliesenfeld/health" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestDefaultOptions(t *testing.T) { - actual := DefaultOptions() - notExpect := Options{} - - if reflect.DeepEqual(actual, notExpect) { - t.Errorf("expected %v and got identical %v", actual, notExpect) - } -} - -func TestDefaultPdfOptions(t *testing.T) { - actual := DefaultPdfOptions() - notExpect := PdfOptions{} - - if reflect.DeepEqual(actual, notExpect) { - t.Errorf("expected %v and got identical %v", actual, notExpect) - } -} - -func TestDefaultScreenshotOptions(t *testing.T) { - actual := DefaultScreenshotOptions() - notExpect := ScreenshotOptions{} - - if reflect.DeepEqual(actual, notExpect) { - t.Errorf("expected %v and got identical %v", actual, notExpect) - } -} - -func TestChromium_Descriptor(t *testing.T) { - descriptor := new(Chromium).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(Chromium)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestChromium_Provision(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - expectError bool - }{ - { - scenario: "no logger provider", - ctx: func() *gotenberg.Context { - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Chromium).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{}, - ) - }(), - expectError: true, - }, - { - scenario: "no logger from logger provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Chromium).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no PDF engine provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return zap.NewNop(), nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Chromium).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no PDF engine from PDF engine provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - gotenberg.PdfEngineProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return zap.NewNop(), nil - } - mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Chromium).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "provision success", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - gotenberg.PdfEngineProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return zap.NewNop(), nil - } - mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) { - return new(gotenberg.PdfEngineMock), nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Chromium).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - err := mod.Provision(tc.ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromium_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - binPath string - expectError bool - }{ - { - scenario: "empty bin path", - binPath: "", - expectError: true, - }, - { - scenario: "bin path does not exist", - binPath: "/foo", - expectError: true, - }, - { - scenario: "validate success", - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.args = browserArguments{ - binPath: tc.binPath, - } - err := mod.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromium_Start(t *testing.T) { - for _, tc := range []struct { - scenario string - autoStart bool - supervisor *gotenberg.ProcessSupervisorMock - expectError bool - }{ - { - scenario: "no auto-start", - autoStart: false, - expectError: false, - }, - { - scenario: "auto-start success", - autoStart: true, - supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error { - return nil - }}, - expectError: false, - }, - { - scenario: "auto-start failed", - autoStart: true, - supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error { - return errors.New("foo") - }}, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.autoStart = tc.autoStart - mod.supervisor = tc.supervisor - - err := mod.Start() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromium_StartupMessage(t *testing.T) { - mod := new(Chromium) - - mod.autoStart = true - autoStartMsg := mod.StartupMessage() - - mod.autoStart = false - noAutoStartMsg := mod.StartupMessage() - - if autoStartMsg == noAutoStartMsg { - t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg) - } -} - -func TestChromium_Stop(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor *gotenberg.ProcessSupervisorMock - expectError bool - }{ - { - scenario: "stop success", - supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error { - return nil - }}, - expectError: false, - }, - { - scenario: "stop failed", - supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error { - return errors.New("foo") - }}, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.logger = zap.NewNop() - mod.supervisor = tc.supervisor - - ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second) - cancel() - - err := mod.Stop(ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromium_Debug(t *testing.T) { - for _, tc := range []struct { - scenario string - mod *Chromium - expect map[string]interface{} - doNotExpect map[string]interface{} - }{ - { - scenario: "cannot determine version", - mod: &Chromium{ - args: browserArguments{ - binPath: "foo", - }, - }, - expect: map[string]interface{}{ - "version": `exec: "foo": executable file not found in $PATH`, - }, - }, - { - scenario: "success", - mod: &Chromium{ - args: browserArguments{ - binPath: "echo", - }, - }, - doNotExpect: map[string]interface{}{ - "version": `exec: "echo": executable file not found in $PATH`, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - d := tc.mod.Debug() - - if tc.expect != nil { - if !reflect.DeepEqual(d, tc.expect) { - t.Errorf("expected '%v' but got '%v'", tc.expect, d) - } - } - - if tc.doNotExpect != nil { - if reflect.DeepEqual(d, tc.doNotExpect) { - t.Errorf("did not expect '%v'", d) - } - } - }) - } -} - -func TestChromium_Metrics(t *testing.T) { - mod := new(Chromium) - mod.supervisor = &gotenberg.ProcessSupervisorMock{ - ReqQueueSizeMock: func() int64 { - return 10 - }, - RestartsCountMock: func() int64 { - return 0 - }, - } - - metrics, err := mod.Metrics() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if len(metrics) != 2 { - t.Fatalf("expected %d metrics, but got %d", 2, len(metrics)) - } - - actual := metrics[0].Read() - if actual != float64(10) { - t.Errorf("expected %f for chromium_requests_queue_size, but got %f", float64(10), actual) - } - - actual = metrics[1].Read() - if actual != float64(0) { - t.Errorf("expected %f for chromium_restarts_count, but got %f", float64(0), actual) - } -} - -func TestChromium_Checks(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor gotenberg.ProcessSupervisor - expectAvailabilityStatus health.AvailabilityStatus - }{ - { - scenario: "healthy module", - supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool { - return true - }}, - expectAvailabilityStatus: health.StatusUp, - }, - { - scenario: "unhealthy module", - supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool { - return false - }}, - expectAvailabilityStatus: health.StatusDown, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.supervisor = tc.supervisor - - checks, err := mod.Checks() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - checker := health.NewChecker(checks...) - result := checker.Check(context.Background()) - - if result.Status != tc.expectAvailabilityStatus { - t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status) - } - }) - } -} - -func TestChromium_Ready(t *testing.T) { - for _, tc := range []struct { - scenario string - autoStart bool - startTimeout time.Duration - browser browser - expectError bool - }{ - { - scenario: "no auto-start", - autoStart: false, - startTimeout: time.Duration(30) * time.Second, - browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool { - return false - }}}, - expectError: false, - }, - { - scenario: "auto-start: context done", - autoStart: true, - startTimeout: time.Duration(200) * time.Millisecond, - browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool { - return false - }}}, - expectError: true, - }, - { - scenario: "auto-start success", - autoStart: true, - startTimeout: time.Duration(30) * time.Second, - browser: &browserMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool { - return true - }}}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.autoStart = tc.autoStart - mod.args = browserArguments{wsUrlReadTimeout: tc.startTimeout} - mod.browser = tc.browser - - err := mod.Ready() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromium_Chromium(t *testing.T) { - mod := new(Chromium) - - _, err := mod.Chromium() - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestChromium_Routes(t *testing.T) { - for _, tc := range []struct { - scenario string - expectRoutes int - disableRoutes bool - }{ - { - scenario: "routes not disabled", - expectRoutes: 6, - disableRoutes: false, - }, - { - scenario: "routes disabled", - expectRoutes: 0, - disableRoutes: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.disableRoutes = tc.disableRoutes - - routes, err := mod.Routes() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectRoutes != len(routes) { - t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes)) - } - }) - } -} - -func TestChromium_Pdf(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor gotenberg.ProcessSupervisor - browser browser - expectError bool - }{ - { - scenario: "PDF task success", - browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - expectError: false, - }, - { - scenario: "PDF task error", - browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return errors.New("PDF task error") - }}, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error { - return task() - }} - mod.browser = tc.browser - - err := mod.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{}) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestChromium_Screenshot(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor gotenberg.ProcessSupervisor - browser browser - expectError bool - }{ - { - scenario: "Screenshot task success", - browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }}, - expectError: false, - }, - { - scenario: "Screenshot task error", - browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return errors.New("screenshot task error") - }}, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Chromium) - mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error { - return task() - }} - mod.browser = tc.browser - - err := mod.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{}) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} diff --git a/pkg/modules/chromium/debug_test.go b/pkg/modules/chromium/debug_test.go deleted file mode 100644 index 579f84db..00000000 --- a/pkg/modules/chromium/debug_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package chromium - -import ( - "testing" - - "go.uber.org/zap" -) - -func TestDebugLogger_Write(t *testing.T) { - actual, err := (&debugLogger{logger: zap.NewNop()}).Write([]byte("foo")) - expected := len([]byte("foo")) - - if actual != expected { - t.Errorf("expected %d but got %d", expected, actual) - } - - if err != nil { - t.Errorf("expected not error but got: %v", err) - } -} - -func TestDebugLogger_Printf(t *testing.T) { - (&debugLogger{logger: zap.NewNop()}).Printf("%s", "foo") -} diff --git a/pkg/modules/chromium/mocks_test.go b/pkg/modules/chromium/mocks_test.go deleted file mode 100644 index 1f361a8a..00000000 --- a/pkg/modules/chromium/mocks_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package chromium - -import ( - "context" - "testing" - - "go.uber.org/zap" -) - -func TestApiMock(t *testing.T) { - mock := &ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }, - ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }, - } - - err := mock.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{}) - if err != nil { - t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err) - } - - err = mock.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{}) - if err != nil { - t.Errorf("expected no error from ApiMock.Screenshot, but got: %v", err) - } -} - -func TestBrowserMock(t *testing.T) { - mock := &browserMock{ - pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }, - screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }, - } - - err := mock.pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{}) - if err != nil { - t.Errorf("expected no error from browserMock.pdf, but got: %v", err) - } - - err = mock.screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{}) - if err != nil { - t.Errorf("expected no error from browserMock.screenshot, but got: %v", err) - } -} diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go deleted file mode 100644 index 552e55ca..00000000 --- a/pkg/modules/chromium/routes_test.go +++ /dev/null @@ -1,1927 +0,0 @@ -package chromium - -import ( - "context" - "errors" - "fmt" - "net/http" - "os" - "reflect" - "sort" - "testing" - - "github.com/dlclark/regexp2" - "github.com/google/uuid" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" - "github.com/gotenberg/gotenberg/v8/pkg/modules/api" -) - -func TestFormDataChromiumOptions(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - expectedOptions Options - compareWithoutDeepEqual bool - expectValidationError bool - }{ - { - scenario: "no custom form fields", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectedOptions: DefaultOptions(), - compareWithoutDeepEqual: false, - expectValidationError: false, - }, - { - scenario: "invalid failOnHttpStatusCodes form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "failOnHttpStatusCodes": { - "foo", - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.FailOnHttpStatusCodes = nil - return options - }(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "valid failOnHttpStatusCodes form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "failOnHttpStatusCodes": { - `[399,499,599]`, - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.FailOnHttpStatusCodes = []int64{399, 499, 599} - return options - }(), - compareWithoutDeepEqual: false, - expectValidationError: false, - }, - { - scenario: "invalid failOnResourceHttpStatusCodes form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "failOnResourceHttpStatusCodes": { - "foo", - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.FailOnResourceHttpStatusCodes = nil - return options - }(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "valid failOnResourceHttpStatusCodes form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "failOnResourceHttpStatusCodes": { - `[399,499,599]`, - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.FailOnResourceHttpStatusCodes = []int64{399, 499, 599} - return options - }(), - compareWithoutDeepEqual: false, - expectValidationError: false, - }, - { - scenario: "invalid cookies form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "cookies": { - "foo", - }, - }) - return ctx - }(), - expectedOptions: DefaultOptions(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "invalid cookies form field (missing required values)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "cookies": { - "[{}]", - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - // No validation in this method, so it still instantiates - // an empty item. - options.Cookies = []Cookie{{}} - return options - }(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "valid cookies form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "cookies": { - `[{"name":"foo","value":"bar","domain":".foo.bar"}]`, - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.Cookies = []Cookie{{ - Name: "foo", - Value: "bar", - Domain: ".foo.bar", - }} - return options - }(), - compareWithoutDeepEqual: false, - expectValidationError: false, - }, - { - scenario: "invalid extraHttpHeaders form field: cannot unmarshall", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "extraHttpHeaders": { - "foo", - }, - }) - return ctx - }(), - expectedOptions: DefaultOptions(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "invalid extraHttpHeaders form field: invalid scope", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "extraHttpHeaders": { - `{"foo":"bar;scope;;"}`, - }, - }) - return ctx - }(), - expectedOptions: DefaultOptions(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "invalid extraHttpHeaders form field: invalid scope regex pattern", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "extraHttpHeaders": { - `{"foo":"bar;scope=*."}`, - }, - }) - return ctx - }(), - expectedOptions: DefaultOptions(), - compareWithoutDeepEqual: false, - expectValidationError: true, - }, - { - scenario: "valid extraHttpHeaders form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "extraHttpHeaders": { - `{"foo":"bar","baz":"qux;scope=https?:\\/\\/([a-zA-Z0-9-]+\\.)*qux\\.com\\/.*"}`, - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.ExtraHttpHeaders = []ExtraHttpHeader{ - { - Name: "foo", - Value: "bar", - }, - { - Name: "baz", - Value: "qux", - Scope: regexp2.MustCompile(`https?:\/\/([a-zA-Z0-9-]+\.)*qux\.com\/.*`, 0), - }, - } - return options - }(), - compareWithoutDeepEqual: true, - expectValidationError: false, - }, - { - scenario: "invalid emulatedMediaType form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "emulatedMediaType": { - "foo", - }, - }) - return ctx - }(), - expectedOptions: DefaultOptions(), - expectValidationError: true, - }, - { - scenario: "valid emulatedMediaType form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "emulatedMediaType": { - "screen", - }, - }) - return ctx - }(), - expectedOptions: func() Options { - options := DefaultOptions() - options.EmulatedMediaType = "screen" - return options - }(), - expectValidationError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - form, actual := FormDataChromiumOptions(tc.ctx.Context) - - if tc.compareWithoutDeepEqual { - if len(tc.expectedOptions.ExtraHttpHeaders) != len(actual.ExtraHttpHeaders) { - t.Fatalf("expected %d extra HTTP headers, but got %d", len(tc.expectedOptions.ExtraHttpHeaders), len(actual.ExtraHttpHeaders)) - } - - sort.Slice(tc.expectedOptions.ExtraHttpHeaders, func(i, j int) bool { - return tc.expectedOptions.ExtraHttpHeaders[i].Name < tc.expectedOptions.ExtraHttpHeaders[j].Name - }) - sort.Slice(actual.ExtraHttpHeaders, func(i, j int) bool { - return actual.ExtraHttpHeaders[i].Name < actual.ExtraHttpHeaders[j].Name - }) - - for i := range tc.expectedOptions.ExtraHttpHeaders { - if tc.expectedOptions.ExtraHttpHeaders[i].Name != actual.ExtraHttpHeaders[i].Name { - t.Fatalf("expected '%s' extra HTTP header, but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Name, tc.expectedOptions.ExtraHttpHeaders[i].Name) - } - - if tc.expectedOptions.ExtraHttpHeaders[i].Value != actual.ExtraHttpHeaders[i].Value { - t.Fatalf("expected '%s' as value for extra HTTP header '%s', but got '%s'", tc.expectedOptions.ExtraHttpHeaders[i].Value, tc.expectedOptions.ExtraHttpHeaders[i].Name, actual.ExtraHttpHeaders[i].Value) - } - - var expectedScope string - if tc.expectedOptions.ExtraHttpHeaders[i].Scope != nil { - expectedScope = tc.expectedOptions.ExtraHttpHeaders[i].Scope.String() - } - var actualScope string - if actual.ExtraHttpHeaders[i].Scope != nil { - actualScope = actual.ExtraHttpHeaders[i].Scope.String() - } - - if expectedScope != actualScope { - t.Fatalf("expected '%s' as scope for extra HTTP header '%s', but got '%s'", expectedScope, tc.expectedOptions.ExtraHttpHeaders[i].Name, actualScope) - } - } - } else { - if !reflect.DeepEqual(actual, tc.expectedOptions) { - t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual) - } - } - - err := form.Validate() - - if tc.expectValidationError && err == nil { - t.Fatal("expected validation error but got none", err) - } - - if !tc.expectValidationError && err != nil { - t.Fatalf("expected no validation error but got: %v", err) - } - }) - } -} - -func TestFormDataChromiumPdfOptions(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - expectedOptions PdfOptions - expectValidationError bool - }{ - { - scenario: "no custom form fields", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectedOptions: DefaultPdfOptions(), - expectValidationError: false, - }, - { - scenario: "custom form fields (Options & PdfOptions)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "landscape": { - "true", - }, - "emulatedMediaType": { - "screen", - }, - }) - return ctx - }(), - expectedOptions: func() PdfOptions { - options := DefaultPdfOptions() - options.Landscape = true - options.EmulatedMediaType = "screen" - return options - }(), - expectValidationError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - form, actual := FormDataChromiumPdfOptions(tc.ctx.Context) - - if !reflect.DeepEqual(actual, tc.expectedOptions) { - t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual) - } - - err := form.Validate() - - if tc.expectValidationError && err == nil { - t.Fatal("expected validation error but got none", err) - } - - if !tc.expectValidationError && err != nil { - t.Fatalf("expected no validation error but got: %v", err) - } - }) - } -} - -func TestFormDataChromiumScreenshotOptions(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - expectedOptions ScreenshotOptions - expectValidationError bool - }{ - { - scenario: "no custom form fields", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectedOptions: DefaultScreenshotOptions(), - expectValidationError: false, - }, - { - scenario: "invalid format form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "format": { - "gif", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Format = "" - return options - }(), - expectValidationError: true, - }, - { - scenario: "valid png format form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "format": { - "png", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Format = "png" - return options - }(), - expectValidationError: false, - }, - { - scenario: "valid jpeg format form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "format": { - "jpeg", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Format = "jpeg" - return options - }(), - expectValidationError: false, - }, - { - scenario: "valid webp format form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "format": { - "webp", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Format = "webp" - return options - }(), - expectValidationError: false, - }, - { - scenario: "invalid quality form field (not an integer)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "quality": { - "foo", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Quality = 0 - return options - }(), - expectValidationError: true, - }, - { - scenario: "invalid quality form field (< 0)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "quality": { - "-1", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Quality = 0 - return options - }(), - expectValidationError: true, - }, - { - scenario: "invalid quality form field (> 100)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "quality": { - "101", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Quality = 0 - return options - }(), - expectValidationError: true, - }, - { - scenario: "valid quality form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "quality": { - "50", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Quality = 50 - return options - }(), - expectValidationError: false, - }, - { - scenario: "custom form fields (Options & ScreenshotOptions)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "width": { - "1280", - }, - "height": { - "800", - }, - "clip": { - "true", - }, - "optimizeForSpeed": { - "true", - }, - "emulatedMediaType": { - "screen", - }, - }) - return ctx - }(), - expectedOptions: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.Width = 1280 - options.Height = 800 - options.Clip = true - options.OptimizeForSpeed = true - options.EmulatedMediaType = "screen" - return options - }(), - expectValidationError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - form, actual := FormDataChromiumScreenshotOptions(tc.ctx.Context) - - if !reflect.DeepEqual(actual, tc.expectedOptions) { - t.Fatalf("expected %+v but got: %+v", tc.expectedOptions, actual) - } - - err := form.Validate() - - if tc.expectValidationError && err == nil { - t.Fatal("expected validation error but got none", err) - } - - if !tc.expectValidationError && err != nil { - t.Fatalf("expected no validation error but got: %v", err) - } - }) - } -} - -func TestConvertUrlRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing mandatory url form field", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "empty url form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "url": { - "", - }, - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "url": { - "foo", - }, - }) - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return errors.New("foo") - }}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "url": { - "foo", - }, - }) - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := convertUrlRoute(tc.api, nil).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestScreenshotUrlRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing mandatory url form field", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "empty url form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "url": { - "", - }, - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "url": { - "foo", - }, - }) - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return errors.New("foo") - }}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "url": { - "foo", - }, - }) - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := screenshotUrlRoute(tc.api).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestConvertHtmlRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing mandatory index.html form file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "index.html": "/index.html", - }) - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return errors.New("foo") - }}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "index.html": "/index.html", - }) - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := convertHtmlRoute(tc.api, nil).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestScreenshotHtmlRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing mandatory index.html form file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "index.html": "/index.html", - }) - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return errors.New("foo") - }}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "index.html": "/index.html", - }) - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := screenshotHtmlRoute(tc.api).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestConvertMarkdownRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing mandatory index.html form file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "missing mandatory markdown form files", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "index.html": "/index.html", - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "markdown file requested in index.html not found", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "wrong_name.md": fmt.Sprintf("%s/wrong_name.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "non-existing markdown file", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "markdown.md": fmt.Sprintf("%s/markdown.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "markdown.md": fmt.Sprintf("%s/markdown.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/markdown.md", dirPath), []byte("# Hello World!"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return errors.New("foo") - }}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "markdown.md": fmt.Sprintf("%s/markdown.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/markdown.md", dirPath), []byte("# Hello World!"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - if tc.ctx.DirPath() != "" { - defer func() { - err := os.RemoveAll(tc.ctx.DirPath()) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - } - - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := convertMarkdownRoute(tc.api, nil).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestScreenshotMarkdownRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing mandatory index.html form file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "missing mandatory markdown form files", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "index.html": "/index.html", - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "markdown file requested in index.html not found", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "wrong_name.md": fmt.Sprintf("%s/wrong_name.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "non-existing markdown file", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "markdown.md": fmt.Sprintf("%s/markdown.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "markdown.md": fmt.Sprintf("%s/markdown.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/markdown.md", dirPath), []byte("# Hello World!"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return errors.New("foo") - }}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetDirPath(dirPath) - ctx.SetFiles(map[string]string{ - "index.html": fmt.Sprintf("%s/index.html", dirPath), - "markdown.md": fmt.Sprintf("%s/markdown.md", dirPath), - }) - - err := os.MkdirAll(dirPath, 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/index.html", dirPath), []byte("
{{ toHTML \"markdown.md\" }}
"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/markdown.md", dirPath), []byte("# Hello World!"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - if tc.ctx.DirPath() != "" { - defer func() { - err := os.RemoveAll(tc.ctx.DirPath()) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - } - - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := screenshotMarkdownRoute(tc.api).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestConvertUrl(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - engine gotenberg.PdfEngine - options PdfOptions - splitMode gotenberg.SplitMode - pdfFormats gotenberg.PdfFormats - metadata map[string]interface{} - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "ErrOmitBackgroundWithoutPrintBackground", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrOmitBackgroundWithoutPrintBackground - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidEvaluationExpression (without waitForExpression form field)", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrInvalidEvaluationExpression - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidEvaluationExpression (with waitForExpression form field)", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrInvalidEvaluationExpression - }}, - options: func() PdfOptions { - options := DefaultPdfOptions() - options.WaitForExpression = "foo" - - return options - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidPrinterSettings", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrInvalidPrinterSettings - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrPageRangesSyntaxError", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrPageRangesSyntaxError - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidHttpStatusCode", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrInvalidHttpStatusCode - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidResourceHttpStatusCode", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrInvalidResourceHttpStatusCode - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrConsoleExceptions", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrConsoleExceptions - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrLoadingFailed", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrLoadingFailed - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrResourceLoadingFailed", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return ErrResourceLoadingFailed - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return errors.New("foo") - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "PDF engine split error", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return nil, errors.New("foo") - }}, - options: DefaultPdfOptions(), - splitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success with split mode", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }}, - options: DefaultPdfOptions(), - splitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "PDF engine convert error", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return errors.New("foo") - }}, - options: DefaultPdfOptions(), - pdfFormats: gotenberg.PdfFormats{PdfA: "foo"}, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success with PDF formats", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }}, - options: DefaultPdfOptions(), - pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "success with split mode and PDF formats", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - options: DefaultPdfOptions(), - splitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"}, - pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "PDF engine write metadata error", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return errors.New("foo") - }}, - options: DefaultPdfOptions(), - metadata: map[string]interface{}{ - "Creator": "foo", - "Producer": "bar", - }, - expectError: true, - expectHttpError: false, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetCancelled(true) - return ctx - }(), - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - options: DefaultPdfOptions(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error { - return nil - }}, - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - }, - options: DefaultPdfOptions(), - pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}, - metadata: map[string]interface{}{ - "Creator": "foo", - "Producer": "bar", - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.options, tc.splitMode, tc.pdfFormats, tc.metadata) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestScreenshotUrl(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - api Api - options ScreenshotOptions - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "ErrInvalidEvaluationExpression (without waitForExpression form field)", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrInvalidEvaluationExpression - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidEvaluationExpression (with waitForExpression form field)", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrInvalidEvaluationExpression - }}, - options: func() ScreenshotOptions { - options := DefaultScreenshotOptions() - options.WaitForExpression = "foo" - - return options - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidHttpStatusCode", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrInvalidHttpStatusCode - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrInvalidResourceHttpStatusCode", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrInvalidResourceHttpStatusCode - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrConsoleExceptions", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrConsoleExceptions - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrLoadingFailed", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrLoadingFailed - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrResourceLoadingFailed", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return ErrResourceLoadingFailed - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusConflict, - expectOutputPathsCount: 0, - }, - { - scenario: "error from Chromium", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return errors.New("foo") - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetCancelled(true) - return ctx - }(), - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }}, - options: DefaultScreenshotOptions(), - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: &api.ContextMock{Context: new(api.Context)}, - api: &ApiMock{ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error { - return nil - }}, - options: DefaultScreenshotOptions(), - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - err := screenshotUrl(tc.ctx.Context, tc.api, "", tc.options) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} diff --git a/pkg/modules/exiftool/exiftool_test.go b/pkg/modules/exiftool/exiftool_test.go deleted file mode 100644 index 87576b69..00000000 --- a/pkg/modules/exiftool/exiftool_test.go +++ /dev/null @@ -1,415 +0,0 @@ -package exiftool - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "reflect" - "testing" - - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestExifTool_Descriptor(t *testing.T) { - descriptor := new(ExifTool).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(ExifTool)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestExifTool_Provision(t *testing.T) { - engine := new(ExifTool) - ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil) - - err := engine.Provision(ctx) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestExifTool_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - binPath string - expectError bool - }{ - { - scenario: "empty bin path", - binPath: "", - expectError: true, - }, - { - scenario: "bin path does not exist", - binPath: "/foo", - expectError: true, - }, - { - scenario: "validate success", - binPath: os.Getenv("EXIFTOOL_BIN_PATH"), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(ExifTool) - engine.binPath = tc.binPath - err := engine.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestExifTool_Debug(t *testing.T) { - for _, tc := range []struct { - scenario string - engine *ExifTool - expect map[string]interface{} - doNotExpect map[string]interface{} - }{ - { - scenario: "cannot determine version", - engine: &ExifTool{ - binPath: "foo", - }, - expect: map[string]interface{}{ - "version": `exec: "foo": executable file not found in $PATH`, - }, - }, - { - scenario: "success", - engine: &ExifTool{ - binPath: "echo", - }, - doNotExpect: map[string]interface{}{ - "version": `exec: "echo": executable file not found in $PATH`, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - d := tc.engine.Debug() - - if tc.expect != nil { - if !reflect.DeepEqual(d, tc.expect) { - t.Errorf("expected '%v' but got '%v'", tc.expect, d) - } - } - - if tc.doNotExpect != nil { - if reflect.DeepEqual(d, tc.doNotExpect) { - t.Errorf("did not expect '%v'", d) - } - } - }) - } -} - -func TestExiftool_Merge(t *testing.T) { - engine := new(ExifTool) - err := engine.Merge(context.Background(), zap.NewNop(), nil, "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestExiftool_Split(t *testing.T) { - engine := new(ExifTool) - _, err := engine.Split(context.Background(), zap.NewNop(), gotenberg.SplitMode{}, "", "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestExiftool_Flatten(t *testing.T) { - engine := new(ExifTool) - err := engine.Flatten(context.Background(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestExiftool_Convert(t *testing.T) { - engine := new(ExifTool) - err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestExiftool_ReadMetadata(t *testing.T) { - for _, tc := range []struct { - scenario string - inputPath string - expectMetadata map[string]interface{} - expectError bool - }{ - { - scenario: "invalid input path", - inputPath: "foo", - expectMetadata: nil, - expectError: true, - }, - { - scenario: "success", - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - expectMetadata: map[string]interface{}{ - "FileName": "sample1.pdf", - "FileTypeExtension": "pdf", - "MIMEType": "application/pdf", - "PDFVersion": 1.4, - "PageCount": float64(3), - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(ExifTool) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), tc.inputPath) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectMetadata != nil && err == nil { - for k, v := range tc.expectMetadata { - if v2, ok := metadata[k]; !ok || v != v2 { - t.Errorf("expected entry %s with value %v to exists", k, v) - } - } - } - }) - } -} - -func TestExiftool_WriteMetadata(t *testing.T) { - for _, tc := range []struct { - scenario string - createCopy bool - inputPath string - metadata map[string]interface{} - expectMetadata map[string]interface{} - expectError bool - expectedError error - }{ - { - scenario: "invalid input path", - createCopy: false, - inputPath: "foo", - expectError: true, - }, - { - scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (not string array)", - createCopy: true, - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - metadata: map[string]interface{}{ - "Unsupported": []interface{}{ - "foo", - 1, - }, - }, - expectError: true, - expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported, - }, - { - scenario: "gotenberg.ErrPdfEngineMetadataValueNotSupported (default)", - createCopy: true, - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - metadata: map[string]interface{}{ - "Unsupported": map[string]interface{}{}, - }, - expectError: true, - expectedError: gotenberg.ErrPdfEngineMetadataValueNotSupported, - }, - { - scenario: "success (interface array to string array)", - createCopy: true, - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - metadata: map[string]interface{}{ - "Keywords": []interface{}{ - "first", - "second", - }, - }, - expectMetadata: map[string]interface{}{ - "Keywords": []interface{}{ - "first", - "second", - }, - }, - expectError: false, - }, - { - scenario: "success", - createCopy: true, - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - metadata: map[string]interface{}{ - "Author": "Julien Neuhart", - "Copyright": "Julien Neuhart", - "CreationDate": "2006-09-18T16:27:50-04:00", - "Creator": "Gotenberg", - "Keywords": []string{ - "first", - "second", - }, - "Marked": true, - "ModDate": "2006-09-18T16:27:50-04:00", - "PDFVersion": 1.7, - "Producer": "Gotenberg", - "Subject": "Sample", - "Title": "Sample", - "Trapped": "Unknown", - // Those are not valid PDF metadata. - "int": 1, - "int64": int64(2), - "float32": float32(2.2), - "float64": 3.3, - }, - expectMetadata: map[string]interface{}{ - "Author": "Julien Neuhart", - "Copyright": "Julien Neuhart", - "CreationDate": "2006:09:18 16:27:50-04:00", - "Creator": "Gotenberg", - "Keywords": []interface{}{ - "first", - "second", - }, - "Marked": true, - "ModDate": "2006:09:18 16:27:50-04:00", - "PDFVersion": 1.7, - "Producer": "Gotenberg", - "Subject": "Sample", - "Title": "Sample", - "Trapped": "Unknown", - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(ExifTool) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var destinationPath string - if tc.createCopy { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error no but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - destinationPath = fmt.Sprintf("%s/copy_temp.pdf", outputDir) - source, err := os.Open(tc.inputPath) - if err != nil { - t.Fatalf("open source file: %v", err) - } - - defer func(source *os.File) { - err := source.Close() - if err != nil { - t.Fatalf("close file: %v", err) - } - }(source) - - destination, err := os.Create(destinationPath) - if err != nil { - t.Fatalf("create destination file: %v", err) - } - - defer func(destination *os.File) { - err := destination.Close() - if err != nil { - t.Fatalf("close file: %v", err) - } - }(destination) - - _, err = io.Copy(destination, source) - if err != nil { - t.Fatalf("copy source into destination: %v", err) - } - } else { - destinationPath = tc.inputPath - } - - err = engine.WriteMetadata(context.Background(), zap.NewNop(), tc.metadata, destinationPath) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - if tc.expectError { - return - } - - metadata, err := engine.ReadMetadata(context.Background(), zap.NewNop(), destinationPath) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectMetadata != nil && err == nil { - for k, v := range tc.expectMetadata { - v2, ok := metadata[k] - if !ok { - t.Errorf("expected entry %s with value %v to exists, but got none", k, v) - continue - } - - switch v2.(type) { - case []interface{}: - for i, entry := range v.([]interface{}) { - if entry != v2.([]interface{})[i] { - t.Errorf("expected entry %s to contain value %v, but got %v", k, entry, v2.([]interface{})[i]) - } - } - default: - if v != v2 { - t.Errorf("expected entry %s with value %v to exists, but got %v", k, v, v2) - } - } - } - } - }) - } -} diff --git a/pkg/modules/libreoffice/api/api_test.go b/pkg/modules/libreoffice/api/api_test.go deleted file mode 100644 index 7df16d9c..00000000 --- a/pkg/modules/libreoffice/api/api_test.go +++ /dev/null @@ -1,522 +0,0 @@ -package api - -import ( - "context" - "errors" - "os" - "reflect" - "testing" - "time" - - "github.com/alexliesenfeld/health" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestDefaultOptions(t *testing.T) { - actual := DefaultOptions() - notExpect := Options{} - - if reflect.DeepEqual(actual, notExpect) { - t.Errorf("expected %v and got identical %v", actual, notExpect) - } -} - -func TestApi_Descriptor(t *testing.T) { - descriptor := new(Api).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(Api)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestApi_Provision(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - expectError bool - }{ - { - scenario: "no logger provider", - ctx: func() *gotenberg.Context { - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{}, - ) - }(), - expectError: true, - }, - { - scenario: "no logger from logger provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "provision success", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.LoggerProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) { - return zap.NewNop(), nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Api).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - err := a.Provision(tc.ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestApi_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - binPath string - unoBinPath string - expectError bool - }{ - { - scenario: "empty LibreOffice bin path", - binPath: "", - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - expectError: true, - }, - { - scenario: "LibreOffice bin path does not exist", - binPath: "/foo", - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - expectError: true, - }, - { - scenario: "empty uno bin path", - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - unoBinPath: "", - expectError: true, - }, - { - scenario: "uno bin path does not exist", - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - unoBinPath: "/foo", - expectError: true, - }, - { - scenario: "validate success", - binPath: os.Getenv("CHROMIUM_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - a.args = libreOfficeArguments{ - binPath: tc.binPath, - unoBinPath: tc.unoBinPath, - } - err := a.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestApi_Start(t *testing.T) { - for _, tc := range []struct { - scenario string - autoStart bool - supervisor *gotenberg.ProcessSupervisorMock - expectError bool - }{ - { - scenario: "no auto-start", - autoStart: false, - expectError: false, - }, - { - scenario: "auto-start success", - autoStart: true, - supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error { - return nil - }}, - expectError: false, - }, - { - scenario: "auto-start failed", - autoStart: true, - supervisor: &gotenberg.ProcessSupervisorMock{LaunchMock: func() error { - return errors.New("foo") - }}, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - a.autoStart = tc.autoStart - a.supervisor = tc.supervisor - - err := a.Start() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestApi_StartupMessage(t *testing.T) { - a := new(Api) - - a.autoStart = true - autoStartMsg := a.StartupMessage() - - a.autoStart = false - noAutoStartMsg := a.StartupMessage() - - if autoStartMsg == noAutoStartMsg { - t.Errorf("expected differrent startup messages based on auto start, but got '%s'", autoStartMsg) - } -} - -func TestApi_Stop(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor *gotenberg.ProcessSupervisorMock - expectError bool - }{ - { - scenario: "stop success", - supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error { - return nil - }}, - expectError: false, - }, - { - scenario: "stop failed", - supervisor: &gotenberg.ProcessSupervisorMock{ShutdownMock: func() error { - return errors.New("foo") - }}, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - a.logger = zap.NewNop() - a.supervisor = tc.supervisor - - ctx, cancel := context.WithTimeout(context.Background(), 0*time.Second) - cancel() - - err := a.Stop(ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPdfTk_Debug(t *testing.T) { - for _, tc := range []struct { - scenario string - a *Api - expect map[string]interface{} - doNotExpect map[string]interface{} - }{ - { - scenario: "cannot determine version", - a: &Api{ - args: libreOfficeArguments{ - binPath: "foo", - }, - }, - expect: map[string]interface{}{ - "version": `exec: "foo": executable file not found in $PATH`, - }, - }, - { - scenario: "success", - a: &Api{ - args: libreOfficeArguments{ - binPath: "echo", - }, - }, - doNotExpect: map[string]interface{}{ - "version": `exec: "echo": executable file not found in $PATH`, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - d := tc.a.Debug() - - if tc.expect != nil { - if !reflect.DeepEqual(d, tc.expect) { - t.Errorf("expected '%v' but got '%v'", tc.expect, d) - } - } - - if tc.doNotExpect != nil { - if reflect.DeepEqual(d, tc.doNotExpect) { - t.Errorf("did not expect '%v'", d) - } - } - }) - } -} - -func TestApi_Metrics(t *testing.T) { - a := new(Api) - a.supervisor = &gotenberg.ProcessSupervisorMock{ - ReqQueueSizeMock: func() int64 { - return 10 - }, - RestartsCountMock: func() int64 { - return 0 - }, - } - - metrics, err := a.Metrics() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if len(metrics) != 2 { - t.Fatalf("expected %d metrics, but got %d", 2, len(metrics)) - } - - actual := metrics[0].Read() - if actual != float64(10) { - t.Errorf("expected %f for libreoffice_requests_queue_size, but got %f", float64(10), actual) - } - - actual = metrics[1].Read() - if actual != float64(0) { - t.Errorf("expected %f for libreoffice_restarts_count, but got %f", float64(0), actual) - } -} - -func TestApi_Checks(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor gotenberg.ProcessSupervisor - expectAvailabilityStatus health.AvailabilityStatus - }{ - { - scenario: "healthy module", - supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool { - return true - }}, - expectAvailabilityStatus: health.StatusUp, - }, - { - scenario: "unhealthy module", - supervisor: &gotenberg.ProcessSupervisorMock{HealthyMock: func() bool { - return false - }}, - expectAvailabilityStatus: health.StatusDown, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - a.supervisor = tc.supervisor - - checks, err := a.Checks() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - checker := health.NewChecker(checks...) - result := checker.Check(context.Background()) - - if result.Status != tc.expectAvailabilityStatus { - t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status) - } - }) - } -} - -func TestChromium_Ready(t *testing.T) { - for _, tc := range []struct { - scenario string - autoStart bool - startTimeout time.Duration - libreOffice libreOffice - expectError bool - }{ - { - scenario: "no auto-start", - autoStart: false, - startTimeout: time.Duration(30) * time.Second, - libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool { - return false - }}}, - expectError: false, - }, - { - scenario: "auto-start: context done", - autoStart: true, - startTimeout: time.Duration(200) * time.Millisecond, - libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool { - return false - }}}, - expectError: true, - }, - { - scenario: "auto-start success", - autoStart: true, - startTimeout: time.Duration(30) * time.Second, - libreOffice: &libreOfficeMock{ProcessMock: gotenberg.ProcessMock{HealthyMock: func(logger *zap.Logger) bool { - return true - }}}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - a.autoStart = tc.autoStart - a.args = libreOfficeArguments{startTimeout: tc.startTimeout} - a.libreOffice = tc.libreOffice - - err := a.Ready() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestApi_LibreOffice(t *testing.T) { - a := new(Api) - - _, err := a.LibreOffice() - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestApi_Pdf(t *testing.T) { - for _, tc := range []struct { - scenario string - supervisor gotenberg.ProcessSupervisor - libreOffice libreOffice - expectError bool - }{ - { - scenario: "PDF task success", - libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return nil - }}, - expectError: false, - }, - { - scenario: "PDF task error", - libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return errors.New("PDF task error") - }}, - expectError: true, - }, - { - scenario: "ErrCoreDumped", - libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return ErrCoreDumped - }}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - a := new(Api) - a.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error { - return task() - }} - a.libreOffice = tc.libreOffice - - err := a.Pdf(context.Background(), zap.NewNop(), "", "", Options{}) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestApi_Extensions(t *testing.T) { - a := new(Api) - extensions := a.Extensions() - - actual := len(extensions) - expect := 130 - - if actual != expect { - t.Errorf("expected %d extensions, but got %d", expect, actual) - } -} diff --git a/pkg/modules/libreoffice/api/libreoffice_test.go b/pkg/modules/libreoffice/api/libreoffice_test.go deleted file mode 100644 index 348d6c18..00000000 --- a/pkg/modules/libreoffice/api/libreoffice_test.go +++ /dev/null @@ -1,699 +0,0 @@ -package api - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "testing" - "time" - - "github.com/google/uuid" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestLibreOfficeProcess_Start(t *testing.T) { - for _, tc := range []struct { - scenario string - libreOffice libreOffice - expectError bool - cleanup bool - }{ - { - scenario: "successful start", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - expectError: false, - cleanup: true, - }, - { - scenario: "LibreOffice already started", - libreOffice: func() libreOffice { - p := new(libreOfficeProcess) - p.isStarted.Store(true) - return p - }(), - expectError: true, - cleanup: false, - }, - { - scenario: "non-exit code 81 on first start", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: "foo", - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - expectError: true, - cleanup: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - logger := zap.NewNop() - err := tc.libreOffice.Start(logger) - - if tc.cleanup { - defer func(p libreOffice, logger *zap.Logger) { - err = p.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.libreOffice, logger) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestLibreOfficeProcess_Stop(t *testing.T) { - for _, tc := range []struct { - scenario string - libreOffice libreOffice - setup func(libreOffice libreOffice, logger *zap.Logger) error - expectError bool - }{ - { - scenario: "successful stop", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - setup: func(p libreOffice, logger *zap.Logger) error { - return p.Start(logger) - }, - expectError: false, - }, - { - scenario: "LibreOffice already stopped", - libreOffice: func() libreOffice { - p := new(libreOfficeProcess) - p.isStarted.Store(false) - return p - }(), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - logger := zap.NewNop() - - if tc.setup != nil { - err := tc.setup(tc.libreOffice, logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - } - - err := tc.libreOffice.Stop(logger) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestLibreOfficeProcess_Healthy(t *testing.T) { - for _, tc := range []struct { - scenario string - libreOffice libreOffice - setup func(libreOffice libreOffice, logger *zap.Logger) error - expectHealthy bool - cleanup bool - }{ - { - scenario: "healthy LibreOffice", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - setup: func(p libreOffice, logger *zap.Logger) error { - return p.Start(logger) - }, - expectHealthy: true, - cleanup: true, - }, - { - scenario: "LibreOffice not started", - libreOffice: func() libreOffice { - p := new(libreOfficeProcess) - p.isStarted.Store(false) - return p - }(), - expectHealthy: false, - cleanup: false, - }, - { - scenario: "unhealthy LibreOffice", - libreOffice: func() libreOffice { - p := new(libreOfficeProcess) - p.isStarted.Store(true) - p.socketPort = 12345 - return p - }(), - expectHealthy: false, - cleanup: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - logger := zap.NewNop() - - if tc.setup != nil { - err := tc.setup(tc.libreOffice, logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - } - - if tc.cleanup { - defer func(p libreOffice, logger *zap.Logger) { - err := p.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.libreOffice, logger) - } - - healthy := tc.libreOffice.Healthy(logger) - - if !tc.expectHealthy && healthy { - t.Fatal("expected unhealthy LibreOffice but got an healthy one") - } - - if tc.expectHealthy && !healthy { - t.Fatal("expected a healthy LibreOffice but got an unhealthy one") - } - }) - } -} - -func TestLibreOfficeProcess_pdf(t *testing.T) { - for _, tc := range []struct { - scenario string - libreOffice libreOffice - fs *gotenberg.FileSystem - options Options - cancelledCtx bool - start bool - expectError bool - expectedError error - }{ - { - scenario: "LibreOffice not started", - libreOffice: func() libreOffice { - p := new(libreOfficeProcess) - p.isStarted.Store(false) - return p - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - cancelledCtx: false, - start: false, - expectError: true, - }, - { - scenario: "ErrInvalidPdfFormats", - libreOffice: func() libreOffice { - p := new(libreOfficeProcess) - p.socketPort = 12345 - p.isStarted.Store(true) - return p - }(), - fs: gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)), - options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}}, - cancelledCtx: false, - start: false, - expectError: true, - expectedError: ErrInvalidPdfFormats, - }, - { - scenario: "ErrUnoException", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - options: Options{PageRanges: "foo"}, - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Context done"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - cancelledCtx: false, - start: true, - expectError: true, - expectedError: ErrUnoException, - }, - { - scenario: "ErrRuntimeException", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - options: Options{Password: "foo"}, - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - in, err := os.Open("/tests/test/testdata/libreoffice/protected.docx") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - defer func() { - err := in.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - - out, err := os.Create(fmt.Sprintf("%s/protected.docx", fs.WorkingDirPath())) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - defer func() { - err := out.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - - _, err = io.Copy(out, in) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - cancelledCtx: false, - start: true, - expectError: true, - expectedError: ErrRuntimeException, - }, - { - scenario: "context done", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Context done"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - cancelledCtx: true, - start: true, - expectError: true, - }, - { - scenario: "success (default options)", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Success"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - cancelledCtx: false, - start: true, - expectError: false, - }, - { - scenario: "success (not default options)", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Success"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: Options{ - Password: "", // Ok, the only exception in this list. - Landscape: true, - PageRanges: "1", - ExportFormFields: false, - AllowDuplicateFieldNames: true, - ExportBookmarks: false, - ExportBookmarksToPdfDestination: true, - ExportPlaceholders: true, - ExportNotes: true, - ExportNotesPages: true, - ExportOnlyNotesPages: true, - ExportNotesInMargin: true, - ConvertOooTargetToPdfTarget: true, - ExportLinksRelativeFsys: true, - ExportHiddenSlides: true, - SkipEmptyPages: true, - AddOriginalDocumentAsStream: true, - SinglePageSheets: true, - LosslessImageCompression: true, - Quality: 100, - ReduceImageResolution: true, - MaxImageResolution: 600, - }, - cancelledCtx: false, - start: true, - expectError: false, - }, - { - scenario: "success (PDF/A-1b)", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1b}}, - cancelledCtx: false, - start: true, - expectError: false, - }, - { - scenario: "success (PDF/A-2b)", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA2b}}, - cancelledCtx: false, - start: true, - expectError: false, - }, - { - scenario: "success (PDF/A-3b)", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA3b}}, - cancelledCtx: false, - start: true, - expectError: false, - }, - { - scenario: "success (PDF/UA)", - libreOffice: newLibreOfficeProcess( - libreOfficeArguments{ - binPath: os.Getenv("LIBREOFFICE_BIN_PATH"), - unoBinPath: os.Getenv("UNOCONVERTER_BIN_PATH"), - startTimeout: 5 * time.Second, - }, - ), - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - options: Options{PdfFormats: gotenberg.PdfFormats{PdfUa: true}}, - cancelledCtx: false, - start: true, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - // Force the debug level. - logger := zap.NewExample() - - defer func() { - err := os.RemoveAll(tc.fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }() - - if tc.start { - err := tc.libreOffice.Start(logger) - if err != nil { - t.Fatalf("setup error: %v", err) - } - - defer func(p libreOffice, logger *zap.Logger) { - err = p.Stop(logger) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }(tc.libreOffice, logger) - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(5)*time.Second) - defer cancel() - - if tc.cancelledCtx { - cancel() - } - - err := tc.libreOffice.pdf( - ctx, - logger, - fmt.Sprintf("%s/document.txt", tc.fs.WorkingDirPath()), - fmt.Sprintf("%s/%s.pdf", tc.fs.WorkingDirPath(), uuid.NewString()), - tc.options, - ) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - }) - } -} - -func TestNonBasicLatinCharactersGuard(t *testing.T) { - for _, tc := range []struct { - scenario string - fs *gotenberg.FileSystem - filename string - expectSameInputPath bool - expectError bool - }{ - { - scenario: "basic latin characters", - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Basic latin characters"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - filename: "document.txt", - expectSameInputPath: true, - expectError: false, - }, - { - scenario: "non-basic latin characters", - fs: func() *gotenberg.FileSystem { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - - err := os.MkdirAll(fs.WorkingDirPath(), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - err = os.WriteFile(fmt.Sprintf("%s/รฉรจรŸร รนรค.txt", fs.WorkingDirPath()), []byte("Non-basic latin characters"), 0o755) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return fs - }(), - filename: "รฉรจรŸร รนรค.txt", - expectSameInputPath: false, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - defer func() { - err := os.RemoveAll(tc.fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up, but got: %v", err) - } - }() - - inputPath := fmt.Sprintf("%s/%s", tc.fs.WorkingDirPath(), tc.filename) - newInputPath, err := nonBasicLatinCharactersGuard( - zap.NewNop(), - inputPath, - ) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectSameInputPath && newInputPath != inputPath { - t.Fatalf("expected same input path, but got '%s'", newInputPath) - } - - if !tc.expectSameInputPath && newInputPath == inputPath { - t.Fatalf("expected different input path, but got same '%s'", newInputPath) - } - }) - } -} diff --git a/pkg/modules/libreoffice/api/mocks_test.go b/pkg/modules/libreoffice/api/mocks_test.go deleted file mode 100644 index f2a03530..00000000 --- a/pkg/modules/libreoffice/api/mocks_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package api - -import ( - "context" - "testing" - - "go.uber.org/zap" -) - -func TestApiMock(t *testing.T) { - mock := &ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return nil - }, - ExtensionsMock: func() []string { - return nil - }, - } - - err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{}) - if err != nil { - t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err) - } - - ext := mock.Extensions() - if ext != nil { - t.Errorf("expected nil result from ApiMock.Extensions, but got: %v", ext) - } -} - -func TestProviderMock(t *testing.T) { - mock := &ProviderMock{ - LibreOfficeMock: func() (Uno, error) { - return nil, nil - }, - } - - _, err := mock.LibreOffice() - if err != nil { - t.Errorf("expected no error from ProviderMock.LibreOffice, but got: %v", err) - } -} - -func TestLibreOfficeMock(t *testing.T) { - for _, tc := range []struct { - scenario string - mock *libreOfficeMock - expectError bool - }{ - { - scenario: "success", - mock: &libreOfficeMock{ - pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return nil - }, - }, - expectError: false, - }, - { - scenario: "ErrCoreDumped (first call)", - mock: &libreOfficeMock{ - pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return ErrCoreDumped - }, - }, - expectError: true, - }, - { - scenario: "ErrCoreDumped (second call)", - mock: func() *libreOfficeMock { - m := &libreOfficeMock{ - pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error { - return ErrCoreDumped - }, - } - m.pdf(context.Background(), zap.NewNop(), "", "", Options{}) - return m - }(), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - err := tc.mock.pdf(context.Background(), zap.NewNop(), "", "", Options{}) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error from libreOfficeMock.pdf but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error from libreOfficeMock.pdf but got none") - } - }) - } -} diff --git a/pkg/modules/libreoffice/libreoffice_test.go b/pkg/modules/libreoffice/libreoffice_test.go deleted file mode 100644 index 4d756a07..00000000 --- a/pkg/modules/libreoffice/libreoffice_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package libreoffice - -import ( - "errors" - "reflect" - "testing" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" - libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api" -) - -func TestLibreOffice_Descriptor(t *testing.T) { - descriptor := new(LibreOffice).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(LibreOffice)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestLibreOffice_Provision(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - expectError bool - }{ - { - scenario: "no LibreOffice API provider", - ctx: func() *gotenberg.Context { - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOffice).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{}, - ) - }(), - expectError: true, - }, - { - scenario: "no LibreOffice API from LibreOffice API provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - libreofficeapi.ProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOffice).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no PDF engine provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - libreofficeapi.ProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) { - return new(libreofficeapi.ApiMock), nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOffice).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "no PDF engine from PDF engine provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - libreofficeapi.ProviderMock - gotenberg.PdfEngineProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) { - return new(libreofficeapi.ApiMock), nil - } - mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOffice).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "provision success", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - libreofficeapi.ProviderMock - gotenberg.PdfEngineProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }} - } - mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) { - return new(libreofficeapi.ApiMock), nil - } - mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) { - return new(gotenberg.PdfEngineMock), nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOffice).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(LibreOffice) - err := mod.Provision(tc.ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestLibreOffice_Routes(t *testing.T) { - for _, tc := range []struct { - scenario string - expectRoutes int - disableRoutes bool - }{ - { - scenario: "routes not disabled", - expectRoutes: 1, - disableRoutes: false, - }, - { - scenario: "routes disabled", - expectRoutes: 0, - disableRoutes: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(LibreOffice) - mod.disableRoutes = tc.disableRoutes - - routes, err := mod.Routes() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectRoutes != len(routes) { - t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes)) - } - }) - } -} diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine_test.go b/pkg/modules/libreoffice/pdfengine/pdfengine_test.go deleted file mode 100644 index d7034c2e..00000000 --- a/pkg/modules/libreoffice/pdfengine/pdfengine_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package pdfengine - -import ( - "context" - "errors" - "reflect" - "testing" - - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" - "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api" -) - -func TestLibreOfficePdfEngine_Descriptor(t *testing.T) { - descriptor := new(LibreOfficePdfEngine).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(LibreOfficePdfEngine)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestLibreOfficePdfEngine_Provider(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - expectError bool - }{ - { - scenario: "no LibreOffice API provider", - ctx: gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{}, - ), - expectError: true, - }, - { - scenario: "no API from LibreOffice API provider", - ctx: func() *gotenberg.Context { - provider := &struct { - gotenberg.ModuleMock - api.ProviderMock - }{} - provider.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { - return provider - }} - } - provider.LibreOfficeMock = func() (api.Uno, error) { - return nil, errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - provider.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "provision success", - ctx: func() *gotenberg.Context { - provider := &struct { - gotenberg.ModuleMock - api.ProviderMock - }{} - provider.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { - return provider - }} - } - provider.LibreOfficeMock = func() (api.Uno, error) { - return new(api.ApiMock), nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(LibreOfficePdfEngine).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - provider.Descriptor(), - }, - ) - }(), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(LibreOfficePdfEngine) - err := engine.Provision(tc.ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestLibreOfficePdfEngine_Merge(t *testing.T) { - engine := new(LibreOfficePdfEngine) - err := engine.Merge(context.Background(), zap.NewNop(), nil, "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_Split(t *testing.T) { - engine := new(LibreOfficePdfEngine) - _, err := engine.Split(context.Background(), zap.NewNop(), gotenberg.SplitMode{}, "", "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_Flatten(t *testing.T) { - engine := new(LibreOfficePdfEngine) - err := engine.Flatten(context.Background(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_Convert(t *testing.T) { - for _, tc := range []struct { - scenario string - api api.Uno - expectError bool - expectedError error - }{ - { - scenario: "convert success", - api: &api.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error { - return nil - }, - }, - expectError: false, - }, - { - scenario: "ErrInvalidPdfFormats", - api: &api.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error { - return api.ErrInvalidPdfFormats - }, - }, - expectError: true, - expectedError: gotenberg.ErrPdfFormatNotSupported, - }, - { - scenario: "convert fail", - api: &api.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options api.Options) error { - return errors.New("foo") - }, - }, - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := &LibreOfficePdfEngine{unoApi: tc.api} - err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "") - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - }) - } -} - -func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { - engine := new(LibreOfficePdfEngine) - _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { - engine := new(LibreOfficePdfEngine) - err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go index 47a2846f..113b5d79 100644 --- a/pkg/modules/libreoffice/routes.go +++ b/pkg/modules/libreoffice/routes.go @@ -1,7 +1,6 @@ package libreoffice import ( - "encoding/json" "errors" "fmt" "net/http" @@ -129,15 +128,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap }). Bool("nativePdfFormats", &nativePdfFormats, true). Bool("merge", &merge, false). - Custom("metadata", func(value string) error { - if len(value) > 0 { - err := json.Unmarshal([]byte(value), &metadata) - if err != nil { - return fmt.Errorf("unmarshal metadata: %w", err) - } - } - return nil - }). Bool("flatten", &flatten, false). Validate() if err != nil { diff --git a/pkg/modules/libreoffice/routes_test.go b/pkg/modules/libreoffice/routes_test.go deleted file mode 100644 index c033591d..00000000 --- a/pkg/modules/libreoffice/routes_test.go +++ /dev/null @@ -1,817 +0,0 @@ -package libreoffice - -import ( - "context" - "errors" - "fmt" - "net/http" - "os" - "path/filepath" - "slices" - "testing" - - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" - "github.com/gotenberg/gotenberg/v8/pkg/modules/api" - libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api" -) - -func TestConvertRoute(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - libreOffice libreofficeapi.Uno - engine gotenberg.PdfEngine - expectOptions libreofficeapi.Options - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid quality form field (not an integer)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "quality": { - "foo", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid quality form field (< 1)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "quality": { - "0", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid quality form field (> 100)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "quality": { - "101", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid maxImageResolution form field (not an integer)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "maxImageResolution": { - "foo", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid maxImageResolution form field (not in range)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "maxImageResolution": { - "1", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid metadata form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "foo", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ExtensionsMock: func() []string { - return []string{".docx"} - }}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrPdfFormatNotSupported (nativePdfFormats)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return libreofficeapi.ErrInvalidPdfFormats - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrUnoException", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "nativePageRanges": { - "foo", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return libreofficeapi.ErrUnoException - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrRuntimeException", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "password": { - "invalid", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return libreofficeapi.ErrRuntimeException - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from LibreOffice", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return errors.New("foo") - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "PDF engine merge error", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - "document2.docx": "/document2.docx", - }) - ctx.SetValues(map[string][]string{ - "merge": { - "true", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "PDF engine split error", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return nil, errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "PDF engine convert error", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "nativePdfFormats": { - "false", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "PDF engine write metadata error", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "PDF engine flatten error", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "flatten": { - "true", - }, - }) - ctx.SetCancelled(true) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetCancelled(true) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success (single file)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - "document2.docx": "/document2.docx", - }) - ctx.SetValues(map[string][]string{ - "quality": { - "100", - }, - "maxImageResolution": { - "1200", - }, - "merge": { - "true", - }, - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - "nativePdfFormats": { - "false", - }, - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - "flatten": { - "true", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "success (many files)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - "document2.docx": "/document2.docx", - "document2.doc": "/document2.doc", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - "nativePdfFormats": { - "false", - }, - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - "flatten": { - "true", - }, - }) - ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx", ".doc"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 3, - expectOutputPaths: []string{"/document.docx.pdf", "/document2.docx.pdf", "/document2.doc.pdf"}, - }, - { - scenario: "success with native PDF/A & PDF/UA", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "success with split (many files)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - "document2.docx": "/document2.docx", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))] - filenameNoExt := filepath.Base(inputPathNoExt) - return []string{ - fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, 0, - ), - fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, 1, - ), - }, nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 4, - expectOutputPaths: []string{"/document_docx/document.docx_0.pdf", "/document_docx/document.docx_1.pdf", "/document2_docx/document2.docx_0.pdf", "/document2_docx/document2.docx_1.pdf"}, - }, - { - scenario: "success with merge and split", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - "document2.docx": "/document2.docx", - }) - ctx.SetValues(map[string][]string{ - "merge": { - "true", - }, - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))] - filenameNoExt := filepath.Base(inputPathNoExt) - return []string{ - fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, 0, - ), - fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, 1, - ), - }, nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 2, - }, - { - scenario: "success with split and native PDF/A & PDF/UA (many files)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - "document2.docx": "/document2.docx", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - }) - return ctx - }(), - libreOffice: &libreofficeapi.ApiMock{ - PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error { - return nil - }, - ExtensionsMock: func() []string { - return []string{".docx"} - }, - }, - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - inputPathNoExt := inputPath[:len(inputPath)-len(filepath.Ext(inputPath))] - filenameNoExt := filepath.Base(inputPathNoExt) - return []string{ - fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, 0, - ), - fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, 1, - ), - }, nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 4, - expectOutputPaths: []string{"/document_docx/document.docx_0.pdf", "/document_docx/document.docx_1.pdf", "/document2_docx/document2.docx_0.pdf", "/document2_docx/document2.docx_1.pdf"}, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := convertRoute(tc.libreOffice, tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - - for _, path := range tc.expectOutputPaths { - if !slices.Contains(tc.ctx.OutputPaths(), path) { - t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths()) - } - } - }) - } -} diff --git a/pkg/modules/pdfcpu/pdfcpu_test.go b/pkg/modules/pdfcpu/pdfcpu_test.go deleted file mode 100644 index b1253cfd..00000000 --- a/pkg/modules/pdfcpu/pdfcpu_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package pdfcpu - -import ( - "context" - "errors" - "os" - "reflect" - "testing" - - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestPdfCpu_Descriptor(t *testing.T) { - descriptor := new(PdfCpu).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(PdfCpu)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestPdfCpu_Provision(t *testing.T) { - engine := new(PdfCpu) - ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil) - - err := engine.Provision(ctx) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestPdfCpu_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - binPath string - expectError bool - }{ - { - scenario: "empty bin path", - binPath: "", - expectError: true, - }, - { - scenario: "bin path does not exist", - binPath: "/foo", - expectError: true, - }, - { - scenario: "validate success", - binPath: os.Getenv("PDFTK_BIN_PATH"), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(PdfCpu) - engine.binPath = tc.binPath - err := engine.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPdfCpu_Debug(t *testing.T) { - for _, tc := range []struct { - scenario string - engine *PdfCpu - expect map[string]interface{} - doNotExpect map[string]interface{} - }{ - { - scenario: "cannot determine version (command error)", - engine: &PdfCpu{ - binPath: "foo", - }, - expect: map[string]interface{}{ - "version": `exec: "foo": executable file not found in $PATH`, - }, - }, - { - scenario: "cannot determine version (no pdfcpu)", - engine: &PdfCpu{ - binPath: "echo", - }, - expect: map[string]interface{}{ - "version": "Unable to determine pdfcpu version", - }, - }, - { - scenario: "success", - engine: &PdfCpu{ - binPath: "pdfcpu", - }, - doNotExpect: map[string]interface{}{ - "version": "Unable to determine pdfcpu version", - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - d := tc.engine.Debug() - - if tc.expect != nil { - if !reflect.DeepEqual(d, tc.expect) { - t.Errorf("expected '%v' but got '%v'", tc.expect, d) - } - } - - if tc.doNotExpect != nil { - if reflect.DeepEqual(d, tc.doNotExpect) { - t.Errorf("did not expect '%v'", d) - } - } - }) - } -} - -func TestPdfCpu_Merge(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - inputPaths []string - expectError bool - }{ - { - scenario: "invalid context", - ctx: nil, - expectError: true, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - inputPaths: []string{ - "foo", - }, - expectError: true, - }, - { - scenario: "single file success", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - }, - expectError: false, - }, - { - scenario: "many files success", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - "/tests/test/testdata/pdfengines/sample2.pdf", - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(PdfCpu) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf") - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPdfCpu_Split(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - mode gotenberg.SplitMode - inputPath string - expectError bool - expectedError error - expectOutputPathsCount int - }{ - { - scenario: "ErrPdfSplitModeNotSupported", - expectError: true, - expectedError: gotenberg.ErrPdfSplitModeNotSupported, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid context", - ctx: nil, - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - expectError: true, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - inputPath: "", - expectError: true, - expectOutputPathsCount: 0, - }, - { - scenario: "success (intervals)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - inputPath: "/tests/test/testdata/pdfengines/sample4.pdf", - expectError: false, - expectOutputPathsCount: 20, - }, - { - scenario: "success (pages)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1"}, - inputPath: "/tests/test/testdata/pdfengines/sample4.pdf", - expectError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "success (pages & unify)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - inputPath: "/tests/test/testdata/pdfengines/sample4.pdf", - expectError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(PdfCpu) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - if tc.expectOutputPathsCount != len(outputPaths) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths)) - } - }) - } -} - -func TestPdfCpu_Flatten(t *testing.T) { - mod := new(PdfCpu) - err := mod.Flatten(context.TODO(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestPdfCpu_Convert(t *testing.T) { - mod := new(PdfCpu) - err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { - engine := new(PdfCpu) - _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { - engine := new(PdfCpu) - err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} diff --git a/pkg/modules/pdfengines/pdfengines_test.go b/pkg/modules/pdfengines/pdfengines_test.go deleted file mode 100644 index b0beed3e..00000000 --- a/pkg/modules/pdfengines/pdfengines_test.go +++ /dev/null @@ -1,417 +0,0 @@ -package pdfengines - -import ( - "errors" - "fmt" - "reflect" - "strings" - "testing" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestPdfEngines_Descriptor(t *testing.T) { - descriptor := new(PdfEngines).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(PdfEngines)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestPdfEngines_Provision(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - expectedMergePdfEngines []string - expectedSplitPdfEngines []string - expectedFlattenPdfEngines []string - expectedConvertPdfEngines []string - expectedReadMetadataPdfEngines []string - expectedWriteMetadataPdfEngines []string - expectError bool - }{ - { - scenario: "no selection from user", - ctx: func() *gotenberg.Context { - provider := &struct { - gotenberg.ModuleMock - }{} - provider.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { - return provider - }} - } - - engine := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.PdfEngineMock - }{} - engine.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }} - } - engine.ValidateMock = func() error { - return nil - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(PdfEngines).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - provider.Descriptor(), - engine.Descriptor(), - }, - ) - }(), - expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"}, - expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"}, - expectedFlattenPdfEngines: []string{"qpdf"}, - expectedConvertPdfEngines: []string{"libreoffice-pdfengine"}, - expectedReadMetadataPdfEngines: []string{"exiftool"}, - expectedWriteMetadataPdfEngines: []string{"exiftool"}, - expectError: false, - }, - { - scenario: "selection from user", - ctx: func() *gotenberg.Context { - provider := &struct { - gotenberg.ModuleMock - }{} - provider.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { - return provider - }} - } - engine1 := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.PdfEngineMock - }{} - engine1.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }} - } - engine1.ValidateMock = func() error { - return nil - } - - engine2 := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.PdfEngineMock - }{} - engine2.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }} - } - engine2.ValidateMock = func() error { - return nil - } - - fs := new(PdfEngines).Descriptor().FlagSet - err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-split-engines=a", "--pdfengines-flatten-engines=c", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - []gotenberg.ModuleDescriptor{ - provider.Descriptor(), - engine1.Descriptor(), - engine2.Descriptor(), - }, - ) - }(), - - expectedMergePdfEngines: []string{"b"}, - expectedSplitPdfEngines: []string{"a"}, - expectedFlattenPdfEngines: []string{"c"}, - expectedConvertPdfEngines: []string{"b"}, - expectedReadMetadataPdfEngines: []string{"a"}, - expectedWriteMetadataPdfEngines: []string{"a"}, - expectError: false, - }, - { - scenario: "no valid PDF engine", - ctx: func() *gotenberg.Context { - provider := &struct { - gotenberg.ModuleMock - }{} - provider.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { - return provider - }} - } - engine := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.PdfEngineMock - }{} - engine.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }} - } - engine.ValidateMock = func() error { - return errors.New("foo") - } - - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(PdfEngines).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - provider.Descriptor(), - engine.Descriptor(), - }, - ) - }(), - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(PdfEngines) - err := mod.Provision(tc.ctx) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if len(tc.expectedMergePdfEngines) != len(mod.mergeNames) { - t.Fatalf("expected %d merge names but got %d", len(tc.expectedMergePdfEngines), len(mod.mergeNames)) - } - - if len(tc.expectedFlattenPdfEngines) != len(mod.flattenNames) { - t.Fatalf("expected %d flatten names but got %d", len(tc.expectedFlattenPdfEngines), len(mod.flattenNames)) - } - - if len(tc.expectedConvertPdfEngines) != len(mod.convertNames) { - t.Fatalf("expected %d convert names but got %d", len(tc.expectedConvertPdfEngines), len(mod.convertNames)) - } - - if len(tc.expectedReadMetadataPdfEngines) != len(mod.readMetadataNames) { - t.Fatalf("expected %d read metadata names but got %d", len(tc.expectedReadMetadataPdfEngines), len(mod.readMetadataNames)) - } - - if len(tc.expectedWriteMetadataPdfEngines) != len(mod.writeMetadataNames) { - t.Fatalf("expected %d write metadata names but got %d", len(tc.expectedWriteMetadataPdfEngines), len(mod.writeMetadataNames)) - } - - for index, name := range mod.mergeNames { - if name != tc.expectedMergePdfEngines[index] { - t.Fatalf("expected merge name at index %d to be %s, but got: %s", index, name, tc.expectedMergePdfEngines[index]) - } - } - - for index, name := range mod.splitNames { - if name != tc.expectedSplitPdfEngines[index] { - t.Fatalf("expected split name at index %d to be %s, but got: %s", index, name, tc.expectedSplitPdfEngines[index]) - } - } - - for index, name := range mod.convertNames { - if name != tc.expectedConvertPdfEngines[index] { - t.Fatalf("expected convert name at index %d to be %s, but got: %s", index, name, tc.expectedConvertPdfEngines[index]) - } - } - - for index, name := range mod.readMetadataNames { - if name != tc.expectedReadMetadataPdfEngines[index] { - t.Fatalf("expected read metadata name at index %d to be %s, but got: %s", index, name, tc.expectedReadMetadataPdfEngines[index]) - } - } - - for index, name := range mod.writeMetadataNames { - if name != tc.expectedWriteMetadataPdfEngines[index] { - t.Fatalf("expected write metadat name at index %d to be %s, but got: %s", index, name, tc.expectedWriteMetadataPdfEngines[index]) - } - } - }) - } -} - -func TestPdfEngines_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - names []string - engines []gotenberg.PdfEngine - expectError bool - }{ - { - scenario: "existing PDF engine", - names: []string{"foo"}, - engines: func() []gotenberg.PdfEngine { - engine := &struct { - gotenberg.ModuleMock - gotenberg.PdfEngineMock - }{} - engine.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }} - } - - return []gotenberg.PdfEngine{ - engine, - } - }(), - expectError: false, - }, - { - scenario: "non-existing bar PDF engine", - names: []string{"foo", "bar", "baz"}, - engines: func() []gotenberg.PdfEngine { - engine1 := &struct { - gotenberg.ModuleMock - gotenberg.PdfEngineMock - }{} - engine1.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }} - } - - engine2 := &struct { - gotenberg.ModuleMock - gotenberg.PdfEngineMock - }{} - engine2.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }} - } - - return []gotenberg.PdfEngine{ - engine1, - engine2, - } - }(), - expectError: true, - }, - { - scenario: "no PDF engine", - expectError: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := PdfEngines{ - mergeNames: tc.names, - convertNames: tc.names, - readMetadataNames: tc.names, - writeMetadataNames: tc.names, - engines: tc.engines, - } - - err := mod.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPdfEngines_SystemMessages(t *testing.T) { - mod := new(PdfEngines) - mod.mergeNames = []string{"foo", "bar"} - mod.splitNames = []string{"foo", "bar"} - mod.convertNames = []string{"foo", "bar"} - mod.readMetadataNames = []string{"foo", "bar"} - mod.writeMetadataNames = []string{"foo", "bar"} - - expectedMessages := 6 - messages := mod.SystemMessages() - if len(messages) != expectedMessages { - t.Errorf("expected %d message(s), but got %d", expectedMessages, len(messages)) - } - - expect := []string{ - fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")), - fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")), - fmt.Sprintf("flatten engines - %s", strings.Join(mod.flattenNames[:], " ")), - fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")), - fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")), - fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")), - } - - for i, message := range messages { - if message != expect[i] { - t.Errorf("expected message at index %d to be %s, but got %s", i, message, expect[i]) - } - } -} - -func TestPdfEngines_PdfEngine(t *testing.T) { - mod := PdfEngines{ - mergeNames: []string{"foo", "bar"}, - splitNames: []string{"foo", "bar"}, - convertNames: []string{"foo", "bar"}, - readMetadataNames: []string{"foo", "bar"}, - writeMetadataNames: []string{"foo", "bar"}, - engines: func() []gotenberg.PdfEngine { - engine1 := &struct { - gotenberg.ModuleMock - gotenberg.PdfEngineMock - }{} - engine1.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }} - } - - engine2 := &struct { - gotenberg.ModuleMock - gotenberg.PdfEngineMock - }{} - engine2.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }} - } - - return []gotenberg.PdfEngine{ - engine1, - engine2, - } - }(), - } - - _, err := mod.PdfEngine() - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestPdfEngines_Routes(t *testing.T) { - for _, tc := range []struct { - scenario string - expectRoutes int - disableRoutes bool - }{ - { - scenario: "routes not disabled", - expectRoutes: 6, - disableRoutes: false, - }, - { - scenario: "routes disabled", - expectRoutes: 0, - disableRoutes: true, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(PdfEngines) - mod.disableRoutes = tc.disableRoutes - - routes, err := mod.Routes() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectRoutes != len(routes) { - t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes)) - } - }) - } -} diff --git a/pkg/modules/pdfengines/routes_test.go b/pkg/modules/pdfengines/routes_test.go deleted file mode 100644 index c50d470e..00000000 --- a/pkg/modules/pdfengines/routes_test.go +++ /dev/null @@ -1,1813 +0,0 @@ -package pdfengines - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "os" - "reflect" - "slices" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" - "github.com/gotenberg/gotenberg/v8/pkg/modules/api" -) - -func TestFormDataPdfSplitMode(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - mandatory bool - expectedSplitMode gotenberg.SplitMode - expectValidationError bool - }{ - { - scenario: "no custom form fields", - ctx: &api.ContextMock{Context: new(api.Context)}, - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{}, - expectValidationError: false, - }, - { - scenario: "no custom form fields (mandatory)", - ctx: &api.ContextMock{Context: new(api.Context)}, - mandatory: true, - expectedSplitMode: gotenberg.SplitMode{}, - expectValidationError: true, - }, - { - scenario: "invalid splitMode", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "foo", - }, - }) - return ctx - }(), - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{}, - expectValidationError: true, - }, - { - scenario: "invalid splitSpan (intervals)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "intervals", - }, - "splitSpan": { - "1-2", - }, - }) - return ctx - }(), - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals}, - expectValidationError: true, - }, - { - scenario: "splitSpan inferior to 1 (intervals)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "intervals", - }, - "splitSpan": { - "-1", - }, - }) - return ctx - }(), - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals}, - expectValidationError: true, - }, - { - scenario: "invalid splitUnify (intervals)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "intervals", - }, - "splitSpan": { - "1", - }, - "splitUnify": { - "true", - }, - }) - return ctx - }(), - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1", Unify: true}, - expectValidationError: true, - }, - { - scenario: "valid form fields (intervals)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "intervals", - }, - "splitSpan": { - "1", - }, - }) - return ctx - }(), - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - expectValidationError: false, - }, - { - scenario: "valid form fields (pages)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "pages", - }, - "splitSpan": { - "1-2", - }, - "splitUnify": { - "true", - }, - }) - return ctx - }(), - mandatory: false, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - expectValidationError: false, - }, - { - scenario: "valid form fields (mandatory)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "splitMode": { - "intervals", - }, - "splitSpan": { - "1", - }, - }) - return ctx - }(), - mandatory: true, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - expectValidationError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - form := tc.ctx.Context.FormData() - actual := FormDataPdfSplitMode(form, tc.mandatory) - - if !reflect.DeepEqual(actual, tc.expectedSplitMode) { - t.Fatalf("expected %+v but got: %+v", tc.expectedSplitMode, actual) - } - - err := form.Validate() - - if tc.expectValidationError && err == nil { - t.Fatal("expected validation error but got none", err) - } - - if !tc.expectValidationError && err != nil { - t.Fatalf("expected no validation error but got: %v", err) - } - }) - } -} - -func TestFormDataPdfFormats(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - expectedPdfFormats gotenberg.PdfFormats - expectValidationError bool - }{ - { - scenario: "no custom form fields", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectedPdfFormats: gotenberg.PdfFormats{}, - expectValidationError: false, - }, - { - scenario: "pdfa and pdfua form fields", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "pdfa": { - "foo", - }, - "pdfua": { - "true", - }, - }) - return ctx - }(), - expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true}, - expectValidationError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - form := tc.ctx.Context.FormData() - actual := FormDataPdfFormats(form) - - if !reflect.DeepEqual(actual, tc.expectedPdfFormats) { - t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual) - } - - err := form.Validate() - - if tc.expectValidationError && err == nil { - t.Fatal("expected validation error but got none", err) - } - - if !tc.expectValidationError && err != nil { - t.Fatalf("expected no validation error but got: %v", err) - } - }) - } -} - -func TestFormDataPdfMetadata(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - mandatory bool - expectedMetadata map[string]interface{} - expectValidationError bool - }{ - { - scenario: "no metadata form field", - ctx: &api.ContextMock{Context: new(api.Context)}, - mandatory: false, - expectedMetadata: nil, - expectValidationError: false, - }, - { - scenario: "no metadata form field (mandatory)", - ctx: &api.ContextMock{Context: new(api.Context)}, - mandatory: true, - expectedMetadata: nil, - expectValidationError: true, - }, - { - scenario: "invalid metadata form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "metadata": { - "foo", - }, - }) - return ctx - }(), - mandatory: false, - expectedMetadata: nil, - expectValidationError: true, - }, - { - scenario: "valid metadata form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetValues(map[string][]string{ - "metadata": { - "{\"foo\":\"bar\"}", - }, - }) - return ctx - }(), - mandatory: false, - expectedMetadata: map[string]interface{}{ - "foo": "bar", - }, - expectValidationError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - form := tc.ctx.Context.FormData() - actual := FormDataPdfMetadata(form, tc.mandatory) - - if !reflect.DeepEqual(actual, tc.expectedMetadata) { - t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual) - } - - err := form.Validate() - - if tc.expectValidationError && err == nil { - t.Fatal("expected validation error but got none", err) - } - - if !tc.expectValidationError && err != nil { - t.Fatalf("expected no validation error but got: %v", err) - } - }) - } -} - -func TestMergeStub(t *testing.T) { - for _, tc := range []struct { - scenario string - engine gotenberg.PdfEngine - inputPaths []string - expectError bool - }{ - { - scenario: "no input path (nil)", - inputPaths: nil, - expectError: true, - }, - { - scenario: "no input path (empty)", - inputPaths: make([]string, 0), - expectError: true, - }, - { - scenario: "only one input path", - inputPaths: []string{"my.pdf"}, - expectError: false, - }, - { - scenario: "merge error", - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return errors.New("foo") - }, - }, - inputPaths: []string{"my.pdf", "my2.pdf"}, - expectError: true, - }, - { - scenario: "merge success", - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - }, - inputPaths: []string{"my.pdf", "my2.pdf"}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - _, err := MergeStub(new(api.Context), tc.engine, tc.inputPaths) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestSplitPdfStub(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - mode gotenberg.SplitMode - expectError bool - }{ - { - scenario: "no split mode", - mode: gotenberg.SplitMode{}, - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: false, - }, - { - scenario: "cannot create subdirectory", - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return errors.New("cannot create subdirectory") - }}) - return ctx - }(), - expectError: true, - }, - { - scenario: "split error", - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return nil, errors.New("foo") - }, - }, - expectError: true, - }, - { - scenario: "rename error", - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return errors.New("cannot rename") - }}) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - }, - expectError: true, - }, - { - scenario: "success (intervals)", - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1"}, - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - }, - expectError: false, - }, - { - scenario: "success (pages)", - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - dirPath := fmt.Sprintf("%s/%s", os.TempDir(), uuid.NewString()) - tc.ctx.SetDirPath(dirPath) - tc.ctx.SetLogger(zap.NewNop()) - - _, err := SplitPdfStub(tc.ctx.Context, tc.engine, tc.mode, []string{"my.pdf", "my2.pdf"}) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestFlattenStub(t *testing.T) { - for _, tc := range []struct { - scenario string - engine gotenberg.PdfEngine - expectError bool - }{ - { - scenario: "flatten error", - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - }, - { - scenario: "flatten success", - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - err := FlattenStub(new(api.Context), tc.engine, []string{"my.pdf", "my2.pdf"}) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestConvertStub(t *testing.T) { - for _, tc := range []struct { - scenario string - engine gotenberg.PdfEngine - pdfFormats gotenberg.PdfFormats - expectError bool - }{ - { - scenario: "no PDF formats", - pdfFormats: gotenberg.PdfFormats{}, - expectError: false, - }, - { - scenario: "convert error", - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return errors.New("foo") - }, - }, - pdfFormats: gotenberg.PdfFormats{ - PdfA: gotenberg.PdfA3b, - PdfUa: true, - }, - expectError: true, - }, - { - scenario: "convert success", - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - pdfFormats: gotenberg.PdfFormats{ - PdfA: gotenberg.PdfA3b, - PdfUa: true, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - _, err := ConvertStub(new(api.Context), tc.engine, tc.pdfFormats, []string{"my.pdf", "my2.pdf"}) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestWriteMetadataStub(t *testing.T) { - for _, tc := range []struct { - scenario string - engine gotenberg.PdfEngine - metadata map[string]interface{} - expectError bool - }{ - { - scenario: "no metadata (nil)", - metadata: nil, - expectError: false, - }, - { - scenario: "no metadata (empty)", - metadata: make(map[string]interface{}, 0), - expectError: false, - }, - { - scenario: "write metadata error", - engine: &gotenberg.PdfEngineMock{ - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return errors.New("foo") - }, - }, - metadata: map[string]interface{}{"foo": "bar"}, - expectError: true, - }, - { - scenario: "write metadata success", - engine: &gotenberg.PdfEngineMock{ - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - }, - metadata: map[string]interface{}{"foo": "bar"}, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - err := WriteMetadataStub(new(api.Context), tc.engine, tc.metadata, []string{"my.pdf", "my2.pdf"}) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }) - } -} - -func TestMergeHandler(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid metadata form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "foo", - }, - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (merge)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (convert)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (write metadata)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (flatten)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "flatten": { - "true", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetCancelled(true) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - "flatten": { - "true", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { - return nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := mergeRoute(tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - }) - } -} - -func TestSplitHandler(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "no split mode", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (split)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return nil, errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (convert)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - "pdfua": { - "true", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (write metadata)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine (flatten)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - "flatten": { - "true", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - }) - ctx.SetCancelled(true) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{inputPath}, nil - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "splitMode": { - gotenberg.SplitModeIntervals, - }, - "splitSpan": { - "1", - }, - "pdfua": { - "true", - }, - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - "flatten": { - "true", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{"file_split_1.pdf", "file_split_2.pdf"}, nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 2, - expectOutputPaths: []string{"/file/file_0.pdf", "/file/file_1.pdf"}, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - tc.ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { - return nil - }}) - tc.ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := splitRoute(tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - - for _, path := range tc.expectOutputPaths { - if !slices.Contains(tc.ctx.OutputPaths(), path) { - t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths()) - } - } - }) - } -} - -func TestFlattenHandler(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetCancelled(true) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success with single file", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "success (many files)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 2, - expectOutputPaths: []string{"/file.pdf", "/file2.pdf"}, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := flattenRoute(tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - - for _, path := range tc.expectOutputPaths { - if !slices.Contains(tc.ctx.OutputPaths(), path) { - t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths()) - } - } - }) - } -} - -func TestConvertHandler(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "no PDF formats", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - }) - ctx.SetCancelled(true) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success with PDF/A & PDF/UA form fields (single file)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "cannot rename many files", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - }) - ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return errors.New("cannot rename") - }}) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success with PDF/A & PDF/UA form fields (many files)", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - "file2.pdf": "/file2.pdf", - }) - ctx.SetValues(map[string][]string{ - "pdfa": { - gotenberg.PdfA1b, - }, - "pdfua": { - "true", - }, - }) - ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error { - return nil - }}) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 2, - expectOutputPaths: []string{"/file.pdf", "/file2.pdf"}, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := convertRoute(tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - - for _, path := range tc.expectOutputPaths { - if !slices.Contains(tc.ctx.OutputPaths(), path) { - t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths()) - } - } - }) - } -} - -func TestReadMetadataHandler(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - expectError bool - expectedError error - expectHttpError bool - expectHttpStatus int - expectedJson string - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "error from PDF engine", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) { - return nil, errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) { - return map[string]interface{}{ - "foo": "bar", - "bar": "foo", - }, nil - }, - }, - expectError: true, - expectedError: api.ErrNoOutputFile, - expectHttpError: false, - expectedJson: `{"file.pdf":{"bar":"foo","foo":"bar"}}`, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - req := httptest.NewRequest(http.MethodPost, "/forms/pdfengines/metadata/read", nil) - rec := httptest.NewRecorder() - c := echo.New().NewContext(req, rec) - c.Set("context", tc.ctx.Context) - - err := readMetadataRoute(tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectedJson != "" && tc.expectedJson != strings.TrimSpace(rec.Body.String()) { - t.Errorf("expected '%s' as HTTP response but got '%s'", tc.expectedJson, strings.TrimSpace(rec.Body.String())) - } - }) - } -} - -func TestWriteMetadataHandler(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *api.ContextMock - engine gotenberg.PdfEngine - expectError bool - expectHttpError bool - expectHttpStatus int - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "missing at least one mandatory file", - ctx: &api.ContextMock{Context: new(api.Context)}, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "no metadata form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid metadata form field", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "foo", - }, - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "no metadata", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "document.docx": "/document.docx", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "{}", - }, - }) - return ctx - }(), - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - expectOutputPathsCount: 0, - }, - { - scenario: "error from PDF engine", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return errors.New("foo") - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "cannot add output paths", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - ctx.SetCancelled(true) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - }, - expectError: true, - expectHttpError: false, - expectOutputPathsCount: 0, - }, - { - scenario: "success", - ctx: func() *api.ContextMock { - ctx := &api.ContextMock{Context: new(api.Context)} - ctx.SetFiles(map[string]string{ - "file.pdf": "/file.pdf", - }) - ctx.SetValues(map[string][]string{ - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - tc.ctx.SetLogger(zap.NewNop()) - c := echo.New().NewContext(nil, nil) - c.Set("context", tc.ctx.Context) - - err := writeMetadataRoute(tc.engine).Handler(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - - if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths())) - } - - for _, path := range tc.expectOutputPaths { - if !slices.Contains(tc.ctx.OutputPaths(), path) { - t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths()) - } - } - }) - } -} diff --git a/pkg/modules/pdftk/pdftk_test.go b/pkg/modules/pdftk/pdftk_test.go deleted file mode 100644 index 0a49aeda..00000000 --- a/pkg/modules/pdftk/pdftk_test.go +++ /dev/null @@ -1,313 +0,0 @@ -package pdftk - -import ( - "context" - "errors" - "os" - "reflect" - "testing" - - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestPdfTk_Descriptor(t *testing.T) { - descriptor := new(PdfTk).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(PdfTk)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestPdfTk_Provision(t *testing.T) { - engine := new(PdfTk) - ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil) - - err := engine.Provision(ctx) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestPdfTk_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - binPath string - expectError bool - }{ - { - scenario: "empty bin path", - binPath: "", - expectError: true, - }, - { - scenario: "bin path does not exist", - binPath: "/foo", - expectError: true, - }, - { - scenario: "validate success", - binPath: os.Getenv("PDFTK_BIN_PATH"), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(PdfTk) - engine.binPath = tc.binPath - err := engine.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPdfTk_Debug(t *testing.T) { - for _, tc := range []struct { - scenario string - engine *PdfTk - expect map[string]interface{} - doNotExpect map[string]interface{} - }{ - { - scenario: "cannot determine version", - engine: &PdfTk{ - binPath: "foo", - }, - expect: map[string]interface{}{ - "version": `exec: "foo": executable file not found in $PATH`, - }, - }, - { - scenario: "success", - engine: &PdfTk{ - binPath: "echo", - }, - doNotExpect: map[string]interface{}{ - "version": `exec: "echo": executable file not found in $PATH`, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - d := tc.engine.Debug() - - if tc.expect != nil { - if !reflect.DeepEqual(d, tc.expect) { - t.Errorf("expected '%v' but got '%v'", tc.expect, d) - } - } - - if tc.doNotExpect != nil { - if reflect.DeepEqual(d, tc.doNotExpect) { - t.Errorf("did not expect '%v'", d) - } - } - }) - } -} - -func TestPdfTk_Merge(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - inputPaths []string - expectError bool - }{ - { - scenario: "invalid context", - ctx: nil, - expectError: true, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - inputPaths: []string{ - "foo", - }, - expectError: true, - }, - { - scenario: "single file success", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - }, - expectError: false, - }, - { - scenario: "many files success", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - "/tests/test/testdata/pdfengines/sample2.pdf", - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(PdfTk) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf") - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPdfCpu_Split(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - mode gotenberg.SplitMode - inputPath string - expectError bool - expectedError error - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "ErrPdfSplitModeNotSupported", - expectError: true, - expectedError: gotenberg.ErrPdfSplitModeNotSupported, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrPdfSplitModeNotSupported (no unify with pages)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1", Unify: false}, - expectError: true, - expectedError: gotenberg.ErrPdfSplitModeNotSupported, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid context", - ctx: nil, - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - expectError: true, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - inputPath: "", - expectError: true, - expectOutputPathsCount: 0, - }, - { - scenario: "success (pages & unify)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - expectError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(PdfTk) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - if tc.expectOutputPathsCount != len(outputPaths) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths)) - } - }) - } -} - -func TestPdfTk_Flatten(t *testing.T) { - engine := new(PdfTk) - err := engine.Flatten(context.TODO(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestPdfTk_Convert(t *testing.T) { - engine := new(PdfTk) - err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { - engine := new(PdfTk) - _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { - engine := new(PdfTk) - err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} diff --git a/pkg/modules/prometheus/prometheus_test.go b/pkg/modules/prometheus/prometheus_test.go deleted file mode 100644 index 02816b0f..00000000 --- a/pkg/modules/prometheus/prometheus_test.go +++ /dev/null @@ -1,360 +0,0 @@ -package prometheus - -import ( - "errors" - "reflect" - "testing" - "time" - - "github.com/prometheus/client_golang/prometheus" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestPrometheus_Descriptor(t *testing.T) { - descriptor := new(Prometheus).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(Prometheus)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestPrometheus_Provision(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx *gotenberg.Context - expectMetrics []gotenberg.Metric - expectError bool - }{ - { - scenario: "disable collect", - ctx: func() *gotenberg.Context { - fs := new(Prometheus).Descriptor().FlagSet - err := fs.Parse([]string{"--prometheus-disable-collect=true"}) - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: fs, - }, - nil, - ) - }(), - expectError: false, - }, - { - scenario: "invalid metrics provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.MetricsProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ValidateMock = func() error { - return errors.New("foo") - } - mod.MetricsMock = func() ([]gotenberg.Metric, error) { - return nil, nil - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Prometheus).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "invalid metrics from metrics provider", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.MetricsProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ValidateMock = func() error { - return nil - } - mod.MetricsMock = func() ([]gotenberg.Metric, error) { - return nil, errors.New("foo") - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Prometheus).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectError: true, - }, - { - scenario: "provision success", - ctx: func() *gotenberg.Context { - mod := &struct { - gotenberg.ModuleMock - gotenberg.ValidatorMock - gotenberg.MetricsProviderMock - }{} - mod.DescriptorMock = func() gotenberg.ModuleDescriptor { - return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }} - } - mod.ValidateMock = func() error { - return nil - } - mod.MetricsMock = func() ([]gotenberg.Metric, error) { - return []gotenberg.Metric{ - { - Name: "foo", - Description: "Bar.", - }, - }, nil - } - return gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Prometheus).Descriptor().FlagSet, - }, - []gotenberg.ModuleDescriptor{ - mod.Descriptor(), - }, - ) - }(), - expectMetrics: []gotenberg.Metric{ - { - Name: "foo", - Description: "Bar.", - }, - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := new(Prometheus) - err := mod.Provision(tc.ctx) - - if !reflect.DeepEqual(mod.metrics, tc.expectMetrics) { - t.Fatalf("expected metrics %+v, but got: %+v", tc.expectMetrics, mod.metrics) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPrometheus_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - namespace string - metrics []gotenberg.Metric - disableCollect bool - expectError bool - }{ - { - scenario: "collect disabled", - namespace: "foo", - disableCollect: true, - expectError: false, - }, - { - scenario: "empty namespace", - namespace: "", - disableCollect: false, - expectError: true, - }, - { - scenario: "empty metric name", - namespace: "foo", - metrics: []gotenberg.Metric{ - { - Name: "", - }, - }, - disableCollect: false, - expectError: true, - }, - { - scenario: "nil read metric method", - namespace: "foo", - metrics: []gotenberg.Metric{ - { - Name: "foo", - Read: nil, - }, - }, - disableCollect: false, - expectError: true, - }, - { - scenario: "already registered metric", - namespace: "foo", - metrics: []gotenberg.Metric{ - { - Name: "foo", - Read: func() float64 { - return 0 - }, - }, - { - Name: "foo", - Read: func() float64 { - return 0 - }, - }, - }, - disableCollect: false, - expectError: true, - }, - { - scenario: "validate success", - namespace: "foo", - metrics: []gotenberg.Metric{ - { - Name: "foo", - Read: func() float64 { - return 0 - }, - }, - { - Name: "bar", - Read: func() float64 { - return 0 - }, - }, - }, - disableCollect: false, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := &Prometheus{ - namespace: tc.namespace, - metrics: tc.metrics, - disableCollect: tc.disableCollect, - } - err := mod.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestPrometheus_Start(t *testing.T) { - for _, tc := range []struct { - scenario string - metrics []gotenberg.Metric - disableCollect bool - }{ - { - scenario: "collect disabled", - disableCollect: true, - }, - { - scenario: "start success", - metrics: []gotenberg.Metric{ - { - Name: "foo", - Read: func() float64 { - return 0 - }, - }, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := &Prometheus{ - namespace: "foo", - interval: time.Duration(1) * time.Second, - metrics: tc.metrics, - disableCollect: tc.disableCollect, - registry: prometheus.NewRegistry(), - } - - err := mod.Start() - if err != nil { - t.Errorf("expected no error but got: %v", err) - } - }) - } -} - -func TestPrometheus_StartupMessage(t *testing.T) { - mod := new(Prometheus) - - mod.disableCollect = true - disableCollectMsg := mod.StartupMessage() - - mod.disableCollect = false - noDisableCollectMsg := mod.StartupMessage() - - if disableCollectMsg == noDisableCollectMsg { - t.Errorf("expected differrent startup messages if collect is disabled or not, but got '%s'", disableCollectMsg) - } -} - -func TestPrometheus_Stop(t *testing.T) { - err := new(Prometheus).Stop(nil) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestPrometheus_Routes(t *testing.T) { - for _, tc := range []struct { - scenario string - disableCollect bool - expectRoutes int - }{ - { - scenario: "collect disabled", - disableCollect: true, - expectRoutes: 0, - }, - { - scenario: "routes not disabled", - disableCollect: false, - expectRoutes: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - mod := &Prometheus{ - disableCollect: tc.disableCollect, - registry: prometheus.NewRegistry(), - } - - routes, err := mod.Routes() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectRoutes != len(routes) { - t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes)) - } - }) - } -} diff --git a/pkg/modules/qpdf/qpdf_test.go b/pkg/modules/qpdf/qpdf_test.go deleted file mode 100644 index 212a2cc4..00000000 --- a/pkg/modules/qpdf/qpdf_test.go +++ /dev/null @@ -1,425 +0,0 @@ -package qpdf - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "reflect" - "testing" - - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestQPdf_Descriptor(t *testing.T) { - descriptor := new(QPdf).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(QPdf)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestQPdf_Provision(t *testing.T) { - engine := new(QPdf) - ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil) - - err := engine.Provision(ctx) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestQPdf_Validate(t *testing.T) { - for _, tc := range []struct { - scenario string - binPath string - expectError bool - }{ - { - scenario: "empty bin path", - binPath: "", - expectError: true, - }, - { - scenario: "bin path does not exist", - binPath: "/foo", - expectError: true, - }, - { - scenario: "validate success", - binPath: os.Getenv("QPDF_BIN_PATH"), - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(QPdf) - engine.binPath = tc.binPath - err := engine.Validate() - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestQPdf_Debug(t *testing.T) { - for _, tc := range []struct { - scenario string - engine *QPdf - expect map[string]interface{} - doNotExpect map[string]interface{} - }{ - { - scenario: "cannot determine version", - engine: &QPdf{ - binPath: "foo", - }, - expect: map[string]interface{}{ - "version": `exec: "foo": executable file not found in $PATH`, - }, - }, - { - scenario: "success", - engine: &QPdf{ - binPath: "echo", - }, - doNotExpect: map[string]interface{}{ - "version": `exec: "echo": executable file not found in $PATH`, - }, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - d := tc.engine.Debug() - - if tc.expect != nil { - if !reflect.DeepEqual(d, tc.expect) { - t.Errorf("expected '%v' but got '%v'", tc.expect, d) - } - } - - if tc.doNotExpect != nil { - if reflect.DeepEqual(d, tc.doNotExpect) { - t.Errorf("did not expect '%v'", d) - } - } - }) - } -} - -func TestQPdf_Merge(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - inputPaths []string - expectError bool - }{ - { - scenario: "invalid context", - ctx: nil, - expectError: true, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - inputPaths: []string{ - "foo", - }, - expectError: true, - }, - { - scenario: "single file success", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - }, - expectError: false, - }, - { - scenario: "many files success", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - "/tests/test/testdata/pdfengines/sample2.pdf", - }, - expectError: false, - }, - { - scenario: "success even with warnings", - ctx: context.TODO(), - inputPaths: []string{ - "/tests/test/testdata/pdfengines/sample1.pdf", - "/tests/test/testdata/pdfengines/sample5.pdf", - }, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(QPdf) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf") - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestQPdf_Split(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - mode gotenberg.SplitMode - inputPath string - expectError bool - expectedError error - expectOutputPathsCount int - expectOutputPaths []string - }{ - { - scenario: "ErrPdfSplitModeNotSupported", - expectError: true, - expectedError: gotenberg.ErrPdfSplitModeNotSupported, - expectOutputPathsCount: 0, - }, - { - scenario: "ErrPdfSplitModeNotSupported (no unify with pages)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1", Unify: false}, - expectError: true, - expectedError: gotenberg.ErrPdfSplitModeNotSupported, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid context", - ctx: nil, - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - expectError: true, - expectOutputPathsCount: 0, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - inputPath: "", - expectError: true, - expectOutputPathsCount: 0, - }, - { - scenario: "success (pages & unify)", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", - expectError: false, - expectOutputPathsCount: 1, - }, - { - scenario: "success even with warnings", - ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, - inputPath: "/tests/test/testdata/pdfengines/sample5.pdf", - expectError: false, - expectOutputPathsCount: 1, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(QPdf) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - outputPaths, err := engine.Split(tc.ctx, zap.NewNop(), tc.mode, tc.inputPath, outputDir) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - - if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { - t.Fatalf("expected error %v but got: %v", tc.expectedError, err) - } - - if tc.expectOutputPathsCount != len(outputPaths) { - t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(outputPaths)) - } - }) - } -} - -func TestQPdf_Flatten(t *testing.T) { - for _, tc := range []struct { - scenario string - ctx context.Context - inputPath string - createCopy bool - expectError bool - }{ - { - scenario: "invalid context", - ctx: nil, - expectError: true, - }, - { - scenario: "invalid input path", - ctx: context.TODO(), - inputPath: "foo.pdf", - expectError: true, - }, - { - scenario: "success", - ctx: context.TODO(), - inputPath: "/tests/test/testdata/pdfengines/sample3.pdf", - createCopy: true, - expectError: false, - }, - { - scenario: "success even with warnings", - ctx: context.TODO(), - inputPath: "/tests/test/testdata/pdfengines/sample5.pdf", - createCopy: true, - expectError: false, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - engine := new(QPdf) - err := engine.Provision(nil) - if err != nil { - t.Fatalf("expected error but got: %v", err) - } - - var destinationPath string - if tc.createCopy { - fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll)) - outputDir, err := fs.MkdirAll() - if err != nil { - t.Fatalf("expected error no but got: %v", err) - } - - defer func() { - err = os.RemoveAll(fs.WorkingDirPath()) - if err != nil { - t.Fatalf("expected no error while cleaning up but got: %v", err) - } - }() - - destinationPath = fmt.Sprintf("%s/copy_temp.pdf", outputDir) - source, err := os.Open(tc.inputPath) - if err != nil { - t.Fatalf("open source file: %v", err) - } - - defer func(source *os.File) { - err := source.Close() - if err != nil { - t.Fatalf("close file: %v", err) - } - }(source) - - destination, err := os.Create(destinationPath) - if err != nil { - t.Fatalf("create destination file: %v", err) - } - - defer func(destination *os.File) { - err := destination.Close() - if err != nil { - t.Fatalf("close file: %v", err) - } - }(destination) - - _, err = io.Copy(destination, source) - if err != nil { - t.Fatalf("copy source into destination: %v", err) - } - } else { - destinationPath = tc.inputPath - } - - err = engine.Flatten(tc.ctx, zap.NewNop(), destinationPath) - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectError && err == nil { - t.Fatal("expected error but got none") - } - }) - } -} - -func TestQPdf_Convert(t *testing.T) { - engine := new(QPdf) - err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_ReadMetadata(t *testing.T) { - engine := new(QPdf) - _, err := engine.ReadMetadata(context.Background(), zap.NewNop(), "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} - -func TestLibreOfficePdfEngine_WriteMetadata(t *testing.T) { - engine := new(QPdf) - err := engine.WriteMetadata(context.Background(), zap.NewNop(), nil, "") - - if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) { - t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err) - } -} diff --git a/pkg/modules/webhook/middleware_test.go b/pkg/modules/webhook/middleware_test.go deleted file mode 100644 index 8940f406..00000000 --- a/pkg/modules/webhook/middleware_test.go +++ /dev/null @@ -1,582 +0,0 @@ -package webhook - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "math/rand" - "mime/multipart" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/dlclark/regexp2" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - - "github.com/gotenberg/gotenberg/v8/pkg/modules/api" -) - -func TestWebhookMiddlewareGuards(t *testing.T) { - buildMultipartFormDataRequest := func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - - err := writer.WriteField("foo", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - - return req - } - - buildWebhookModule := func() *Webhook { - return &Webhook{ - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - errorAllowList: regexp2.MustCompile("", 0), - errorDenyList: regexp2.MustCompile("", 0), - maxRetry: 0, - retryMinWait: 0, - retryMaxWait: 0, - disable: false, - } - } - - for _, tc := range []struct { - scenario string - request *http.Request - mod *Webhook - next echo.HandlerFunc - noDeadline bool - expectError bool - expectHttpError bool - expectHttpStatus int - }{ - { - scenario: "no webhook URL, skip middleware", - request: buildMultipartFormDataRequest(), - mod: buildWebhookModule(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return nil - } - }(), - noDeadline: false, - expectError: false, - expectHttpError: false, - }, - { - scenario: "no webhook error URL", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "context has no deadline", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - return req - }(), - mod: buildWebhookModule(), - noDeadline: true, - expectError: true, - }, - { - scenario: "webhook URL is not allowed", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - return req - }(), - mod: func() *Webhook { - mod := buildWebhookModule() - mod.allowList = regexp2.MustCompile("bar", 0) - return mod - }(), - noDeadline: false, - expectError: true, - }, - { - scenario: "webhook URL is denied", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - return req - }(), - mod: func() *Webhook { - mod := buildWebhookModule() - mod.denyList = regexp2.MustCompile("foo", 0) - return mod - }(), - noDeadline: false, - expectError: true, - }, - { - scenario: "webhook error URL is not allowed", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - return req - }(), - mod: func() *Webhook { - mod := buildWebhookModule() - mod.errorAllowList = regexp2.MustCompile("foo", 0) - return mod - }(), - noDeadline: false, - expectError: true, - }, - { - scenario: "webhook error URL is denied", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - return req - }(), - mod: func() *Webhook { - mod := buildWebhookModule() - mod.errorDenyList = regexp2.MustCompile("bar", 0) - return mod - }(), - noDeadline: false, - expectError: true, - }, - { - scenario: "invalid webhook method (GET)", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Method", http.MethodGet) - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid webhook error method (GET)", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet) - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "valid webhook method (POST) but invalid webhook error method (GET)", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Method", http.MethodPost) - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet) - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "valid webhook method (PATH) but invalid webhook error method (GET)", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Method", http.MethodPatch) - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet) - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "valid webhook method (PUT) but invalid webhook error method (GET)", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Method", http.MethodPut) - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - req.Header.Set("Gotenberg-Webhook-Error-Method", http.MethodGet) - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - { - scenario: "invalid webhook extra HTTP headers", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Webhook-Url", "foo") - req.Header.Set("Gotenberg-Webhook-Error-Url", "bar") - req.Header.Set("Gotenberg-Webhook-Extra-Http-Headers", "foo") - return req - }(), - mod: buildWebhookModule(), - noDeadline: false, - expectError: true, - expectHttpError: true, - expectHttpStatus: http.StatusBadRequest, - }, - } { - t.Run(tc.scenario, func(t *testing.T) { - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(tc.request, httptest.NewRecorder()) - - if tc.noDeadline { - ctx := &api.ContextMock{Context: &api.Context{Context: context.Background()}} - ctx.SetEchoContext(c) - c.Set("context", ctx.Context) - c.Set("cancel", func() context.CancelFunc { - return nil - }()) - } else { - timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second) - ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}} - ctx.SetEchoContext(c) - c.Set("context", ctx.Context) - c.Set("cancel", cancel) - } - - err := webhookMiddleware(tc.mod).Handler(tc.next)(c) - - if tc.expectError && err == nil { - t.Fatal("expected error but got none", err) - } - - if !tc.expectError && err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - var httpErr api.HttpError - isHttpError := errors.As(err, &httpErr) - - if tc.expectHttpError && !isHttpError { - t.Errorf("expected an HTTP error but got: %v", err) - } - - if !tc.expectHttpError && isHttpError { - t.Errorf("expected no HTTP error but got one: %v", httpErr) - } - - if err != nil && tc.expectHttpError && isHttpError { - status, _ := httpErr.HttpError() - if status != tc.expectHttpStatus { - t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status) - } - } - }) - } -} - -func TestWebhookMiddlewareAsynchronousProcess(t *testing.T) { - buildMultipartFormDataRequest := func() *http.Request { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - defer func() { - err := writer.Close() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - }() - - err := writer.WriteField("foo", "foo") - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, writer.FormDataContentType()) - - return req - } - - buildWebhookModule := func() *Webhook { - return &Webhook{ - allowList: regexp2.MustCompile("", 0), - denyList: regexp2.MustCompile("", 0), - errorAllowList: regexp2.MustCompile("", 0), - errorDenyList: regexp2.MustCompile("", 0), - maxRetry: 0, - retryMinWait: 0, - retryMaxWait: 0, - clientTimeout: time.Duration(30) * time.Second, - disable: false, - } - } - - for _, tc := range []struct { - scenario string - request *http.Request - mod *Webhook - next echo.HandlerFunc - expectWebhookContentType string - expectWebhookMethod string - expectWebhookExtraHttpHeaders map[string]string - expectWebhookFilename string - expectWebhookErrorStatus int - expectWebhookErrorMessage string - returnedError *echo.HTTPError - }{ - { - scenario: "next handler return an error", - request: buildMultipartFormDataRequest(), - mod: buildWebhookModule(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return errors.New("foo") - } - }(), - expectWebhookContentType: echo.MIMEApplicationJSON, - expectWebhookMethod: http.MethodPost, - expectWebhookErrorStatus: http.StatusInternalServerError, - expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError), - }, - { - scenario: "next handler return an HTTP error", - request: buildMultipartFormDataRequest(), - mod: buildWebhookModule(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - return api.NewSentinelHttpError(http.StatusBadRequest, http.StatusText(http.StatusBadRequest)) - } - }(), - expectWebhookContentType: echo.MIMEApplicationJSON, - expectWebhookMethod: http.MethodPost, - expectWebhookErrorStatus: http.StatusBadRequest, - expectWebhookErrorMessage: http.StatusText(http.StatusBadRequest), - }, - { - scenario: "success", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Output-Filename", "foo") - req.Header.Set("Gotenberg-Webhook-Extra-Http-Headers", `{ "foo": "bar" }`) - return req - }(), - mod: buildWebhookModule(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - ctx := c.Get("context").(*api.Context) - return ctx.AddOutputPaths("/tests/test/testdata/api/sample2.pdf") - } - }(), - expectWebhookContentType: "application/pdf", - expectWebhookMethod: http.MethodPost, - expectWebhookFilename: "foo", - expectWebhookExtraHttpHeaders: map[string]string{"foo": "bar"}, - }, - { - scenario: "success (return an error)", - request: func() *http.Request { - req := buildMultipartFormDataRequest() - req.Header.Set("Gotenberg-Output-Filename", "foo") - return req - }(), - mod: buildWebhookModule(), - next: func() echo.HandlerFunc { - return func(c echo.Context) error { - ctx := c.Get("context").(*api.Context) - return ctx.AddOutputPaths("/tests/test/testdata/api/sample1.pdf") - } - }(), - returnedError: echo.ErrInternalServerError, - expectWebhookContentType: echo.MIMEApplicationJSON, - expectWebhookMethod: http.MethodPost, - expectWebhookErrorStatus: http.StatusInternalServerError, - expectWebhookErrorMessage: http.StatusText(http.StatusInternalServerError), - expectWebhookFilename: "foo", - }, - } { - func() { - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - - c := srv.NewContext(tc.request, httptest.NewRecorder()) - c.Set("logger", zap.NewNop()) - c.Set("traceHeader", "Gotenberg-Trace") - c.Set("trace", "foo") - c.Set("startTime", time.Now()) - - timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second) - ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}} - ctx.SetLogger(zap.NewNop()) - ctx.SetEchoContext(c) - - c.Set("context", ctx.Context) - c.Set("cancel", cancel) - - webhook := echo.New() - webhook.HideBanner = true - webhook.HidePort = true - webhookPort := rand.Intn(65535-1025+1) + 1025 - - c.Request().Header.Set("Gotenberg-Webhook-Url", fmt.Sprintf("http://localhost:%d/", webhookPort)) - c.Request().Header.Set("Gotenberg-Webhook-Error-Url", fmt.Sprintf("http://localhost:%d/", webhookPort)) - - errChan := make(chan error, 1) - - webhook.POST( - "/", - func() echo.HandlerFunc { - return func(c echo.Context) error { - contentType := c.Request().Header.Get(echo.HeaderContentType) - if contentType != tc.expectWebhookContentType { - t.Errorf("expected '%s' '%s' but got '%s'", echo.HeaderContentType, tc.expectWebhookContentType, contentType) - } - - trace := c.Request().Header.Get("Gotenberg-Trace") - if trace != "foo" { - t.Errorf("expected '%s' '%s' but got '%s'", "Gotenberg-Trace", "foo", trace) - } - - method := c.Request().Method - if method != tc.expectWebhookMethod { - t.Errorf("expected HTTP method '%s' but got '%s'", tc.expectWebhookMethod, method) - } - - for key, expect := range tc.expectWebhookExtraHttpHeaders { - actual := c.Request().Header.Get(key) - - if actual != expect { - t.Errorf("expected '%s' '%s' but got '%s'", key, expect, actual) - } - } - - if contentType == echo.MIMEApplicationJSON { - body, err := io.ReadAll(c.Request().Body) - if err != nil { - errChan <- err - return nil - } - - result := struct { - Status int `json:"status"` - Message string `json:"message"` - }{} - - err = json.Unmarshal(body, &result) - if err != nil { - errChan <- err - return nil - } - - if result.Status != tc.expectWebhookErrorStatus { - t.Errorf("expected status %d from JSON but got %d", tc.expectWebhookErrorStatus, result.Status) - } - - if result.Message != tc.expectWebhookErrorMessage { - t.Errorf("expected message '%s' from JSON but got '%s'", tc.expectWebhookErrorMessage, result.Message) - } - - errChan <- nil - return nil - } - - contentLength := c.Request().Header.Get(echo.HeaderContentLength) - if contentLength == "" { - t.Errorf("expected non empty '%s'", echo.HeaderContentLength) - } - - contentDisposition := c.Request().Header.Get(echo.HeaderContentDisposition) - if !strings.Contains(contentDisposition, tc.expectWebhookFilename) { - t.Errorf("expected '%s' '%s' to contain '%s'", echo.HeaderContentDisposition, contentDisposition, tc.expectWebhookFilename) - } - - body, err := io.ReadAll(c.Request().Body) - if err != nil { - errChan <- err - return nil - } - - if body == nil || len(body) == 0 { - t.Error("expected non nil body") - } - - errChan <- nil - - if tc.returnedError != nil { - return tc.returnedError - } - - return nil - } - }(), - ) - - go func() { - err := webhook.Start(fmt.Sprintf(":%d", webhookPort)) - if !errors.Is(err, http.ErrServerClosed) { - t.Errorf("expected no error but got: %v", err) - } - }() - - defer func() { - err := webhook.Shutdown(context.TODO()) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } - }() - - err := webhookMiddleware(tc.mod).Handler(tc.next)(c) - if err != nil && !errors.Is(err, api.ErrAsyncProcess) { - t.Errorf("expected no error but got: %v", err) - } - - err = <-errChan - if err != nil { - t.Errorf("expected no error but got: %v", err) - } - }() - } -} diff --git a/pkg/modules/webhook/webhook_test.go b/pkg/modules/webhook/webhook_test.go deleted file mode 100644 index 03928046..00000000 --- a/pkg/modules/webhook/webhook_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package webhook - -import ( - "reflect" - "testing" - - "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" -) - -func TestWebhook_Descriptor(t *testing.T) { - descriptor := new(Webhook).Descriptor() - - actual := reflect.TypeOf(descriptor.New()) - expect := reflect.TypeOf(new(Webhook)) - - if actual != expect { - t.Errorf("expected '%s' but got '%s'", expect, actual) - } -} - -func TestWebhook_Provision(t *testing.T) { - mod := new(Webhook) - ctx := gotenberg.NewContext( - gotenberg.ParsedFlags{ - FlagSet: new(Webhook).Descriptor().FlagSet, - }, - nil, - ) - - err := mod.Provision(ctx) - if err != nil { - t.Errorf("expected no error but got: %v", err) - } -} - -func TestWebhook_Middlewares(t *testing.T) { - for _, tc := range []struct { - scenario string - disable bool - expectMiddlewares int - }{ - { - scenario: "webhook disabled", - disable: true, - expectMiddlewares: 0, - }, - { - scenario: "webhook enabled", - disable: false, - expectMiddlewares: 1, - }, - } { - mod := new(Webhook) - mod.disable = tc.disable - - middlewares, err := mod.Middlewares() - if err != nil { - t.Fatalf("expected no error but got: %v", err) - } - - if tc.expectMiddlewares != len(middlewares) { - t.Errorf("expected %d middlewares but got %d", tc.expectMiddlewares, len(middlewares)) - } - } -} diff --git a/test/Dockerfile b/test/Dockerfile deleted file mode 100644 index a0dad3ff..00000000 --- a/test/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -ARG GOLANG_VERSION -ARG DOCKER_REGISTRY -ARG DOCKER_REPOSITORY -ARG GOTENBERG_VERSION -ARG GOLANGCI_LINT_VERSION - -FROM golang:$GOLANG_VERSION-bookworm AS golang - -# We're extending the Gotenberg's Docker image because our code relies on external -# dependencies like Google Chrome, LibreOffice, etc. -FROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION - -USER root - -ENV GOPATH=/go -ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH -ENV CGO_ENABLED=1 - -COPY --from=golang /usr/local/go /usr/local/go - -RUN apt-get update -qq &&\ - apt-get upgrade -yqq &&\ - apt-get install -y -qq --no-install-recommends \ - sudo \ - # gcc for cgo. - g++ \ - gcc \ - libc6-dev \ - make \ - pkg-config &&\ - rm -rf /var/lib/apt/lists/* &&\ - mkdir -p "$GOPATH/src" "$GOPATH/bin" &&\ - chmod -R 777 "$GOPATH" &&\ - adduser gotenberg sudo &&\ - echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers &&\ - # We cannot use $PATH in the next command (print $PATH instead of the environment variable value). - sed -i 's#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin#/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/go/bin:/usr/local/go/bin#g' /etc/sudoers &&\ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin $GOLANGCI_LINT_VERSION &&\ - go install mvdan.cc/gofumpt@latest &&\ - go install github.com/daixiang0/gci@latest - -COPY ./test/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh -COPY ./test/golint.sh /usr/bin/golint -COPY ./test/gotest.sh /usr/bin/gotest -COPY ./test/gotodos.sh /usr/bin/gotodos - -# Pristine working directory. -WORKDIR /tests - -ENTRYPOINT [ "/usr/bin/tini", "--", "docker-entrypoint.sh" ] \ No newline at end of file diff --git a/test/docker-entrypoint.sh b/test/docker-entrypoint.sh deleted file mode 100755 index 4862f516..00000000 --- a/test/docker-entrypoint.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -# This entrypoint allows us to set the UID and GID of the host user so that -# our testing environment does not override files permissions from the host. -# Credits: https://github.com/thecodingmachine/docker-images-php. - -set +e -mkdir -p testing_file_system_rights.foo -chmod 700 testing_file_system_rights.foo -su gotenberg -c "touch testing_file_system_rights.foo/foo > /dev/null 2>&1" -HAS_CONSISTENT_RIGHTS=$? - -if [[ "$HAS_CONSISTENT_RIGHTS" != "0" ]]; then - # If not specified, the DOCKER_USER is the owner of the current working directory (heuristic!). - DOCKER_USER=$(stat -c '%u' "$(pwd)") -else - # macOs or Windows. - # Note: in most cases, we don't care about the rights (they are not respected). - FILE_OWNER=$(stat -c '%u' "testing_file_system_rights.foo/foo") - if [[ "$FILE_OWNER" == "0" ]]; then - # If root, we are likely on a Windows host. - # All files will belong to root, but it does not matter as everybody can write/delete - # those (0777 access rights). - DOCKER_USER=gotenberg - else - # In case of a NFS mount (common on macOS), the created files will belong to the NFS user. - DOCKER_USER=$FILE_OWNER - fi -fi - -rm -rf testing_file_system_rights.foo -set -e -unset HAS_CONSISTENT_RIGHTS - -# Note: DOCKER_USER is either a username (if the user exists in the container), -# otherwise a user ID (a user from the host). - -# DOCKER_USER is an ID. -if [[ "$DOCKER_USER" =~ ^[0-9]+$ ]] ; then - # Let's change the gotenberg user's ID in order to match this free ID. - usermod -u "$DOCKER_USER" -G sudo gotenberg - DOCKER_USER=gotenberg -fi - -DOCKER_USER_ID=$(id -ur $DOCKER_USER) - -# Fix access rights to stdout and stderr. -set +e -chown $DOCKER_USER /proc/self/fd/{1,2} -set -e - -# Install modules. -set -x -go mod download -go mod tidy -set +x - -# Run the command with the correct user. -exec "sudo" "-E" "-H" "-u" "#$DOCKER_USER_ID" "$@" diff --git a/test/golint.sh b/test/golint.sh deleted file mode 100755 index 107c7018..00000000 --- a/test/golint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -set -x - -golangci-lint run \ No newline at end of file diff --git a/test/gotest.sh b/test/gotest.sh deleted file mode 100755 index 99bb6433..00000000 --- a/test/gotest.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -x - -go test -race -covermode=atomic -coverprofile=/tests/coverage.txt ./... -RESULT=$? - -go tool cover -html=coverage.txt -o /tests/coverage.html - -exit $RESULT \ No newline at end of file diff --git a/test/gotodos.sh b/test/gotodos.sh deleted file mode 100755 index c6201369..00000000 --- a/test/gotodos.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -x - -golangci-lint run \ - --no-config \ - --disable-all \ - --enable godox \ No newline at end of file diff --git a/test/integration/doc.go b/test/integration/doc.go new file mode 100644 index 00000000..d52de84a --- /dev/null +++ b/test/integration/doc.go @@ -0,0 +1,2 @@ +// Package integration contains everything related to integration testing. +package integration diff --git a/test/integration/features/chromium_convert_html.feature b/test/integration/features/chromium_convert_html.feature new file mode 100644 index 00000000..4f615c74 --- /dev/null +++ b/test/integration/features/chromium_convert_html.feature @@ -0,0 +1,852 @@ +Feature: /forms/chromium/convert/html + + Scenario: POST /forms/chromium/convert/html (Default) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/chromium/convert/html (Single Page) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-12-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 12 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + Page 12 + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-12-html/index.html | file | + | singlePage | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + # page-break-after: always; tells the browser's print engine to force a page break after each element, + # even when calculating a large enough paper height, Chromium's PDF rendering will still honor those page break + # directives. + """ + Page 12 + """ + + Scenario: POST /forms/chromium/convert/html (Landscape) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should NOT be set to landscape orientation + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | landscape | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should be set to landscape orientation + + Scenario: POST /forms/chromium/convert/html (Native Page Ranges) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-12-html/index.html | file | + | nativePageRanges | 2-3 | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/html (Header & Footer) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-12-html/index.html | file | + | files | testdata/header-footer-html/header.html | file | + | files | testdata/header-footer-html/footer.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 12 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Pages 12 + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + 1 of 12 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + Pages 12 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + 12 of 12 + """ + + Scenario: POST /forms/chromium/convert/html (Wait Delay) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | waitDelay | 2.5s | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + + Scenario: POST /forms/chromium/convert/html (Wait For Expression) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | waitForExpression | window.globalVar === 'ready' | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + + Scenario: POST /forms/chromium/convert/html (Emulated Media Type) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | emulatedMediaType | print | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | emulatedMediaType | screen | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'print'. + """ + + Scenario: POST /forms/chromium/convert/html (Default Allow / Deny Lists) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the Gotenberg container should log the following entries: + | 'file:///etc/passwd' matches the expression from the denied list | + + Scenario: POST /forms/chromium/convert/html (Main URL does NOT match allowed list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | ^file:(?!//\\/tmp/).* | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + Then the response status code should be 403 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Forbidden + """ + + Scenario: POST /forms/chromium/convert/html (Main URL does match denied list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | | + | CHROMIUM_DENY_LIST | ^file:///tmp.* | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + Then the response status code should be 403 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Forbidden + """ + + Scenario: POST /forms/chromium/convert/html (Request does not match the allowed list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | ^file:///tmp.* | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the Gotenberg container should log the following entries: + | 'file:///etc/passwd' does not match the expression from the allowed list | + + Scenario: POST /forms/chromium/convert/html (JavaScript Enabled) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + JavaScript is enabled. + """ + + Scenario: POST /forms/chromium/convert/html (JavaScript Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_DISABLE_JAVASCRIPT | true | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + JavaScript is enabled. + """ + + Scenario: POST /forms/chromium/convert/html (Fail On Resource HTTP Status Codes) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | failOnResourceHttpStatusCodes | [499,599] | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid HTTP status code from resources: + https://httpstat.us/400 - 400: Bad Request + """ + + Scenario: POST /forms/chromium/convert/html (Fail On Resource Loading Failed) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | failOnResourceLoadingFailed | true | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Chromium failed to load resources + """ + Then the response body should contain string: + """ + resource Stylesheet: net::ERR_CONNECTION_REFUSED + """ + Then the response body should contain string: + """ + resource Stylesheet: net::ERR_FILE_NOT_FOUND + """ + + Scenario: POST /forms/chromium/convert/html (Fail On Console Exceptions) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | failOnConsoleExceptions | true | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Chromium console exceptions + """ + Then the response body should contain string: + """ + exception "Uncaught" (61:12): Error: Exception 1 + """ + Then the response body should contain string: + """ + exception "Uncaught" (65:12): Error: Exception 2 + """ + + Scenario: POST /forms/chromium/convert/html (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | singlePage | foo | field | + | paperWidth | foo | field | + | paperHeight | foo | field | + | marginTop | foo | field | + | marginBottom | foo | field | + | marginLeft | foo | field | + | marginRight | foo | field | + | preferCssPageSize | foo | field | + | generateDocumentOutline | foo | field | + | printBackground | foo | field | + | omitBackground | foo | field | + | landscape | foo | field | + | scale | foo | field | + | waitDelay | foo | field | + | emulatedMediaType | foo | field | + | failOnHttpStatusCodes | foo | field | + | failOnResourceHttpStatusCodes | foo | field | + | failOnResourceLoadingFailed | foo | field | + | failOnConsoleExceptions | foo | field | + | skipNetworkIdleEvent | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form file 'index.html' is required + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | omitBackground | true | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + omitBackground requires printBackground set to true + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | paperWidth | 0 | field | + | paperHeight | 0 | field | + | marginTop | 1000000 | field | + | marginBottom | 1000000 | field | + | marginLeft | 1000000 | field | + | marginRight | 1000000 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Chromium does not handle the provided settings; please check for aberrant form values + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | nativePageRanges | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Chromium does not handle the page ranges 'foo' (nativePageRanges) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | waitForExpression | undefined | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + The expression 'undefined' (waitForExpression) returned an exception or undefined + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | cookies | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'cookies' is invalid (got 'foo', resulting to unmarshal cookies: invalid character 'o' in literal false (expecting 'a')) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | cookies | [{"name":"yummy_cookie","value":"choco"}] | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'cookies' is invalid (got '[{"name":"yummy_cookie","value":"choco"}]', resulting to cookie 0 must have its name, value and domain set) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | extraHttpHeaders | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got 'foo', resulting to unmarshal extraHttpHeaders: invalid character 'o' in literal false (expecting 'a')) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | extraHttpHeaders | {"foo":"bar;scope;;"} | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope;;"}', resulting to invalid scope '' for header 'foo') + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | extraHttpHeaders | {"foo":"bar;scope=*."} | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | splitMode | foo | field | + | splitSpan | 2 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitMode' is invalid (got 'foo', resulting to wrong value, expected either 'intervals' or 'pages') + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | splitMode | intervals | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitSpan' is invalid (got 'foo', resulting to strconv.Atoi: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | splitMode | pages | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/chromium/convert/html (Split Intervals) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-3-html/index.html | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 2 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/html (Split Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-3-html/index.html | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/html (Split Pages & Unify) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-3-html/index.html | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/html (Split Many PDFs - Lot of Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-12-html/index.html | file | + | splitMode | intervals | field | + | splitSpan | 1 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 12 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + | *_2.pdf | + | *_3.pdf | + | *_4.pdf | + | *_5.pdf | + | *_6.pdf | + | *_7.pdf | + | *_8.pdf | + | *_9.pdf | + | *_10.pdf | + | *_11.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_11.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_11.pdf" PDF should have the following content at page 1: + """ + Page 12 + """ + + Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/chromium/convert/html (Split & PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/pages-3-html/index.html | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 2 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/chromium/convert/html (Metadata) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/chromium/convert/html (Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | flatten | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be flatten + + Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | flatten | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + Then the response PDF(s) should be flatten + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/chromium/convert/html (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + Then the response status code should be 404 + + Scenario: POST /forms/chromium/convert/html (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | Gotenberg-Trace | forms_chromium_convert_html | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_chromium_convert_html" + Then the Gotenberg container should log the following entries: + | "trace":"forms_chromium_convert_html" | + + Scenario: POST /forms/chromium/convert/html (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page-1-html/index.html","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + Then the response status code should be 200 + Then the file request header "X-Foo" should be "bar" + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/chromium/convert/html (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then there should be the following file(s) in the webhook request: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/chromium/convert/html (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + Then the response status code should be 401 + + Scenario: POST /foo/forms/chromium/convert/html (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/chromium_convert_markdown.feature b/test/integration/features/chromium_convert_markdown.feature new file mode 100644 index 00000000..286ea933 --- /dev/null +++ b/test/integration/features/chromium_convert_markdown.feature @@ -0,0 +1,977 @@ +Feature: /forms/chromium/convert/markdown + + Scenario: POST /forms/chromium/convert/markdown (Default) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/chromium/convert/markdown (Single Page) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-12-markdown/index.html | file | + | files | testdata/pages-12-markdown/page_1.md | file | + | files | testdata/pages-12-markdown/page_2.md | file | + | files | testdata/pages-12-markdown/page_3.md | file | + | files | testdata/pages-12-markdown/page_4.md | file | + | files | testdata/pages-12-markdown/page_5.md | file | + | files | testdata/pages-12-markdown/page_6.md | file | + | files | testdata/pages-12-markdown/page_7.md | file | + | files | testdata/pages-12-markdown/page_8.md | file | + | files | testdata/pages-12-markdown/page_9.md | file | + | files | testdata/pages-12-markdown/page_10.md | file | + | files | testdata/pages-12-markdown/page_11.md | file | + | files | testdata/pages-12-markdown/page_12.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 12 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + Page 12 + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-12-markdown/index.html | file | + | files | testdata/pages-12-markdown/page_1.md | file | + | files | testdata/pages-12-markdown/page_2.md | file | + | files | testdata/pages-12-markdown/page_3.md | file | + | files | testdata/pages-12-markdown/page_4.md | file | + | files | testdata/pages-12-markdown/page_5.md | file | + | files | testdata/pages-12-markdown/page_6.md | file | + | files | testdata/pages-12-markdown/page_7.md | file | + | files | testdata/pages-12-markdown/page_8.md | file | + | files | testdata/pages-12-markdown/page_9.md | file | + | files | testdata/pages-12-markdown/page_10.md | file | + | files | testdata/pages-12-markdown/page_11.md | file | + | files | testdata/pages-12-markdown/page_12.md | file | + | singlePage | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + # page-break-after: always; tells the browser's print engine to force a page break after each element, + # even when calculating a large enough paper height, Chromium's PDF rendering will still honor those page break + # directives. + """ + Page 12 + """ + + Scenario: POST /forms/chromium/convert/markdown (Landscape) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should NOT be set to landscape orientation + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | landscape | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should be set to landscape orientation + + Scenario: POST /forms/chromium/convert/markdown (Native Page Ranges) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-12-markdown/index.html | file | + | files | testdata/pages-12-markdown/page_1.md | file | + | files | testdata/pages-12-markdown/page_2.md | file | + | files | testdata/pages-12-markdown/page_3.md | file | + | files | testdata/pages-12-markdown/page_4.md | file | + | files | testdata/pages-12-markdown/page_5.md | file | + | files | testdata/pages-12-markdown/page_6.md | file | + | files | testdata/pages-12-markdown/page_7.md | file | + | files | testdata/pages-12-markdown/page_8.md | file | + | files | testdata/pages-12-markdown/page_9.md | file | + | files | testdata/pages-12-markdown/page_10.md | file | + | files | testdata/pages-12-markdown/page_11.md | file | + | files | testdata/pages-12-markdown/page_12.md | file | + | nativePageRanges | 2-3 | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/markdown (Header & Footer) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-12-markdown/index.html | file | + | files | testdata/pages-12-markdown/page_1.md | file | + | files | testdata/pages-12-markdown/page_2.md | file | + | files | testdata/pages-12-markdown/page_3.md | file | + | files | testdata/pages-12-markdown/page_4.md | file | + | files | testdata/pages-12-markdown/page_5.md | file | + | files | testdata/pages-12-markdown/page_6.md | file | + | files | testdata/pages-12-markdown/page_7.md | file | + | files | testdata/pages-12-markdown/page_8.md | file | + | files | testdata/pages-12-markdown/page_9.md | file | + | files | testdata/pages-12-markdown/page_10.md | file | + | files | testdata/pages-12-markdown/page_11.md | file | + | files | testdata/pages-12-markdown/page_12.md | file | + | files | testdata/header-footer-html/header.html | file | + | files | testdata/header-footer-html/footer.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 12 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Pages 12 + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + 1 of 12 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + Pages 12 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + 12 of 12 + """ + + Scenario: POST /forms/chromium/convert/markdown (Wait Delay) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | waitDelay | 2.5s | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + + Scenario: POST /forms/chromium/convert/markdown (Wait For Expression) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | waitForExpression | window.globalVar === 'ready' | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + + Scenario: POST /forms/chromium/convert/markdown (Emulated Media Type) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | emulatedMediaType | print | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | emulatedMediaType | screen | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'print'. + """ + + Scenario: POST /forms/chromium/convert/markdown (Default Allow / Deny Lists) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the Gotenberg container should log the following entries: + | 'file:///etc/passwd' matches the expression from the denied list | + + Scenario: POST /forms/chromium/convert/markdown (Main URL does NOT match allowed list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | ^file:(?!//\\/tmp/).* | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + Then the response status code should be 403 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Forbidden + """ + + Scenario: POST /forms/chromium/convert/markdown (Main URL does match denied list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | | + | CHROMIUM_DENY_LIST | ^file:///tmp.* | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + Then the response status code should be 403 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Forbidden + """ + + Scenario: POST /forms/chromium/convert/markdown (Request does not match the allowed list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | ^file:///tmp.* | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the Gotenberg container should log the following entries: + | 'file:///etc/passwd' does not match the expression from the allowed list | + + Scenario: POST /forms/chromium/convert/markdown (JavaScript Enabled) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + JavaScript is enabled. + """ + + Scenario: POST /forms/chromium/convert/markdown (JavaScript Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_DISABLE_JAVASCRIPT | true | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + JavaScript is enabled. + """ + + Scenario: POST /forms/chromium/convert/markdown (Fail On Resource HTTP Status Codes) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | failOnResourceHttpStatusCodes | [499,599] | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid HTTP status code from resources: + https://httpstat.us/400 - 400: Bad Request + """ + + Scenario: POST /forms/chromium/convert/markdown (Fail On Resource Loading Failed) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | failOnResourceLoadingFailed | true | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Chromium failed to load resources + """ + Then the response body should contain string: + """ + resource Stylesheet: net::ERR_CONNECTION_REFUSED + """ + Then the response body should contain string: + """ + resource Stylesheet: net::ERR_FILE_NOT_FOUND + """ + + Scenario: POST /forms/chromium/convert/markdown (Fail On Console Exceptions) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/feature-rich-markdown/index.html | file | + | files | testdata/feature-rich-markdown/table.md | file | + | failOnConsoleExceptions | true | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Chromium console exceptions + """ + Then the response body should contain string: + """ + exception "Uncaught" (93:12): Error: Exception 1 + """ + Then the response body should contain string: + """ + exception "Uncaught" (97:12): Error: Exception 2 + """ + + Scenario: POST /forms/chromium/convert/markdown (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-3-markdown/index.html | file | + | files | testdata/pages-3-markdown/page_1.md | file | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Markdown file(s) not found: 'page_2.md'; 'page_3.md' + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | singlePage | foo | field | + | paperWidth | foo | field | + | paperHeight | foo | field | + | marginTop | foo | field | + | marginBottom | foo | field | + | marginLeft | foo | field | + | marginRight | foo | field | + | preferCssPageSize | foo | field | + | generateDocumentOutline | foo | field | + | printBackground | foo | field | + | omitBackground | foo | field | + | landscape | foo | field | + | scale | foo | field | + | waitDelay | foo | field | + | emulatedMediaType | foo | field | + | failOnHttpStatusCodes | foo | field | + | failOnResourceHttpStatusCodes | foo | field | + | failOnResourceLoadingFailed | foo | field | + | failOnConsoleExceptions | foo | field | + | skipNetworkIdleEvent | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form file 'index.html' is required; no form file found for extensions: [.md] + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | omitBackground | true | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + omitBackground requires printBackground set to true + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | paperWidth | 0 | field | + | paperHeight | 0 | field | + | marginTop | 1000000 | field | + | marginBottom | 1000000 | field | + | marginLeft | 1000000 | field | + | marginRight | 1000000 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Chromium does not handle the provided settings; please check for aberrant form values + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | nativePageRanges | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Chromium does not handle the page ranges 'foo' (nativePageRanges) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | waitForExpression | undefined | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + The expression 'undefined' (waitForExpression) returned an exception or undefined + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | cookies | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'cookies' is invalid (got 'foo', resulting to unmarshal cookies: invalid character 'o' in literal false (expecting 'a')) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | cookies | [{"name":"yummy_cookie","value":"choco"}] | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'cookies' is invalid (got '[{"name":"yummy_cookie","value":"choco"}]', resulting to cookie 0 must have its name, value and domain set) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | extraHttpHeaders | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got 'foo', resulting to unmarshal extraHttpHeaders: invalid character 'o' in literal false (expecting 'a')) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | extraHttpHeaders | {"foo":"bar;scope;;"} | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope;;"}', resulting to invalid scope '' for header 'foo') + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | extraHttpHeaders | {"foo":"bar;scope=*."} | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | splitMode | foo | field | + | splitSpan | 2 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitMode' is invalid (got 'foo', resulting to wrong value, expected either 'intervals' or 'pages') + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | splitMode | intervals | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitSpan' is invalid (got 'foo', resulting to strconv.Atoi: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | splitMode | pages | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/chromium/convert/markdown (Split Intervals) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-3-markdown/index.html | file | + | files | testdata/pages-3-markdown/page_1.md | file | + | files | testdata/pages-3-markdown/page_2.md | file | + | files | testdata/pages-3-markdown/page_3.md | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 2 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/markdown (Split Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-3-markdown/index.html | file | + | files | testdata/pages-3-markdown/page_1.md | file | + | files | testdata/pages-3-markdown/page_2.md | file | + | files | testdata/pages-3-markdown/page_3.md | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/markdown (Split Pages & Unify) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-3-markdown/index.html | file | + | files | testdata/pages-3-markdown/page_1.md | file | + | files | testdata/pages-3-markdown/page_2.md | file | + | files | testdata/pages-3-markdown/page_3.md | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/markdown (Split Many PDFs - Lot of Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-12-markdown/index.html | file | + | files | testdata/pages-12-markdown/page_1.md | file | + | files | testdata/pages-12-markdown/page_2.md | file | + | files | testdata/pages-12-markdown/page_3.md | file | + | files | testdata/pages-12-markdown/page_4.md | file | + | files | testdata/pages-12-markdown/page_5.md | file | + | files | testdata/pages-12-markdown/page_6.md | file | + | files | testdata/pages-12-markdown/page_7.md | file | + | files | testdata/pages-12-markdown/page_8.md | file | + | files | testdata/pages-12-markdown/page_9.md | file | + | files | testdata/pages-12-markdown/page_10.md | file | + | files | testdata/pages-12-markdown/page_11.md | file | + | files | testdata/pages-12-markdown/page_12.md | file | + | splitMode | intervals | field | + | splitSpan | 1 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 12 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + | *_2.pdf | + | *_3.pdf | + | *_4.pdf | + | *_5.pdf | + | *_6.pdf | + | *_7.pdf | + | *_8.pdf | + | *_9.pdf | + | *_10.pdf | + | *_11.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_11.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_11.pdf" PDF should have the following content at page 1: + """ + Page 12 + """ + + Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/chromium/convert/markdown (Split & PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/pages-3-markdown/index.html | file | + | files | testdata/pages-3-markdown/page_1.md | file | + | files | testdata/pages-3-markdown/page_2.md | file | + | files | testdata/pages-3-markdown/page_3.md | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 2 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/chromium/convert/markdown (Metadata) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/chromium/convert/markdown (Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | flatten | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be flatten + + Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | flatten | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + Then the response PDF(s) should be flatten + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/chromium/convert/markdown (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + Then the response status code should be 404 + + Scenario: POST /forms/chromium/convert/markdown (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | Gotenberg-Trace | forms_chromium_convert_html | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_chromium_convert_html" + Then the Gotenberg container should log the following entries: + | "trace":"forms_chromium_convert_html" | + + Scenario: POST /forms/chromium/convert/markdown (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page-1-markdown/index.html","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + | files | testdata/page-1-markdown/page_1.md | file | + Then the response status code should be 200 + Then the file request header "X-Foo" should be "bar" + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/chromium/convert/markdown (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + | Gotenberg-Output-Filename | foo | header | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then there should be the following file(s) in the webhook request: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/chromium/convert/markdown (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + Then the response status code should be 401 + + Scenario: POST /foo/forms/chromium/convert/markdown (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/chromium/convert/markdown" endpoint with the following form data and header(s): + | files | testdata/page-1-markdown/index.html | file | + | files | testdata/page-1-markdown/page_1.md | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/chromium_convert_url.feature b/test/integration/features/chromium_convert_url.feature new file mode 100644 index 00000000..b48c2f4a --- /dev/null +++ b/test/integration/features/chromium_convert_url.feature @@ -0,0 +1,929 @@ +Feature: /forms/chromium/convert/url + + Scenario: POST /forms/chromium/convert/url (Default) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/chromium/convert/url (Single Page) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-12-html/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 12 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + Page 12 + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-12-html/index.html | field | + | singlePage | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + # page-break-after: always; tells the browser's print engine to force a page break after each element, + # even when calculating a large enough paper height, Chromium's PDF rendering will still honor those page break + # directives. + """ + Page 12 + """ + + Scenario: POST /forms/chromium/convert/url (Landscape) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should NOT be set to landscape orientation + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | landscape | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should be set to landscape orientation + + Scenario: POST /forms/chromium/convert/url (Native Page Ranges) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-12-html/index.html | field | + | nativePageRanges | 2-3 | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/url (Header & Footer) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-12-html/index.html | field | + | files | testdata/header-footer-html/header.html | file | + | files | testdata/header-footer-html/footer.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 12 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Pages 12 + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + 1 of 12 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + Pages 12 + """ + Then the "foo.pdf" PDF should have the following content at page 12: + """ + 12 of 12 + """ + + Scenario: POST /forms/chromium/convert/url (Custom HTTP Headers) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | userAgent | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) | field | + | extraHttpHeaders | {"X-Header":"foo","X-Scoped-Header-1":"bar;scope=https?:\\/\\/([a-zA-Z0-9-]+\\\\.)*domain\\\\.com\\/.*","X-Scoped-Header-2":"baz;scope=https?:\\/\\/([a-zA-Z0-9-]+\\\\.)*docker\\\\.internal:(\\\\d+)\\/.*"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the server request header "User-Agent" should be "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)" + Then the server request header "X-Header" should be "foo" + Then the server request header "X-Scoped-Header-1" should be "" + Then the server request header "X-Scoped-Header-2" should be "baz" + + Scenario: POST /forms/chromium/convert/url (Cookies) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | cookies | [{"name":"cookie_1","value":"foo","domain":"host.docker.internal:%d"},{"name":"cookie_2","value":"bar","domain":"domain.com"}] | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the server request cookie "cookie_1" should be "foo" + Then the server request cookie "cookie_2" should be "" + + Scenario: POST /forms/chromium/convert/url (Wait Delay) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | waitDelay | 2.5s | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + + Scenario: POST /forms/chromium/convert/url (Wait For Expression) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | waitForExpression | window.globalVar === 'ready' | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. + """ + + Scenario: POST /forms/chromium/convert/url (Emulated Media Type) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | emulatedMediaType | print | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | emulatedMediaType | screen | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'print'. + """ + + Scenario: POST /forms/chromium/convert/url (Default Allow / Deny Lists) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the Gotenberg container should NOT log the following entries: + # Modern browsers block file URIs from being loaded into iframes when the parent page is served over HTTP/HTTPS. + | 'file:///etc/passwd' matches the expression from the denied list | + + Scenario: POST /forms/chromium/convert/url (Main URL does NOT match allowed list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | ^file:(?!//\\/tmp/).* | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + Then the response status code should be 403 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Forbidden + """ + + Scenario: POST /forms/chromium/convert/url (Main URL does match denied list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | | + | CHROMIUM_DENY_LIST | ^http.* | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + Then the response status code should be 403 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Forbidden + """ + + Scenario: POST /forms/chromium/convert/url (Request does not match the allowed list) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_ALLOW_LIST | ^.* | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the Gotenberg container should NOT log the following entries: + # Modern browsers block file URIs from being loaded into iframes when the parent page is served over HTTP/HTTPS. + | 'file:///etc/passwd' does not match the expression from the allowed list | + + Scenario: POST /forms/chromium/convert/url (JavaScript Enabled) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + JavaScript is enabled. + """ + + Scenario: POST /forms/chromium/convert/url (JavaScript Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_DISABLE_JAVASCRIPT | true | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + JavaScript is enabled. + """ + + Scenario: POST /forms/chromium/convert/url (Fail On Resource HTTP Status Codes) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | failOnResourceHttpStatusCodes | [499,599] | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Invalid HTTP status code from resources: + """ + Then the response body should contain string: + """ + /favicon.ico - 404: Not Found + """ + + Scenario: POST /forms/chromium/convert/url (Fail On Resource Loading Failed) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | failOnResourceLoadingFailed | true | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Chromium failed to load resources + """ + Then the response body should contain string: + """ + resource Stylesheet: net::ERR_CONNECTION_REFUSED + """ + + Scenario: POST /forms/chromium/convert/url (Fail On Console Exceptions) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | failOnConsoleExceptions | true | field | + Then the response status code should be 409 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should contain string: + """ + Chromium console exceptions + """ + Then the response body should contain string: + """ + exception "Uncaught" (56:12): Error: Exception 1 + """ + Then the response body should contain string: + """ + exception "Uncaught" (60:12): Error: Exception 2 + """ + + Scenario: POST /forms/chromium/convert/url (Bad Request) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | singlePage | foo | field | + | paperWidth | foo | field | + | paperHeight | foo | field | + | marginTop | foo | field | + | marginBottom | foo | field | + | marginLeft | foo | field | + | marginRight | foo | field | + | preferCssPageSize | foo | field | + | generateDocumentOutline | foo | field | + | printBackground | foo | field | + | omitBackground | foo | field | + | landscape | foo | field | + | scale | foo | field | + | waitDelay | foo | field | + | emulatedMediaType | foo | field | + | failOnHttpStatusCodes | foo | field | + | failOnResourceHttpStatusCodes | foo | field | + | failOnResourceLoadingFailed | foo | field | + | failOnConsoleExceptions | foo | field | + | skipNetworkIdleEvent | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'skipNetworkIdleEvent' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceHttpStatusCodes' is invalid (got 'foo', resulting to unmarshal failOnResourceHttpStatusCodes: invalid character 'o' in literal false (expecting 'a')); form field 'failOnResourceLoadingFailed' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'failOnConsoleExceptions' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'waitDelay' is invalid (got 'foo', resulting to time: invalid duration "foo"); form field 'emulatedMediaType' is invalid (got 'foo', resulting to wrong value, expected either 'screen', 'print' or empty); form field 'omitBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'printBackground' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'scale' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'singlePage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'paperWidth' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'paperHeight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginTop' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginBottom' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginLeft' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'marginRight' is invalid (got 'foo', resulting to strconv.ParseFloat: parsing "foo": invalid syntax); form field 'preferCssPageSize' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'generateDocumentOutline' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'url' is required + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | omitBackground | true | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + omitBackground requires printBackground set to true + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | paperWidth | 0 | field | + | paperHeight | 0 | field | + | marginTop | 1000000 | field | + | marginBottom | 1000000 | field | + | marginLeft | 1000000 | field | + | marginRight | 1000000 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Chromium does not handle the provided settings; please check for aberrant form values + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | nativePageRanges | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Chromium does not handle the page ranges 'foo' (nativePageRanges) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | waitForExpression | undefined | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + The expression 'undefined' (waitForExpression) returned an exception or undefined + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | cookies | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'cookies' is invalid (got 'foo', resulting to unmarshal cookies: invalid character 'o' in literal false (expecting 'a')) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | cookies | [{"name":"yummy_cookie","value":"choco"}] | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'cookies' is invalid (got '[{"name":"yummy_cookie","value":"choco"}]', resulting to cookie 0 must have its name, value and domain set) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | extraHttpHeaders | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got 'foo', resulting to unmarshal extraHttpHeaders: invalid character 'o' in literal false (expecting 'a')) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | extraHttpHeaders | {"foo":"bar;scope;;"} | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope;;"}', resulting to invalid scope '' for header 'foo') + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | extraHttpHeaders | {"foo":"bar;scope=*."} | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | splitMode | foo | field | + | splitSpan | 2 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitMode' is invalid (got 'foo', resulting to wrong value, expected either 'intervals' or 'pages') + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | splitMode | intervals | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitSpan' is invalid (got 'foo', resulting to strconv.Atoi: parsing "foo": invalid syntax) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | splitMode | pages | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/chromium/convert/url (Split Intervals) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-3-html/index.html | field | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 2 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/url (Split Pages) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-3-html/index.html | field | + | splitMode | pages | field | + | splitSpan | 2- | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/url (Split Pages & Unify) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-3-html/index.html | field | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/chromium/convert/url (Split Many PDFs - Lot of Pages) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-12-html/index.html | field | + | splitMode | intervals | field | + | splitSpan | 1 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 12 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + | *_2.pdf | + | *_3.pdf | + | *_4.pdf | + | *_5.pdf | + | *_6.pdf | + | *_7.pdf | + | *_8.pdf | + | *_9.pdf | + | *_10.pdf | + | *_11.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_11.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_11.pdf" PDF should have the following content at page 1: + """ + Page 12 + """ + + Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/chromium/convert/url (Split & PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/pages-3-html/index.html | field | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 2 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/chromium/convert/url (Metadata) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/chromium/convert/url (Flatten) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | flatten | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be flatten + + Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | flatten | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + Then the response PDF(s) should be flatten + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/chromium/convert/url (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | CHROMIUM_DISABLE_ROUTES | true | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + Then the response status code should be 404 + + Scenario: POST /forms/chromium/convert/url (Gotenberg Trace) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | Gotenberg-Trace | forms_chromium_convert_url | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_chromium_convert_url" + Then the Gotenberg container should log the following entries: + | "trace":"forms_chromium_convert_url" | + + Scenario: POST /forms/chromium/convert/url (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | Gotenberg-Output-Filename | foo | header | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then there should be the following file(s) in the webhook request: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/chromium/convert/url (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + Then the response status code should be 401 + + Scenario: POST /foo/forms/chromium/convert/url (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + Given I have a static server + When I make a "POST" request to Gotenberg at the "/foo/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/debug.feature b/test/integration/features/debug.feature new file mode 100644 index 00000000..a68e5fe7 --- /dev/null +++ b/test/integration/features/debug.feature @@ -0,0 +1,147 @@ +Feature: /debug + + Scenario: GET /debug (Disabled) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/debug" endpoint + Then the response status code should be 404 + + Scenario: GET /debug (Enabled) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + When I make a "GET" request to Gotenberg at the "/debug" endpoint + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "version": "{version}", + "architecture": "ignore", + "modules": [ + "api", + "chromium", + "exiftool", + "libreoffice", + "libreoffice-api", + "libreoffice-pdfengine", + "logging", + "pdfcpu", + "pdfengines", + "pdftk", + "prometheus", + "qpdf", + "webhook" + ], + "modules_additional_data": { + "chromium": { + "version": "ignore" + }, + "exiftool": { + "version": "ignore" + }, + "libreoffice-api": { + "version": "ignore" + }, + "pdfcpu": { + "version": "ignore" + }, + "pdftk": { + "version": "ignore" + }, + "qpdf": { + "version": "ignore" + } + }, + "flags": { + "api-bind-ip": "", + "api-body-limit": "", + "api-disable-download-from": "false", + "api-disable-health-check-logging": "false", + "api-download-from-allow-list": "", + "api-download-from-deny-list": "", + "api-download-from-max-retry": "4", + "api-enable-basic-auth": "false", + "api-enable-debug-route": "true", + "api-port": "3000", + "api-port-from-env": "", + "api-root-path": "/", + "api-start-timeout": "30s", + "api-timeout": "30s", + "api-tls-cert-file": "", + "api-tls-key-file": "", + "api-trace-header": "Gotenberg-Trace", + "chromium-allow-file-access-from-files": "false", + "chromium-allow-insecure-localhost": "false", + "chromium-allow-list": "", + "chromium-auto-start": "false", + "chromium-clear-cache": "false", + "chromium-clear-cookies": "false", + "chromium-deny-list": "^file:(?!//\\/tmp/).*", + "chromium-disable-javascript": "false", + "chromium-disable-routes": "false", + "chromium-disable-web-security": "false", + "chromium-host-resolver-rules": "", + "chromium-ignore-certificate-errors": "false", + "chromium-incognito": "false", + "chromium-max-queue-size": "0", + "chromium-proxy-server": "", + "chromium-restart-after": "10", + "chromium-start-timeout": "20s", + "gotenberg-graceful-shutdown-duration": "30s", + "libreoffice-auto-start": "false", + "libreoffice-disable-routes": "false", + "libreoffice-max-queue-size": "0", + "libreoffice-restart-after": "10", + "libreoffice-start-timeout": "20s", + "log-fields-prefix": "", + "log-format": "auto", + "log-level": "info", + "pdfengines-convert-engines": "[libreoffice-pdfengine]", + "pdfengines-disable-routes": "false", + "pdfengines-engines": "[]", + "pdfengines-flatten-engines": "[qpdf]", + "pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]", + "pdfengines-read-metadata-engines": "[exiftool]", + "pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]", + "pdfengines-write-metadata-engines": "[exiftool]", + "prometheus-collect-interval": "1s", + "prometheus-disable-collect": "false", + "prometheus-disable-route-logging": "false", + "prometheus-namespace": "gotenberg", + "webhook-allow-list": "", + "webhook-client-timeout": "30s", + "webhook-deny-list": "", + "webhook-disable": "false", + "webhook-error-allow-list": "", + "webhook-error-deny-list": "", + "webhook-max-retry": "4", + "webhook-retry-max-wait": "30s", + "webhook-retry-min-wait": "1s" + } + } + """ + + Scenario: GET /debug (Gotenberg Trace) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + When I make a "GET" request to Gotenberg at the "/debug" endpoint with the following header(s): + | Gotenberg-Trace | debug | + Then the response status code should be 200 + Then the response header "Gotenberg-Trace" should be "debug" + Then the Gotenberg container should log the following entries: + | "trace":"debug" | + + Scenario: GET /debug (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "GET" request to Gotenberg at the "/debug" endpoint + Then the response status code should be 401 + + Scenario: GET /foo/debug (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "GET" request to Gotenberg at the "/foo/debug" endpoint + Then the response status code should be 200 diff --git a/test/integration/features/health.feature b/test/integration/features/health.feature new file mode 100644 index 00000000..887808f8 --- /dev/null +++ b/test/integration/features/health.feature @@ -0,0 +1,107 @@ +# TODO: +# 1. Check if down for each module. +# 2. Restarting modules do not make health check fail. + +Feature: /health + + Scenario: GET /health + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/health" endpoint + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json; charset=utf-8" + Then the response body should match JSON: + """ + { + "status": "up", + "details": { + "chromium": { + "status": "up", + "timestamp": "ignore" + }, + "libreoffice": { + "status": "up", + "timestamp": "ignore" + } + } + } + """ + Then the Gotenberg container should log the following entries: + | "path":"/health" | + + Scenario: GET /health (No Logging) + Given I have a Gotenberg container with the following environment variable(s): + | API_DISABLE_HEALTH_CHECK_LOGGING | true | + When I make a "GET" request to Gotenberg at the "/health" endpoint + Then the response status code should be 200 + Then the Gotenberg container should NOT log the following entries: + | "path":"/health" | + + Scenario: GET /health (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/health" endpoint with the following header(s): + | Gotenberg-Trace | get_health | + Then the response status code should be 200 + Then the response header "Gotenberg-Trace" should be "get_health" + Then the Gotenberg container should log the following entries: + | "trace":"get_health" | + + Scenario: GET /health (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "GET" request to Gotenberg at the "/health" endpoint + Then the response status code should be 200 + + Scenario: GET /foo/health (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ROOT_PATH | /foo/ | + When I make a "GET" request to Gotenberg at the "/foo/health" endpoint + Then the response status code should be 200 + + Scenario: HEAD /health + Given I have a default Gotenberg container + When I make a "HEAD" request to Gotenberg at the "/health" endpoint + Then the response status code should be 200 + Then the response body should match string: + """ + + """ + Then the Gotenberg container should log the following entries: + | "path":"/health" | + + Scenario: HEAD /health (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "HEAD" request to Gotenberg at the "/health" endpoint with the following header(s): + | Gotenberg-Trace | head_health | + Then the response status code should be 200 + Then the response header "Gotenberg-Trace" should be "head_health" + Then the Gotenberg container should log the following entries: + | "trace":"head_health" | + + Scenario: HEAD /health (No Logging) + Given I have a Gotenberg container with the following environment variable(s): + | API_DISABLE_HEALTH_CHECK_LOGGING | true | + When I make a "HEAD" request to Gotenberg at the "/health" endpoint + Then the response status code should be 200 + Then the Gotenberg container should NOT log the following entries: + | "path":"/health" | + + Scenario: HEAD /health (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "HEAD" request to Gotenberg at the "/health" endpoint + Then the response status code should be 200 + + Scenario: HEAD /foo/health (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ROOT_PATH | /foo/ | + When I make a "HEAD" request to Gotenberg at the "/foo/health" endpoint + Then the response status code should be 200 + + +# TODO: +# 1. Check if down for each module. +# 2. Restarting modules do not make health check fail. \ No newline at end of file diff --git a/test/integration/features/libreoffice_convert.feature b/test/integration/features/libreoffice_convert.feature new file mode 100644 index 00000000..91a4fcb9 --- /dev/null +++ b/test/integration/features/libreoffice_convert.feature @@ -0,0 +1,614 @@ +Feature: /forms/libreoffice/convert + + Scenario: POST /forms/libreoffice/convert (Single Document) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/libreoffice/convert (Many Documents) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | files | testdata/page_2.docx | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.zip | + | page_1.docx.pdf | + | page_2.docx.pdf | + Then the "page_1.docx.pdf" PDF should have 1 page(s) + Then the "page_2.docx.pdf" PDF should have 1 page(s) + Then the "page_1.docx.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "page_2.docx.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + + Scenario: POST /forms/libreoffice/convert (Protected) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/protected_page_1.docx | file | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + LibreOffice failed to process a document: a password may be required, or, if one has been given, it is invalid. In any case, the exact cause is uncertain. + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/protected_page_1.docx | file | + | password | foo | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + + Scenario: POST /forms/libreoffice/convert (Landscape) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | Gotenberg-Output-Filename | foo | header | + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should NOT be set to landscape orientation + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | landscape | true | field | + | Gotenberg-Output-Filename | foo | header | + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should be set to landscape orientation + + Scenario: POST /forms/libreoffice/convert (Native Page Ranges - Single Document) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | nativePageRanges | 2-3 | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/libreoffice/convert (Native Page Ranges - Many Documents) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | files | testdata/pages_12.docx | file | + | nativePageRanges | 2-3 | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.zip | + | pages_3.docx.pdf | + | pages_12.docx.pdf | + Then the "pages_3.docx.pdf" PDF should have 2 page(s) + Then the "pages_12.docx.pdf" PDF should have 2 page(s) + Then the "pages_3.docx.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.docx.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + Then the "pages_12.docx.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_12.docx.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/libreoffice/convert (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | landscape | foo | field | + | exportFormFields | foo | field | + | allowDuplicateFieldNames | foo | field | + | exportBookmarks | foo | field | + | exportBookmarksToPdfDestination | foo | field | + | exportPlaceholders | foo | field | + | exportNotes | foo | field | + | exportNotesPages | foo | field | + | exportOnlyNotesPages | foo | field | + | exportNotesInMargin | foo | field | + | convertOooTargetToPdfTarget | foo | field | + | exportLinksRelativeFsys | foo | field | + | exportHiddenSlides | foo | field | + | skipEmptyPages | foo | field | + | addOriginalDocumentAsStream | foo | field | + | singlePageSheets | foo | field | + | losslessImageCompression | foo | field | + | quality | -1 | field | + | reduceImageResolution | foo | field | + | maxImageResolution | 10 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: no form file found for extensions: [.123 .602 .abw .bib .bmp .cdr .cgm .cmx .csv .cwk .dbf .dif .doc .docm .docx .dot .dotm .dotx .dxf .emf .eps .epub .fodg .fodp .fods .fodt .fopd .gif .htm .html .hwp .jpeg .jpg .key .ltx .lwp .mcw .met .mml .mw .numbers .odd .odg .odm .odp .ods .odt .otg .oth .otp .ots .ott .pages .pbm .pcd .pct .pcx .pdb .pdf .pgm .png .pot .potm .potx .ppm .pps .ppt .pptm .pptx .psd .psw .pub .pwp .pxl .ras .rtf .sda .sdc .sdd .sdp .sdw .sgl .slk .smf .stc .std .sti .stw .svg .svm .swf .sxc .sxd .sxg .sxi .sxm .sxw .tga .tif .tiff .txt .uof .uop .uos .uot .vdx .vor .vsd .vsdm .vsdx .wb2 .wk1 .wks .wmf .wpd .wpg .wps .xbm .xhtml .xls .xlsb .xlsm .xlsx .xlt .xltm .xltx .xlw .xml .xpm .zabw]; form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportFormFields' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'allowDuplicateFieldNames' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportBookmarks' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportBookmarksToPdfDestination' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportPlaceholders' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotes' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotesPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportOnlyNotesPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotesInMargin' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'convertOooTargetToPdfTarget' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportLinksRelativeFsys' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportHiddenSlides' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'skipEmptyPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'addOriginalDocumentAsStream' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'singlePageSheets' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'losslessImageCompression' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'quality' is invalid (got '-1', resulting to value is inferior to 1); form field 'reduceImageResolution' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'maxImageResolution' is invalid (got '10', resulting to value is not 75, 150, 300, 600 or 1200) + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | nativePageRanges | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + LibreOffice failed to process a document: possible causes include malformed page ranges 'foo' (nativePageRanges), or, if a password has been provided, it may not be required. In any case, the exact cause is uncertain. + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | password | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + LibreOffice failed to process a document: possible causes include malformed page ranges '' (nativePageRanges), or, if a password has been provided, it may not be required. In any case, the exact cause is uncertain. + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/protected_page_1.docx | file | + | password | bar | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + LibreOffice failed to process a document: a password may be required, or, if one has been given, it is invalid. In any case, the exact cause is uncertain. + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | files | testdata/page_2.docx | file | + | merge | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'merge' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | foo | field | + | splitSpan | 2 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitMode' is invalid (got 'foo', resulting to wrong value, expected either 'intervals' or 'pages') + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | intervals | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitSpan' is invalid (got 'foo', resulting to strconv.Atoi: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | pages | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + A PDF format in '{PdfA:foo PdfUa:false}' is not supported + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/libreoffice/convert (Merge) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | files | testdata/page_2.docx | file | + | merge | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + + Scenario: POST /forms/libreoffice/convert (Merge & Split) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | files | testdata/page_2.docx | file | + | merge | true | field | + | splitMode | intervals | field | + | splitSpan | 1 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | *_0.pdf | + | *_1.pdf | + Then the "*_0.pdf" PDF should have 1 page(s) + Then the "*_1.pdf" PDF should have 1 page(s) + Then the "*_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "*_1.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + + Scenario: POST /forms/libreoffice/convert (Split Intervals) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.docx_0.pdf | + | pages_3.docx_1.pdf | + Then the "pages_3.docx_0.pdf" PDF should have 2 page(s) + Then the "pages_3.docx_1.pdf" PDF should have 1 page(s) + Then the "pages_3.docx_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3.docx_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3.docx_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/libreoffice/convert (Split Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.docx_0.pdf | + | pages_3.docx_1.pdf | + Then the "pages_3.docx_0.pdf" PDF should have 1 page(s) + Then the "pages_3.docx_1.pdf" PDF should have 1 page(s) + Then the "pages_3.docx_0.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.docx_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/libreoffice/convert (Split Pages & Unify) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.docx.pdf | + Then the "pages_3.docx.pdf" PDF should have 2 page(s) + Then the "pages_3.docx.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.docx.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/libreoffice/convert (Split Many PDFs - Lot of Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_12.docx | file | + | files | testdata/pages_3.docx | file | + | splitMode | intervals | field | + | splitSpan | 1 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 15 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.docx_0.pdf | + | pages_3.docx_1.pdf | + | pages_3.docx_2.pdf | + | pages_12.docx_0.pdf | + | pages_12.docx_1.pdf | + | pages_12.docx_2.pdf | + | pages_12.docx_3.pdf | + | pages_12.docx_4.pdf | + | pages_12.docx_5.pdf | + | pages_12.docx_6.pdf | + | pages_12.docx_7.pdf | + | pages_12.docx_8.pdf | + | pages_12.docx_9.pdf | + | pages_12.docx_10.pdf | + | pages_12.docx_11.pdf | + Then the "pages_3.docx_0.pdf" PDF should have 1 page(s) + Then the "pages_3.docx_2.pdf" PDF should have 1 page(s) + Then the "pages_12.docx_0.pdf" PDF should have 1 page(s) + Then the "pages_12.docx_11.pdf" PDF should have 1 page(s) + Then the "pages_3.docx_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3.docx_2.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the "pages_12.docx_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_12.docx_11.pdf" PDF should have the following content at page 1: + """ + Page 12 + """ + + Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/libreoffice/convert (Split & PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/pages_3.docx | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.docx_0.pdf | + | pages_3.docx_1.pdf | + Then the "pages_3.docx_0.pdf" PDF should have 2 page(s) + Then the "pages_3.docx_1.pdf" PDF should have 1 page(s) + Then the "pages_3.docx_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3.docx_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3.docx_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/libreoffice/convert (Metadata) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/libreoffice/convert (Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | flatten | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be flatten + + Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | flatten | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + Then the response PDF(s) should be flatten + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/libreoffice/convert (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | LIBREOFFICE_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + Then the response status code should be 404 + + Scenario: POST /forms/libreoffice/convert (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | Gotenberg-Trace | forms_libreoffice_convert | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_libreoffice_convert" + Then the Gotenberg container should log the following entries: + | "trace":"forms_libreoffice_convert" | + + Scenario: POST /forms/libreoffice/convert (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.docx","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + Then the response status code should be 200 + Then the file request header "X-Foo" should be "bar" + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/libreoffice/convert (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + | Gotenberg-Output-Filename | foo | header | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then there should be the following file(s) in the webhook request: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + + Scenario: POST /forms/libreoffice/convert (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + Then the response status code should be 401 + + Scenario: POST /foo/forms/libreoffice/convert (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/libreoffice/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.docx | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/pdfengines_convert.feature b/test/integration/features/pdfengines_convert.feature new file mode 100644 index 00000000..867dfcf0 --- /dev/null +++ b/test/integration/features/pdfengines_convert.feature @@ -0,0 +1,185 @@ +# TODO: +# 1. PDF/UA-2. + +Feature: /forms/pdfengines/convert + + Scenario: POST /forms/pdfengines/convert (Single PDF/A-1b) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Single PDF/A-2b) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-2b | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-2b" with a tolerance of 1 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Single PDF/A-3b) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-3b | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 1 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Single PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Single PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | pdfa | PDF/A-1b | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | pdfa | PDF/A-1b | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: no form file found for extensions: [.pdf] + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + + Scenario: POST /forms/pdfengines/convert (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + | Gotenberg-Trace | forms_pdfengines_convert | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_pdfengines_convert" + Then the Gotenberg container should log the following entries: + | "trace":"forms_pdfengines_convert" | + + Scenario: POST /forms/pdfengines/convert (Output Filename - Single PDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be the following file(s) in the response: + | foo.pdf | + + Scenario: POST /forms/pdfengines/convert (Output Filename - Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | pdfa | PDF/A-1b | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be the following file(s) in the response: + | foo.zip | + | page_1.pdf | + | page_2.pdf | + + Scenario: POST /forms/pdfengines/convert (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + | pdfa | PDF/A-1b | field | + Then the file request header "X-Foo" should be "bar" + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/pdfengines/convert (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then the webhook request PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + + Scenario: POST /forms/pdfengines/convert (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + Then the response status code should be 401 + + Scenario: POST /foo/forms/pdfengines/convert (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/convert" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | pdfa | PDF/A-1b | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/pdfengines_flatten.feature b/test/integration/features/pdfengines_flatten.feature new file mode 100644 index 00000000..521a93ae --- /dev/null +++ b/test/integration/features/pdfengines_flatten.feature @@ -0,0 +1,112 @@ +Feature: /forms/pdfengines/flatten + + Scenario: POST /forms/pdfengines/flatten (Single PDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then the response PDF(s) should be flatten + + Scenario: POST /forms/pdfengines/flatten (Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then the response PDF(s) should be flatten + + Scenario: POST /forms/pdfengines/flatten (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: no form file found for extensions: [.pdf] + """ + + Scenario: POST /forms/pdfengines/flatten (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 404 + + Scenario: POST /forms/pdfengines/flatten (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | Gotenberg-Trace | forms_pdfengines_flatten | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_pdfengines_flatten" + Then the Gotenberg container should log the following entries: + | "trace":"forms_pdfengines_flatten" | + + Scenario: POST /forms/pdfengines/flatten (Output Filename - Single PDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be the following file(s) in the response: + | foo.pdf | + + Scenario: POST /forms/pdfengines/flatten (Output Filename - Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be the following file(s) in the response: + | foo.zip | + | page_1.pdf | + | page_2.pdf | + + Scenario: POST /forms/pdfengines/flatten (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + Then the file request header "X-Foo" should be "bar" + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/pdfengines/flatten (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then the response PDF(s) should be flatten + + Scenario: POST /forms/pdfengines/flatten (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 401 + + Scenario: POST /foo/forms/pdfengines/flatten (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/flatten" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/pdfengines_merge.feature b/test/integration/features/pdfengines_merge.feature new file mode 100644 index 00000000..248a81ad --- /dev/null +++ b/test/integration/features/pdfengines_merge.feature @@ -0,0 +1,324 @@ +Feature: /forms/pdfengines/merge + + Scenario: POST /forms/pdfengines/merge (default - QPDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + + Scenario: POST /forms/pdfengines/merge (pdfcpu) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_MERGE_ENGINES | pdfcpu | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + + Scenario: POST /forms/pdfengines/merge (PDFtk) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_MERGE_ENGINES | pdftk | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + + Scenario: POST /forms/pdfengines/merge (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: no form file found for extensions: [.pdf] + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/pdfengines/merge (Metadata) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/merge (Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | flatten | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the response PDF(s) should be flatten + + Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | flatten | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + Then the response PDF(s) should be flatten + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/merge (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + Then the response status code should be 404 + + Scenario: POST /forms/pdfengines/merge (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | Gotenberg-Trace | forms_pdfengines_merge | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_pdfengines_merge" + Then the Gotenberg container should log the following entries: + | "trace":"forms_pdfengines_merge" | + + Scenario: POST /forms/pdfengines/merge (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf"},{"url":"http://host.docker.internal:%d/static/testdata/page_2.pdf"}] | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/pdfengines/merge (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | Gotenberg-Output-Filename | foo | header | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + Then there should be the following file(s) in the webhook request: + | foo.pdf | + Then the "foo.pdf" PDF should have 2 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "foo.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + + Scenario: POST /forms/pdfengines/merge (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + Then the response status code should be 401 + + Scenario: POST /foo/forms/pdfengines/merge (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/merge" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" diff --git a/test/integration/features/pdfengines_metadata.feature b/test/integration/features/pdfengines_metadata.feature new file mode 100644 index 00000000..1cbb0ee1 --- /dev/null +++ b/test/integration/features/pdfengines_metadata.feature @@ -0,0 +1,268 @@ +Feature: /forms/pdfengines/{write|read} + + Scenario: POST /forms/pdfengines/metadata/{write|read} (Single PDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/metadata/{write|read} (Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files. | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/page_1.pdf | file | + | files | teststore/page_2.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "page_1.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + }, + "page_2.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/metadata/write (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is required; no form file found for extensions: [.pdf] + """ + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/pdfengines/metadata/read (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: no form file found for extensions: [.pdf] + """ + + Scenario: POST /forms/pdfengines/metadata/write (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + Then the response status code should be 404 + + Scenario: POST /forms/pdfengines/metadata/read (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 404 + + Scenario: POST /forms/pdfengines/metadata/write (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Trace | forms_pdfengines_metadata_write | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then the response header "Gotenberg-Trace" should be "forms_pdfengines_metadata_write" + Then the Gotenberg container should log the following entries: + | "trace":"forms_pdfengines_metadata_write" | + + Scenario: POST /forms/pdfengines/metadata/read (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | Gotenberg-Trace | forms_pdfengines_metadata_read | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response header "Gotenberg-Trace" should be "forms_pdfengines_metadata_read" + Then the Gotenberg container should log the following entries: + | "trace":"forms_pdfengines_metadata_read" | + + Scenario: POST /forms/pdfengines/metadata/write (Output Filename - Single PDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be the following file(s) in the response: + | foo.pdf | + + Scenario: POST /forms/pdfengines/metadata/write (Output Filename - Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | files | testdata/page_2.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be the following file(s) in the response: + | foo.zip | + | page_1.pdf | + | page_2.pdf | + + Scenario: POST /forms/pdfengines/metadata/write (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + | Gotenberg-Output-Filename | foo | header | + Then the file request header "X-Foo" should be "bar" + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + + Scenario: POST /forms/pdfengines/metadata/read (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + Then the file request header "X-Foo" should be "bar" + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + + Scenario: POST /forms/pdfengines/metadata/write (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the webhook request + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "foo.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/metadata/write (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + Then the response status code should be 401 + + Scenario: POST /forms/pdfengines/metadata/read (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + Then the response status code should be 401 + + Scenario: POST /foo/forms/pdfengines/metadata/{write|read} (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/metadata/write" endpoint with the following form data and header(s): + | files | testdata/page_1.pdf | file | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/foo.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" diff --git a/test/integration/features/pdfengines_split.feature b/test/integration/features/pdfengines_split.feature new file mode 100644 index 00000000..d9813bfb --- /dev/null +++ b/test/integration/features/pdfengines_split.feature @@ -0,0 +1,551 @@ +Feature: /forms/pdfengines/split + + Scenario: POST /forms/pdfengines/split (Intervals - Default - pdfcpu) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Pages - Default - pdfcpu) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 1 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Pages & Unify - Default - pdfcpu) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.pdf | + Then the "pages_3.pdf" PDF should have 2 page(s) + Then the "pages_3.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Pages & Unify - QPDF) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_SPLIT_ENGINES | qpdf | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2-z | field | + | splitUnify | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.pdf | + Then the "pages_3.pdf" PDF should have 2 page(s) + Then the "pages_3.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Pages & Unify - PDFtk) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_SPLIT_ENGINES | pdftk | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2-end | field | + | splitUnify | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3.pdf | + Then the "pages_3.pdf" PDF should have 2 page(s) + Then the "pages_3.pdf" PDF should have the following content at page 1: + """ + Page 2 + """ + Then the "pages_3.pdf" PDF should have the following content at page 2: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Many PDFs - Lot of Pages) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_12.pdf | file | + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 1 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 15 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + | pages_3_2.pdf | + | pages_12_0.pdf | + | pages_12_1.pdf | + | pages_12_2.pdf | + | pages_12_3.pdf | + | pages_12_4.pdf | + | pages_12_5.pdf | + | pages_12_6.pdf | + | pages_12_7.pdf | + | pages_12_8.pdf | + | pages_12_9.pdf | + | pages_12_10.pdf | + | pages_12_11.pdf | + Then the "pages_3_0.pdf" PDF should have 1 page(s) + Then the "pages_3_2.pdf" PDF should have 1 page(s) + Then the "pages_12_0.pdf" PDF should have 1 page(s) + Then the "pages_12_11.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_2.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the "pages_12_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_12_11.pdf" PDF should have the following content at page 1: + """ + Page 12 + """ + + Scenario: POST /forms/pdfengines/split (Bad Request) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitMode' is required; form field 'splitSpan' is required; no form file found for extensions: [.pdf] + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | foo | field | + | splitSpan | 2 | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitMode' is invalid (got 'foo', resulting to wrong value, expected either 'intervals' or 'pages') + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'splitSpan' is invalid (got 'foo', resulting to strconv.Atoi: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF split mode, while others may have failed to split due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + At least one PDF engine cannot process the requested PDF format, while others may have failed to convert due to different issues + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfua | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'pdfua' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax) + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | metadata | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a')) + """ + + Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 1 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + + Scenario: POST /forms/pdfengines/split (Metadata) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/pages_3_0.pdf | file | + | files | teststore/pages_3_1.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "pages_3_0.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + }, + "pages_3_1.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/split (Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | flatten | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be flatten + + Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1 & Metadata & Flatten) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | pdfa | PDF/A-1b | field | + | pdfua | true | field | + | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | + | flatten | true | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the response + Then there should be the following file(s) in the response: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 7 failed rule(s) + Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) + Then the response PDF(s) should be flatten + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s): + | files | teststore/pages_3_0.pdf | file | + | files | teststore/pages_3_1.pdf | file | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/json" + Then the response body should match JSON: + """ + { + "pages_3_0.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + }, + "pages_3_1.pdf": { + "Author": "Julien Neuhart", + "Copyright": "Julien Neuhart", + "CreateDate": "2006:09:18 16:27:50-04:00", + "Creator": "Gotenberg", + "Keywords": ["first", "second"], + "Marked": true, + "ModDate": "2006:09:18 16:27:50-04:00", + "PDFVersion": 1.7, + "Producer": "Gotenberg", + "Subject": "Sample", + "Title": "Sample", + "Trapped": "Unknown" + } + } + """ + + Scenario: POST /forms/pdfengines/split (Routes Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | PDFENGINES_DISABLE_ROUTES | true | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 404 + + Scenario: POST /forms/pdfengines/split (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | Gotenberg-Trace | forms_pdfengines_split | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then the response header "Gotenberg-Trace" should be "forms_pdfengines_split" + Then the Gotenberg container should log the following entries: + | "trace":"forms_pdfengines_split" | + + Scenario: POST /forms/pdfengines/split (Output Filename - Single PDF) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | pages | field | + | splitSpan | 2- | field | + | splitUnify | true | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be the following file(s) in the response: + | foo.pdf | + + Scenario: POST /forms/pdfengines/split (Output Filename - Many PDFs) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" + Then there should be the following file(s) in the response: + | foo.zip | + | pages_3_0.pdf | + | pages_3_1.pdf | + + Scenario: POST /forms/pdfengines/split (Download From) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/pages_3.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the file request header "X-Foo" should be "bar" + Then the response header "Content-Type" should be "application/zip" + + Scenario: POST /forms/pdfengines/split (Webhook) + Given I have a default Gotenberg container + Given I have a webhook server + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + | Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header | + | Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header | + Then the response status code should be 204 + When I wait for the asynchronous request to the webhook + Then the webhook request header "Content-Type" should be "application/zip" + Then there should be 2 PDF(s) in the webhook request + Then there should be the following file(s) in the webhook request: + | pages_3_0.pdf | + | pages_3_1.pdf | + Then the "pages_3_0.pdf" PDF should have 2 page(s) + Then the "pages_3_1.pdf" PDF should have 1 page(s) + Then the "pages_3_0.pdf" PDF should have the following content at page 1: + """ + Page 1 + """ + Then the "pages_3_0.pdf" PDF should have the following content at page 2: + """ + Page 2 + """ + Then the "pages_3_1.pdf" PDF should have the following content at page 1: + """ + Page 3 + """ + + Scenario: POST /forms/pdfengines/split (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 401 + + Scenario: POST /foo/forms/pdfengines/split (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/split" endpoint with the following form data and header(s): + | files | testdata/pages_3.pdf | file | + | splitMode | intervals | field | + | splitSpan | 2 | field | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/zip" diff --git a/test/integration/features/prometheus_metrics.feature b/test/integration/features/prometheus_metrics.feature new file mode 100644 index 00000000..e42c3d78 --- /dev/null +++ b/test/integration/features/prometheus_metrics.feature @@ -0,0 +1,90 @@ +# TODO: +# 1. Count restarts. +# 2. Count queue size. + +Feature: /prometheus/metrics + + Scenario: GET /prometheus/metrics (Enabled) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/prometheus/metrics" endpoint + Then the response status code should be 200 + Then the response header "Content-Type" should be "text/plain; version=0.0.4; charset=utf-8; escaping=underscores" + Then the response body should match string: + """ + # HELP gotenberg_chromium_requests_queue_size Current number of Chromium conversion requests waiting to be treated. + # TYPE gotenberg_chromium_requests_queue_size gauge + gotenberg_chromium_requests_queue_size 0 + # HELP gotenberg_chromium_restarts_count Current number of Chromium restarts. + # TYPE gotenberg_chromium_restarts_count gauge + gotenberg_chromium_restarts_count 0 + # HELP gotenberg_libreoffice_requests_queue_size Current number of LibreOffice conversion requests waiting to be treated. + # TYPE gotenberg_libreoffice_requests_queue_size gauge + gotenberg_libreoffice_requests_queue_size 0 + # HELP gotenberg_libreoffice_restarts_count Current number of LibreOffice restarts. + # TYPE gotenberg_libreoffice_restarts_count gauge + gotenberg_libreoffice_restarts_count 0 + + """ + Then the Gotenberg container should log the following entries: + | "path":"/prometheus/metrics" | + + Scenario: GET /prometheus/metrics (Custom Namespace) + Given I have a Gotenberg container with the following environment variable(s): + | PROMETHEUS_NAMESPACE | foo | + When I make a "GET" request to Gotenberg at the "/prometheus/metrics" endpoint + Then the response status code should be 200 + Then the response header "Content-Type" should be "text/plain; version=0.0.4; charset=utf-8; escaping=underscores" + Then the response body should match string: + """ + # HELP foo_chromium_requests_queue_size Current number of Chromium conversion requests waiting to be treated. + # TYPE foo_chromium_requests_queue_size gauge + foo_chromium_requests_queue_size 0 + # HELP foo_chromium_restarts_count Current number of Chromium restarts. + # TYPE foo_chromium_restarts_count gauge + foo_chromium_restarts_count 0 + # HELP foo_libreoffice_requests_queue_size Current number of LibreOffice conversion requests waiting to be treated. + # TYPE foo_libreoffice_requests_queue_size gauge + foo_libreoffice_requests_queue_size 0 + # HELP foo_libreoffice_restarts_count Current number of LibreOffice restarts. + # TYPE foo_libreoffice_restarts_count gauge + foo_libreoffice_restarts_count 0 + + """ + + Scenario: GET /prometheus/metrics (Disabled) + Given I have a Gotenberg container with the following environment variable(s): + | PROMETHEUS_DISABLE_COLLECT | true | + When I make a "GET" request to Gotenberg at the "/prometheus/metrics" endpoint + Then the response status code should be 404 + + Scenario: GET /prometheus/metrics (No Logging) + Given I have a Gotenberg container with the following environment variable(s): + | PROMETHEUS_DISABLE_ROUTE_LOGGING | true | + When I make a "GET" request to Gotenberg at the "/prometheus/metrics" endpoint + Then the response status code should be 200 + Then the Gotenberg container should NOT log the following entries: + | "path":"/prometheus/metrics" | + + Scenario: GET /prometheus/metrics (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/prometheus/metrics" endpoint with the following header(s): + | Gotenberg-Trace | prometheus_metrics | + Then the response status code should be 200 + Then the response header "Gotenberg-Trace" should be "prometheus_metrics" + Then the Gotenberg container should log the following entries: + | "trace":"prometheus_metrics" | + + Scenario: GET /prometheus/metrics (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "GET" request to Gotenberg at the "/prometheus/metrics" endpoint + Then the response status code should be 401 + + Scenario: GET /foo/prometheus/metrics (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "GET" request to Gotenberg at the "/foo/prometheus/metrics" endpoint + Then the response status code should be 200 diff --git a/test/integration/features/root.feature b/test/integration/features/root.feature new file mode 100644 index 00000000..9ebf0e17 --- /dev/null +++ b/test/integration/features/root.feature @@ -0,0 +1,62 @@ +Feature: / + + Scenario: GET / + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/" endpoint + Then the response status code should be 200 + Then the response header "Content-Type" should be "text/html; charset=UTF-8" + Then the response body should match string: + """ + Hey, Gotenberg has no UI, it's an API. Head to the documentation to learn how to interact with it ๐Ÿš€ + """ + + Scenario: GET / (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/" endpoint with the following header(s): + | Gotenberg-Trace | root | + Then the response status code should be 200 + Then the response header "Gotenberg-Trace" should be "root" + Then the Gotenberg container should log the following entries: + | "trace":"root" | + + Scenario: GET / (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "GET" request to Gotenberg at the "/" endpoint + Then the response status code should be 401 + + Scenario: GET /foo/ (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ROOT_PATH | /foo/ | + When I make a "GET" request to Gotenberg at the "/foo/" endpoint + Then the response status code should be 200 + + Scenario: GET /favicon.ico + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/favicon.ico" endpoint + Then the response status code should be 204 + + Scenario: GET /favicon.ico (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/favicon.ico" endpoint with the following header(s): + | Gotenberg-Trace | favicon | + Then the response status code should be 204 + Then the response header "Gotenberg-Trace" should be "favicon" + Then the Gotenberg container should log the following entries: + | "trace":"favicon" | + + Scenario: GET /favicon.ico (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "GET" request to Gotenberg at the "/favicon.ico" endpoint + Then the response status code should be 401 + + Scenario: GET /foo/favicon.ico (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ROOT_PATH | /foo/ | + When I make a "GET" request to Gotenberg at the "/foo/favicon.ico" endpoint + Then the response status code should be 204 diff --git a/test/integration/features/version.feature b/test/integration/features/version.feature new file mode 100644 index 00000000..b9d9ae93 --- /dev/null +++ b/test/integration/features/version.feature @@ -0,0 +1,35 @@ +Feature: /version + + Scenario: GET /version + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/version" endpoint + Then the response status code should be 200 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + {version} + """ + + Scenario: GET /version (Gotenberg Trace) + Given I have a default Gotenberg container + When I make a "GET" request to Gotenberg at the "/version" endpoint with the following header(s): + | Gotenberg-Trace | version | + Then the response status code should be 200 + Then the response header "Gotenberg-Trace" should be "version" + Then the Gotenberg container should log the following entries: + | "trace":"version" | + + Scenario: GET /version (Basic Auth) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_BASIC_AUTH | true | + | GOTENBERG_API_BASIC_AUTH_USERNAME | foo | + | GOTENBERG_API_BASIC_AUTH_PASSWORD | bar | + When I make a "GET" request to Gotenberg at the "/version" endpoint + Then the response status code should be 401 + + Scenario: GET /foo/version (Root Path) + Given I have a Gotenberg container with the following environment variable(s): + | API_ENABLE_DEBUG_ROUTE | true | + | API_ROOT_PATH | /foo/ | + When I make a "GET" request to Gotenberg at the "/foo/version" endpoint + Then the response status code should be 200 diff --git a/test/integration/main_test.go b/test/integration/main_test.go new file mode 100644 index 00000000..f7743281 --- /dev/null +++ b/test/integration/main_test.go @@ -0,0 +1,50 @@ +//go:build integration + +package integration + +import ( + "os" + "runtime" + "testing" + + "github.com/cucumber/godog" + "github.com/cucumber/godog/colors" + flag "github.com/spf13/pflag" + + "github.com/gotenberg/gotenberg/v8/test/integration/scenario" +) + +var opts = godog.Options{ + Format: "pretty", + Paths: []string{"features"}, + Output: colors.Colored(os.Stdout), + Concurrency: runtime.NumCPU(), +} + +func TestMain(m *testing.M) { + repository := flag.String("gotenberg-docker-repository", "", "") + version := flag.String("gotenberg-version", "", "") + platform := flag.String("gotenberg-container-platform", "", "") + flag.Parse() + + if *platform == "" { + switch runtime.GOARCH { + case "arm64": + *platform = "linux/arm64" + default: + *platform = "linux/amd64" + } + } + + scenario.GotenbergDockerRepository = *repository + scenario.GotenbergVersion = *version + scenario.GotenbergContainerPlatform = *platform + + code := godog.TestSuite{ + Name: "integration", + ScenarioInitializer: scenario.InitializeScenario, + Options: &opts, + }.Run() + + os.Exit(code) +} diff --git a/test/integration/scenario/compare.go b/test/integration/scenario/compare.go new file mode 100644 index 00000000..5f61337f --- /dev/null +++ b/test/integration/scenario/compare.go @@ -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 +} diff --git a/test/integration/scenario/containers.go b/test/integration/scenario/containers.go new file mode 100644 index 00000000..1d752ca1 --- /dev/null +++ b/test/integration/scenario/containers.go @@ -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 +} diff --git a/test/integration/scenario/doc.go b/test/integration/scenario/doc.go new file mode 100644 index 00000000..a874ef8f --- /dev/null +++ b/test/integration/scenario/doc.go @@ -0,0 +1,2 @@ +// Package scenario gathers all steps used in the features. +package scenario diff --git a/test/integration/scenario/http.go b/test/integration/scenario/http.go new file mode 100644 index 00000000..7d88d02b --- /dev/null +++ b/test/integration/scenario/http.go @@ -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 +} diff --git a/test/integration/scenario/scenario.go b/test/integration/scenario/scenario.go new file mode 100644 index 00000000..b76e739d --- /dev/null +++ b/test/integration/scenario/scenario.go @@ -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, "") { + return fmt.Errorf("PDF %q is flatten", path) + } + + if !invert && !strings.Contains(output, "") { + 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 + }) +} diff --git a/test/integration/scenario/server.go b/test/integration/scenario/server.go new file mode 100644 index 00000000..ddf2f0a1 --- /dev/null +++ b/test/integration/scenario/server.go @@ -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) +} diff --git a/test/integration/testdata/feature-rich-html-remote/index.html b/test/integration/testdata/feature-rich-html-remote/index.html new file mode 100644 index 00000000..edfb3077 --- /dev/null +++ b/test/integration/testdata/feature-rich-html-remote/index.html @@ -0,0 +1,64 @@ + + + + + + + Feature Rich HTML + + + + +

Emulated media type is 'print'.

+

Emulated media type is 'screen'.

+ + + + + + + + + + + + + + diff --git a/test/integration/testdata/feature-rich-html/index.html b/test/integration/testdata/feature-rich-html/index.html new file mode 100644 index 00000000..f679383e --- /dev/null +++ b/test/integration/testdata/feature-rich-html/index.html @@ -0,0 +1,69 @@ + + + + + + + + + + + Feature Rich HTML + + + + +

Emulated media type is 'print'.

+

Emulated media type is 'screen'.

+ + + + + + + + + + + + + + + diff --git a/test/integration/testdata/feature-rich-markdown/index.html b/test/integration/testdata/feature-rich-markdown/index.html new file mode 100644 index 00000000..cc3fec01 --- /dev/null +++ b/test/integration/testdata/feature-rich-markdown/index.html @@ -0,0 +1,70 @@ + + + + + + + + + + + Feature Rich HTML + + + + {{ toHTML "table.md" }} + +

Emulated media type is 'print'.

+

Emulated media type is 'screen'.

+ + + + + + + + + + + + + + + diff --git a/test/integration/testdata/feature-rich-markdown/table.md b/test/integration/testdata/feature-rich-markdown/table.md new file mode 100644 index 00000000..bf2ab2f7 --- /dev/null +++ b/test/integration/testdata/feature-rich-markdown/table.md @@ -0,0 +1,7 @@ +## This paragraph displays a table from a markdown file + +| Tables | Are | Cool | +| -------- | :-----------: | ----: | +| col 1 is | left-aligned | $1600 | +| col 2 is | centered | $12 | +| col 3 is | right-aligned | $1 | diff --git a/test/integration/testdata/header-footer-html/footer.html b/test/integration/testdata/header-footer-html/footer.html new file mode 100644 index 00000000..41185251 --- /dev/null +++ b/test/integration/testdata/header-footer-html/footer.html @@ -0,0 +1,13 @@ + + + + + +

of

+ + diff --git a/test/integration/testdata/header-footer-html/header.html b/test/integration/testdata/header-footer-html/header.html new file mode 100644 index 00000000..f9501f91 --- /dev/null +++ b/test/integration/testdata/header-footer-html/header.html @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/test/integration/testdata/page-1-html/index.html b/test/integration/testdata/page-1-html/index.html new file mode 100644 index 00000000..9fd123da --- /dev/null +++ b/test/integration/testdata/page-1-html/index.html @@ -0,0 +1,9 @@ + + + + Page 1 + + +

Page 1

+ + diff --git a/test/integration/testdata/page-1-markdown/index.html b/test/integration/testdata/page-1-markdown/index.html new file mode 100644 index 00000000..d2b7442b --- /dev/null +++ b/test/integration/testdata/page-1-markdown/index.html @@ -0,0 +1,9 @@ + + + + Page 1 + + + {{ toHTML "page_1.md" }} + + diff --git a/test/integration/testdata/page-1-markdown/page_1.md b/test/integration/testdata/page-1-markdown/page_1.md new file mode 100644 index 00000000..96080014 --- /dev/null +++ b/test/integration/testdata/page-1-markdown/page_1.md @@ -0,0 +1 @@ +# Page 1 diff --git a/test/integration/testdata/page_1.docx b/test/integration/testdata/page_1.docx new file mode 100644 index 00000000..1868509c Binary files /dev/null and b/test/integration/testdata/page_1.docx differ diff --git a/test/integration/testdata/page_1.pdf b/test/integration/testdata/page_1.pdf new file mode 100644 index 00000000..c41ff06d Binary files /dev/null and b/test/integration/testdata/page_1.pdf differ diff --git a/test/integration/testdata/page_2.docx b/test/integration/testdata/page_2.docx new file mode 100644 index 00000000..4d4b64de Binary files /dev/null and b/test/integration/testdata/page_2.docx differ diff --git a/test/integration/testdata/page_2.pdf b/test/integration/testdata/page_2.pdf new file mode 100644 index 00000000..b8e18570 Binary files /dev/null and b/test/integration/testdata/page_2.pdf differ diff --git a/test/integration/testdata/pages-12-html/index.html b/test/integration/testdata/pages-12-html/index.html new file mode 100644 index 00000000..6db56629 --- /dev/null +++ b/test/integration/testdata/pages-12-html/index.html @@ -0,0 +1,25 @@ + + + + Pages 12 + + + +

Page 1

+

Page 2

+

Page 3

+

Page 4

+

Page 5

+

Page 6

+

Page 7

+

Page 8

+

Page 9

+

Page 10

+

Page 11

+

Page 12

+ + diff --git a/test/integration/testdata/pages-12-markdown/index.html b/test/integration/testdata/pages-12-markdown/index.html new file mode 100644 index 00000000..79b9c6d2 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/index.html @@ -0,0 +1,25 @@ + + + + Pages 12 + + + +
{{ toHTML "page_1.md" }}
+
{{ toHTML "page_2.md" }}
+
{{ toHTML "page_3.md" }}
+
{{ toHTML "page_4.md" }}
+
{{ toHTML "page_5.md" }}
+
{{ toHTML "page_6.md" }}
+
{{ toHTML "page_7.md" }}
+
{{ toHTML "page_8.md" }}
+
{{ toHTML "page_9.md" }}
+
{{ toHTML "page_10.md" }}
+
{{ toHTML "page_11.md" }}
+
{{ toHTML "page_12.md" }}
+ + diff --git a/test/integration/testdata/pages-12-markdown/page_1.md b/test/integration/testdata/pages-12-markdown/page_1.md new file mode 100644 index 00000000..96080014 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_1.md @@ -0,0 +1 @@ +# Page 1 diff --git a/test/integration/testdata/pages-12-markdown/page_10.md b/test/integration/testdata/pages-12-markdown/page_10.md new file mode 100644 index 00000000..50459fd6 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_10.md @@ -0,0 +1 @@ +# Page 10 diff --git a/test/integration/testdata/pages-12-markdown/page_11.md b/test/integration/testdata/pages-12-markdown/page_11.md new file mode 100644 index 00000000..fad63c84 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_11.md @@ -0,0 +1 @@ +# Page 11 diff --git a/test/integration/testdata/pages-12-markdown/page_12.md b/test/integration/testdata/pages-12-markdown/page_12.md new file mode 100644 index 00000000..cfe1ccf8 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_12.md @@ -0,0 +1 @@ +# Page 12 diff --git a/test/integration/testdata/pages-12-markdown/page_2.md b/test/integration/testdata/pages-12-markdown/page_2.md new file mode 100644 index 00000000..f310be33 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_2.md @@ -0,0 +1 @@ +# Page 2 diff --git a/test/integration/testdata/pages-12-markdown/page_3.md b/test/integration/testdata/pages-12-markdown/page_3.md new file mode 100644 index 00000000..294d95c1 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_3.md @@ -0,0 +1 @@ +# Page 3 diff --git a/test/integration/testdata/pages-12-markdown/page_4.md b/test/integration/testdata/pages-12-markdown/page_4.md new file mode 100644 index 00000000..7caf305d --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_4.md @@ -0,0 +1 @@ +# Page 4 diff --git a/test/integration/testdata/pages-12-markdown/page_5.md b/test/integration/testdata/pages-12-markdown/page_5.md new file mode 100644 index 00000000..eb51736e --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_5.md @@ -0,0 +1 @@ +# Page 5 diff --git a/test/integration/testdata/pages-12-markdown/page_6.md b/test/integration/testdata/pages-12-markdown/page_6.md new file mode 100644 index 00000000..74deae7e --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_6.md @@ -0,0 +1 @@ +# Page 6 diff --git a/test/integration/testdata/pages-12-markdown/page_7.md b/test/integration/testdata/pages-12-markdown/page_7.md new file mode 100644 index 00000000..8bc5eeaf --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_7.md @@ -0,0 +1 @@ +# Page 7 diff --git a/test/integration/testdata/pages-12-markdown/page_8.md b/test/integration/testdata/pages-12-markdown/page_8.md new file mode 100644 index 00000000..32ea6ab3 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_8.md @@ -0,0 +1 @@ +# Page 8 diff --git a/test/integration/testdata/pages-12-markdown/page_9.md b/test/integration/testdata/pages-12-markdown/page_9.md new file mode 100644 index 00000000..6288d6f8 --- /dev/null +++ b/test/integration/testdata/pages-12-markdown/page_9.md @@ -0,0 +1 @@ +# Page 9 diff --git a/test/integration/testdata/pages-3-html/index.html b/test/integration/testdata/pages-3-html/index.html new file mode 100644 index 00000000..ab0e1d83 --- /dev/null +++ b/test/integration/testdata/pages-3-html/index.html @@ -0,0 +1,16 @@ + + + + Pages 3 + + + +

Page 1

+

Page 2

+

Page 3

+ + diff --git a/test/integration/testdata/pages-3-markdown/index.html b/test/integration/testdata/pages-3-markdown/index.html new file mode 100644 index 00000000..46ba2fe4 --- /dev/null +++ b/test/integration/testdata/pages-3-markdown/index.html @@ -0,0 +1,16 @@ + + + + Pages 3 + + + +
{{ toHTML "page_1.md" }}
+
{{ toHTML "page_2.md" }}
+
{{ toHTML "page_3.md" }}
+ + diff --git a/test/integration/testdata/pages-3-markdown/page_1.md b/test/integration/testdata/pages-3-markdown/page_1.md new file mode 100644 index 00000000..96080014 --- /dev/null +++ b/test/integration/testdata/pages-3-markdown/page_1.md @@ -0,0 +1 @@ +# Page 1 diff --git a/test/integration/testdata/pages-3-markdown/page_2.md b/test/integration/testdata/pages-3-markdown/page_2.md new file mode 100644 index 00000000..f310be33 --- /dev/null +++ b/test/integration/testdata/pages-3-markdown/page_2.md @@ -0,0 +1 @@ +# Page 2 diff --git a/test/integration/testdata/pages-3-markdown/page_3.md b/test/integration/testdata/pages-3-markdown/page_3.md new file mode 100644 index 00000000..294d95c1 --- /dev/null +++ b/test/integration/testdata/pages-3-markdown/page_3.md @@ -0,0 +1 @@ +# Page 3 diff --git a/test/integration/testdata/pages_12.docx b/test/integration/testdata/pages_12.docx new file mode 100644 index 00000000..6fa6d127 Binary files /dev/null and b/test/integration/testdata/pages_12.docx differ diff --git a/test/integration/testdata/pages_12.pdf b/test/integration/testdata/pages_12.pdf new file mode 100644 index 00000000..7e7459ff Binary files /dev/null and b/test/integration/testdata/pages_12.pdf differ diff --git a/test/integration/testdata/pages_3.docx b/test/integration/testdata/pages_3.docx new file mode 100644 index 00000000..792f530b Binary files /dev/null and b/test/integration/testdata/pages_3.docx differ diff --git a/test/integration/testdata/pages_3.pdf b/test/integration/testdata/pages_3.pdf new file mode 100644 index 00000000..2f5c4c03 Binary files /dev/null and b/test/integration/testdata/pages_3.pdf differ diff --git a/test/testdata/api/README.md b/test/integration/testdata/pem/README.md similarity index 100% rename from test/testdata/api/README.md rename to test/integration/testdata/pem/README.md diff --git a/test/testdata/api/cert.pem b/test/integration/testdata/pem/cert.pem similarity index 100% rename from test/testdata/api/cert.pem rename to test/integration/testdata/pem/cert.pem diff --git a/test/testdata/api/key.pem b/test/integration/testdata/pem/key.pem similarity index 100% rename from test/testdata/api/key.pem rename to test/integration/testdata/pem/key.pem diff --git a/test/integration/testdata/protected_page_1.docx b/test/integration/testdata/protected_page_1.docx new file mode 100644 index 00000000..a7b0deb2 Binary files /dev/null and b/test/integration/testdata/protected_page_1.docx differ diff --git a/test/integration/teststore/.gitkeep b/test/integration/teststore/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/testdata/api/sample1.txt b/test/testdata/api/sample1.txt deleted file mode 100644 index 19102815..00000000 --- a/test/testdata/api/sample1.txt +++ /dev/null @@ -1 +0,0 @@ -foo \ No newline at end of file diff --git a/test/testdata/api/sample2.pdf b/test/testdata/api/sample2.pdf deleted file mode 100644 index 0a0b284f..00000000 Binary files a/test/testdata/api/sample2.pdf and /dev/null differ diff --git a/test/testdata/chromium/html/font.woff b/test/testdata/chromium/html/font.woff deleted file mode 100644 index ebd62d59..00000000 Binary files a/test/testdata/chromium/html/font.woff and /dev/null differ diff --git a/test/testdata/chromium/html/footer.html b/test/testdata/chromium/html/footer.html deleted file mode 100644 index c355f721..00000000 --- a/test/testdata/chromium/html/footer.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - -

- of -

- - \ No newline at end of file diff --git a/test/testdata/chromium/html/header.html b/test/testdata/chromium/html/header.html deleted file mode 100644 index 213ec4fc..00000000 --- a/test/testdata/chromium/html/header.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/test/testdata/chromium/html/img.gif b/test/testdata/chromium/html/img.gif deleted file mode 100644 index 6b066b53..00000000 Binary files a/test/testdata/chromium/html/img.gif and /dev/null differ diff --git a/test/testdata/chromium/html/index.html b/test/testdata/chromium/html/index.html deleted file mode 100644 index a19f166e..00000000 --- a/test/testdata/chromium/html/index.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - Gutenberg - - - -
-
-

Gutenberg

- An image -
- -
-

It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.

- -
-
- -
-

This paragraph uses the default font

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- -

This paragraph uses a Google font

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- -

This paragraph uses a local font

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-
- -
-

This image is loaded from a URL

- -
- -
-

This paragraph appears if wait delay > 2 seconds or if expression window.globalVar === 'ready' returns true

- - -

This paragraph appears if the emulated media type is 'print'

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- -

This paragraph appears if the emulated media type is 'screen'

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-
- -
-

This paragraph appears if JavaScript is NOT disabled

- -
- -
-

/etc/passwd

- - -

\\localhost/etc/passwd

- -
- - - - - - - - - - - - \ No newline at end of file diff --git a/test/testdata/chromium/html/style.css b/test/testdata/chromium/html/style.css deleted file mode 100644 index a32c9a71..00000000 --- a/test/testdata/chromium/html/style.css +++ /dev/null @@ -1,29 +0,0 @@ -body { - font-family: Arial, Helvetica, sans-serif; -} - -.center { - text-align: center; -} - -.google-font { - font-family: 'Montserrat', sans-serif; -} - -@font-face { - font-family: 'Local'; - src: url('font.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -.local-font { - font-family: 'Local' -} - -@media print { - .page-break-after { - page-break-after: always; - } -} - diff --git a/test/testdata/chromium/markdown/defaultfont.md b/test/testdata/chromium/markdown/defaultfont.md deleted file mode 100644 index 14c83035..00000000 --- a/test/testdata/chromium/markdown/defaultfont.md +++ /dev/null @@ -1,3 +0,0 @@ -## This paragraph uses the default font and has been generated from a markdown file - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/chromium/markdown/font.woff b/test/testdata/chromium/markdown/font.woff deleted file mode 100644 index ebd62d59..00000000 Binary files a/test/testdata/chromium/markdown/font.woff and /dev/null differ diff --git a/test/testdata/chromium/markdown/footer.html b/test/testdata/chromium/markdown/footer.html deleted file mode 100644 index c355f721..00000000 --- a/test/testdata/chromium/markdown/footer.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - -

- of -

- - \ No newline at end of file diff --git a/test/testdata/chromium/markdown/googlefont.md b/test/testdata/chromium/markdown/googlefont.md deleted file mode 100644 index d8dd4f23..00000000 --- a/test/testdata/chromium/markdown/googlefont.md +++ /dev/null @@ -1,3 +0,0 @@ -## This paragraph uses a Google font and has been generated from a markdown file - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/chromium/markdown/header.html b/test/testdata/chromium/markdown/header.html deleted file mode 100644 index 213ec4fc..00000000 --- a/test/testdata/chromium/markdown/header.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/test/testdata/chromium/markdown/img.gif b/test/testdata/chromium/markdown/img.gif deleted file mode 100644 index 6b066b53..00000000 Binary files a/test/testdata/chromium/markdown/img.gif and /dev/null differ diff --git a/test/testdata/chromium/markdown/index.html b/test/testdata/chromium/markdown/index.html deleted file mode 100644 index faa522db..00000000 --- a/test/testdata/chromium/markdown/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - Gutenberg - - -
-
-

Gutenberg

- -
- -
-

It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.

- -
-
- -
- {{ toHTML "defaultfont.md" }} - -
- {{ toHTML "googlefont.md" }} -
- -
- {{ toHTML "localfont.md" }} -
-
- -
- {{ toHTML "table.md" }} - -

HTML from previous table

- -
- - \ No newline at end of file diff --git a/test/testdata/chromium/markdown/localfont.md b/test/testdata/chromium/markdown/localfont.md deleted file mode 100644 index c0aac6f3..00000000 --- a/test/testdata/chromium/markdown/localfont.md +++ /dev/null @@ -1,3 +0,0 @@ -## This paragraph uses a local font and has been generated from a markdown file - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/chromium/markdown/style.css b/test/testdata/chromium/markdown/style.css deleted file mode 100644 index e937998f..00000000 --- a/test/testdata/chromium/markdown/style.css +++ /dev/null @@ -1,28 +0,0 @@ -body { - font-family: Arial, Helvetica, sans-serif; -} - -.center { - text-align: center; -} - -.google-font { - font-family: 'Montserrat', sans-serif; -} - -@font-face { - font-family: 'Local'; - src: url('font.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -.local-font { - font-family: 'Local' -} - -@media print { - .page-break-after { - page-break-after: always; - } -} \ No newline at end of file diff --git a/test/testdata/chromium/markdown/table.md b/test/testdata/chromium/markdown/table.md deleted file mode 100644 index 057f69b3..00000000 --- a/test/testdata/chromium/markdown/table.md +++ /dev/null @@ -1,7 +0,0 @@ -## This paragraph displays a table from a markdown file - -| Tables | Are | Cool | -|----------|:-------------:|------:| -| col 1 is | left-aligned | $1600 | -| col 2 is | centered | $12 | -| col 3 is | right-aligned | $1 | \ No newline at end of file diff --git a/test/testdata/libreoffice/document.docx b/test/testdata/libreoffice/document.docx deleted file mode 100644 index e745b3e5..00000000 Binary files a/test/testdata/libreoffice/document.docx and /dev/null differ diff --git a/test/testdata/libreoffice/protected.docx b/test/testdata/libreoffice/protected.docx deleted file mode 100644 index 840d00d5..00000000 Binary files a/test/testdata/libreoffice/protected.docx and /dev/null differ diff --git a/test/testdata/pdfengines/sample1.pdf b/test/testdata/pdfengines/sample1.pdf deleted file mode 100644 index 0a0b284f..00000000 Binary files a/test/testdata/pdfengines/sample1.pdf and /dev/null differ diff --git a/test/testdata/pdfengines/sample2.pdf b/test/testdata/pdfengines/sample2.pdf deleted file mode 100644 index 0a0b284f..00000000 Binary files a/test/testdata/pdfengines/sample2.pdf and /dev/null differ diff --git a/test/testdata/pdfengines/sample3.pdf b/test/testdata/pdfengines/sample3.pdf deleted file mode 100644 index d079b892..00000000 Binary files a/test/testdata/pdfengines/sample3.pdf and /dev/null differ diff --git a/test/testdata/pdfengines/sample4.pdf b/test/testdata/pdfengines/sample4.pdf deleted file mode 100644 index 5fc57f14..00000000 Binary files a/test/testdata/pdfengines/sample4.pdf and /dev/null differ diff --git a/test/testdata/pdfengines/sample5.pdf b/test/testdata/pdfengines/sample5.pdf deleted file mode 100644 index a6185f71..00000000 Binary files a/test/testdata/pdfengines/sample5.pdf and /dev/null differ