diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e2b5c9c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitignore +.docker-cache +.bundle +coverage +pkg +node_modules +vendor/bundle +.env diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e94322f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.jpg binary + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97db50e..3ecd823 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,3 +43,28 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage/coverage.json fail_ci_if_error: true + e2e: + needs: ci + if: > + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + env: + TRANSLOADIT_KEY: ${{ secrets.TRANSLOADIT_KEY }} + TRANSLOADIT_SECRET: ${{ secrets.TRANSLOADIT_SECRET }} + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true + - name: Ensure e2e credentials are configured + if: ${{ env.TRANSLOADIT_KEY == '' || env.TRANSLOADIT_SECRET == '' }} + run: | + echo "TRANSLOADIT_KEY and TRANSLOADIT_SECRET must be configured in repository secrets to run the e2e job." >&2 + exit 1 + - name: Run end-to-end upload test + env: + RUBY_SDK_E2E: 1 + if: ${{ env.TRANSLOADIT_KEY != '' && env.TRANSLOADIT_SECRET != '' }} + run: bundle exec ruby -Itest test/integration/test_e2e_upload.rb diff --git a/.gitignore b/.gitignore index 7397957..8a2cdcc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ transloadit-*.gem env.sh .env vendor/bundle/ +.docker-cache/ +.cache/rubocop_cache/04e06e0faf5ad652d8bcbcfd85bac5f6c32e711e/3031a80880d8a984708138f0d003f77c4bad2648 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a63280..2bbed25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 3.1.1 / 2025-10-28 + +- Add optional live end-to-end upload harness and CI job for parity verification, defaulted in Docker tests (kvz) +- Restore missing `require "uri"` to prevent `NameError` when loading `Transloadit::Request` (kvz) + ### 3.1.0 / 2024-11-24 - Add Smart CDN signature support via `signed_smart_cdn_url` method (kvz) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..29f8ae5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contributing + +Thanks for helping improve the Transloadit Ruby SDK! This guide covers local development, testing, and publishing new releases. + +## Local Development + +After cloning the repository, install dependencies and run the test suite: + +```bash +bundle install +bundle exec rake test +``` + +To exercise the signature parity suite against the Node.js CLI, make sure `npx transloadit` is available and run: + +```bash +TEST_NODE_PARITY=1 bundle exec rake test +``` + +You can warm the CLI cache ahead of time: + +```bash +TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \ + npx --yes transloadit smart_sig --help +TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \ + npx --yes transloadit sig --algorithm sha384 --help +``` + +Set `COVERAGE=0` to skip coverage instrumentation if desired: + +```bash +COVERAGE=0 bundle exec rake test +``` + +## Docker Workflow + +The repository ships with a helper that runs tests inside a reproducible Docker image: + +```bash +./scripts/test-in-docker.sh +``` + +Pass a custom command to run alternatives (Bundler still installs first): + +```bash +./scripts/test-in-docker.sh bundle exec ruby -Itest test/unit/transloadit/test_request.rb +``` + +The script forwards environment variables such as `TEST_NODE_PARITY` and credentials from `.env`, so you can combine parity checks and integration tests. End-to-end uploads are enabled by default; unset them by running: + +```bash +RUBY_SDK_E2E=0 ./scripts/test-in-docker.sh +``` + +## Live End-to-End Test + +To exercise the optional live upload: + +```bash +RUBY_SDK_E2E=1 TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \ + ./scripts/test-in-docker.sh bundle exec ruby -Itest test/integration/test_e2e_upload.rb +``` + +The test uploads `chameleon.jpg`, resizes it, and asserts on a real assembly response. + +## Releasing to RubyGems + +1. Update the version and changelog: + - Bump `lib/transloadit/version.rb`. + - Add a corresponding entry to `CHANGELOG.md`. +2. Run the full test suite (including Docker, parity, and e2e checks as needed). +3. Commit the release changes and tag: + ```bash + git commit -am "Release X.Y.Z" + git tag -a vX.Y.Z -m "Release X.Y.Z" + ``` +4. Push the commit and tag: + ```bash + git push origin main + git push origin vX.Y.Z + ``` +5. Publish the gem using the helper script: + ```bash + GEM_HOST_API_KEY=... ./scripts/notify-registry.sh + ``` +6. Draft a GitHub release from the new tag and publish the generated notes. + +### RubyGems Credentials + +- You must belong to the `transloadit` organization on RubyGems with permission to push the `transloadit` gem. +- Generate an API key with **Push Rubygems** permissions at . Copy the token and keep it secure. +- Export the token as `GEM_HOST_API_KEY` in your environment before running `./scripts/notify-registry.sh`. The script refuses to run if the variable is missing. + +That’s it! Thank you for contributing. + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f85b91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 + +FROM ruby:3.3 AS base + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js for parity checks +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && npm install -g npm@latest \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace diff --git a/README.md b/README.md index 141438c..400ffb7 100644 --- a/README.md +++ b/README.md @@ -467,34 +467,4 @@ If you still need support for Ruby 2.x, 2.0.1 is the last version that supports ## Contributing -Contributions are welcome! - -### Running tests - -```bash -bundle install -bundle exec rake test -``` - -To also test parity against the Node.js reference implementation, run: - -```bash -TEST_NODE_PARITY=1 bundle exec rake test -``` - -To disable coverage reporting, run: - -```bash -COVERAGE=0 bundle exec rake test -``` - -### Releasing on RubyGems - -Let's say you wanted to release version `3.1.0`, here are the steps: - -1. Update the version number in the version file `version.rb` and `CHANGELOG.md` -2. Commit: `git add CHANGELOG.md lib/transloadit/version.rb && git commit -m "Release 3.1.0"` -3. Create a git tag: `git tag -a v3.1.0 -m "Release 3.1.0"` -4. Push the git tag: `git push origin v3.1.0` -5. Release on RubyGems: `gem build transloadit.gemspec && gem push transloadit-3.1.0.gem` -6. Draft a release [here](https://github.com/transloadit/ruby-sdk/releases). Click the `v3.1.0` tag and click `Generate release notes`. Inspect and Publish. +See [CONTRIBUTING.md](CONTRIBUTING.md) for local development, testing, and release instructions. diff --git a/chameleon.jpg b/chameleon.jpg new file mode 100644 index 0000000..ea5dcc0 Binary files /dev/null and b/chameleon.jpg differ diff --git a/lib/transloadit.rb b/lib/transloadit.rb index 9cda61d..9f5c9d3 100644 --- a/lib/transloadit.rb +++ b/lib/transloadit.rb @@ -2,8 +2,8 @@ require "date" require "json" require "openssl" -require "uri" require "cgi" +require "uri" # # Implements the Transloadit REST API in Ruby. Check the {file:README.md README} diff --git a/lib/transloadit/response.rb b/lib/transloadit/response.rb index 35641ae..82279bd 100644 --- a/lib/transloadit/response.rb +++ b/lib/transloadit/response.rb @@ -52,6 +52,7 @@ def inspect # def extend!(mod) extend(mod) + self end diff --git a/lib/transloadit/version.rb b/lib/transloadit/version.rb index 61c4e95..1df53e6 100644 --- a/lib/transloadit/version.rb +++ b/lib/transloadit/version.rb @@ -1,3 +1,3 @@ class Transloadit - VERSION = "3.1.0" + VERSION = "3.1.1" end diff --git a/scripts/notify-registry.sh b/scripts/notify-registry.sh new file mode 100755 index 0000000..ffdeed2 --- /dev/null +++ b/scripts/notify-registry.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME=${IMAGE_NAME:-transloadit-ruby-sdk-dev} + +err() { + echo "notify-registry: $*" >&2 +} + +if ! command -v docker >/dev/null 2>&1; then + err "Docker is required to publish the gem." + exit 1 +fi + +if [[ -z "${GEM_HOST_API_KEY:-}" ]]; then + err "GEM_HOST_API_KEY environment variable is not set. Generate a RubyGems API key with push permissions and export it before running this script." + exit 1 +fi + +if ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then + err "Docker image '$IMAGE_NAME' not found. Building it now..." + docker build -t "$IMAGE_NAME" -f Dockerfile . +fi + +version=$( + docker run --rm \ + -v "$PWD":/workspace \ + -w /workspace \ + "$IMAGE_NAME" \ + ruby -Ilib -e 'require "transloadit/version"; puts Transloadit::VERSION' +) + +gem_file="transloadit-${version}.gem" + +err "Building ${gem_file}..." +docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e HOME=/workspace \ + -v "$PWD":/workspace \ + -w /workspace \ + "$IMAGE_NAME" \ + bash -lc "set -euo pipefail; rm -f ${gem_file}; gem build transloadit.gemspec >/dev/null" + +err "Pushing ${gem_file} to RubyGems..." +docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e HOME=/workspace \ + -e GEM_HOST_API_KEY="$GEM_HOST_API_KEY" \ + -v "$PWD":/workspace \ + -w /workspace \ + "$IMAGE_NAME" \ + bash -lc "set -euo pipefail; gem push ${gem_file}" + +err "Removing local ${gem_file}..." +rm -f "${gem_file}" + +echo "notify-registry: Successfully pushed ${gem_file} to RubyGems." diff --git a/scripts/test-in-docker.sh b/scripts/test-in-docker.sh new file mode 100755 index 0000000..7a09a78 --- /dev/null +++ b/scripts/test-in-docker.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_NAME=${IMAGE_NAME:-transloadit-ruby-sdk-dev} +CACHE_DIR=.docker-cache + +ensure_docker() { + if ! command -v docker >/dev/null 2>&1; then + echo "Docker is required to run this script." >&2 + exit 1 + fi + + if ! docker info >/dev/null 2>&1; then + if [[ -z "${DOCKER_HOST:-}" && -S "$HOME/.colima/default/docker.sock" ]]; then + export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" + fi + fi + + if ! docker info >/dev/null 2>&1; then + echo "Docker daemon is not reachable. Start Docker (or Colima) and retry." >&2 + exit 1 + fi +} + +configure_platform() { + if [[ -z "${DOCKER_PLATFORM:-}" ]]; then + local arch + arch=$(uname -m) + if [[ "$arch" == "arm64" || "$arch" == "aarch64" ]]; then + DOCKER_PLATFORM=linux/amd64 + fi + fi +} + +ensure_docker +configure_platform + +if [[ $# -eq 0 ]]; then + RUN_CMD='set -e; bundle install --jobs 4 --retry 3; bundle exec rake test' +else + printf -v USER_CMD '%q ' "$@" + RUN_CMD="set -e; bundle install --jobs 4 --retry 3; ${USER_CMD}" +fi + +mkdir -p "$CACHE_DIR/bundle" "$CACHE_DIR/npm-cache" + +BUILD_ARGS=() +if [[ -n "${DOCKER_PLATFORM:-}" ]]; then + BUILD_ARGS+=(--platform "$DOCKER_PLATFORM") +fi +BUILD_ARGS+=(-t "$IMAGE_NAME" -f Dockerfile .) + +docker build "${BUILD_ARGS[@]}" + +DOCKER_ARGS=( + --rm + --user "$(id -u):$(id -g)" + -e HOME=/workspace + -e BUNDLE_PATH=/workspace/$CACHE_DIR/bundle + -e BUNDLE_APP_CONFIG=/workspace/$CACHE_DIR/bundle-config + -e BUNDLE_CACHE_PATH=/workspace/$CACHE_DIR/bundle-cache + -e npm_config_cache=/workspace/$CACHE_DIR/npm-cache + -e TEST_NODE_PARITY="${TEST_NODE_PARITY:-0}" + -e RUBY_SDK_E2E="${RUBY_SDK_E2E:-1}" + -v "$PWD":/workspace + -w /workspace +) + +if [[ -n "${DOCKER_PLATFORM:-}" ]]; then + DOCKER_ARGS+=(--platform "$DOCKER_PLATFORM") +fi + +if [[ -f .env ]]; then + DOCKER_ARGS+=(--env-file "$PWD/.env") +fi + +PASSTHROUGH_ENV_VARS=( + TRANSLOADIT_KEY + TRANSLOADIT_SECRET + TRANSLOADIT_TEMPLATE_ID +) + +for var in "${PASSTHROUGH_ENV_VARS[@]}"; do + if [[ -n "${!var:-}" ]]; then + DOCKER_ARGS+=(-e "$var=${!var}") + fi +done + +exec docker run "${DOCKER_ARGS[@]}" "$IMAGE_NAME" bash -lc "$RUN_CMD" diff --git a/test/integration/test_e2e_upload.rb b/test/integration/test_e2e_upload.rb new file mode 100644 index 0000000..9a2b51f --- /dev/null +++ b/test/integration/test_e2e_upload.rb @@ -0,0 +1,97 @@ +require "test_helper" +require "webmock" + +describe "Transloadit end-to-end upload" do + before do + skip "Set RUBY_SDK_E2E=1 to run live upload tests" unless e2e_enabled? + + @key = ENV["TRANSLOADIT_KEY"] + @secret = ENV["TRANSLOADIT_SECRET"] + skip "TRANSLOADIT_KEY and TRANSLOADIT_SECRET must be set to run live upload tests" if blank?(@key) || blank?(@secret) + + @fixture_path = File.expand_path("../../chameleon.jpg", __dir__) + skip "chameleon.jpg fixture missing; run tests from the repository root" unless File.file?(@fixture_path) + end + + it "uploads and processes the chameleon image" do + with_live_http do + transloadit = Transloadit.new(key: @key, secret: @secret) + + resize_step = transloadit.step( + "resize", + "/image/resize", + use: ":original", + width: 128, + height: 128, + resize_strategy: "fit", + format: "png" + ) + + response = File.open(@fixture_path, "rb") do |upload| + transloadit.assembly.create!( + upload, + wait: true, + steps: resize_step + ) + end + + response.reload_until_finished!(tries: 120) unless response.finished? + + _(response.completed?).must_equal true, "Assembly did not complete successfully: #{response.body.inspect}" + + uploads = response["uploads"] || [] + refute_empty uploads, "Expected uploads in the assembly response" + + upload_info = uploads.first + basename = upload_info["basename"] + _(basename).must_equal File.basename(@fixture_path, ".*") if basename + + filename = upload_info["name"] + _(filename).must_equal File.basename(@fixture_path) if filename + + results = (response["results"] || {})["resize"] || [] + refute_empty results, "Expected resize results in assembly response" + + first_result = results.first + ssl_url = first_result["ssl_url"] + refute_nil ssl_url, "Missing ssl_url in resize result: #{first_result.inspect}" + _(ssl_url).must_match(/\Ahttps:\/\//) + + meta = first_result["meta"] || {} + width = integer_if_present(meta["width"]) + height = integer_if_present(meta["height"]) + refute_nil width, "Missing width metadata: #{meta.inspect}" + refute_nil height, "Missing height metadata: #{meta.inspect}" + assert width.positive? && width <= 128, "Unexpected width #{width.inspect}" + assert height.positive? && height <= 128, "Unexpected height #{height.inspect}" + end + end + + private + + def with_live_http + VCR.turn_off!(ignore_cassettes: true) + WebMock.allow_net_connect! + yield + ensure + WebMock.disable_net_connect!(allow_localhost: true) + VCR.turn_on! + end + + def e2e_enabled? + flag = ENV["RUBY_SDK_E2E"] + return false if blank?(flag) + + %w[1 true yes on].include?(flag.to_s.strip.downcase) + end + + def integer_if_present(value) + return nil if blank?(value) + + value.to_i + end + + def blank?(value) + value.respond_to?(:empty?) ? value.empty? : !value + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 78e5bfa..ed79841 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -15,6 +15,8 @@ require "minitest/autorun" require "transloadit" require "vcr" +require "open3" +require "json" VCR.configure do |c| c.cassette_library_dir = "test/fixtures/cassettes" @@ -25,3 +27,48 @@ def values_from_post_body(body) Addressable::URI.parse("?" + CGI.unescape(body)).query_values end + +module TransloaditCliHelpers + TRANSLOADIT_CLI_PACKAGE = ENV.fetch("TRANSLOADIT_CLI_PACKAGE", "transloadit@4.0.5") + + def run_transloadit_cli(command, payload, key:, secret:, algorithm: nil) + return nil unless ENV["TEST_NODE_PARITY"] == "1" + + env = { + "TRANSLOADIT_KEY" => key, + "TRANSLOADIT_SECRET" => secret, + "TRANSLOADIT_AUTH_KEY" => key, + "TRANSLOADIT_AUTH_SECRET" => secret + } + + args = [ + "npm", "exec", "--yes", "--package", TRANSLOADIT_CLI_PACKAGE, "--", + "transloadit", command + ] + args += ["--algorithm", algorithm] if algorithm + + stdout, stderr, status = Open3.capture3(env, *args, stdin_data: JSON.dump(payload)) + raise "transloadit CLI #{command} failed: #{stderr}" unless status.success? + + stdout.strip + end + + def run_transloadit_smart_sig(payload, key:, secret:) + cli_payload = { + workspace: payload.fetch(:workspace), + template: payload.fetch(:template), + input: payload.fetch(:input) + } + cli_payload[:url_params] = payload[:url_params] if payload.key?(:url_params) + cli_payload[:expire_at_ms] = payload[:expire_at_ms] if payload.key?(:expire_at_ms) + + run_transloadit_cli("smart_sig", cli_payload, key: key, secret: secret) + end + + def run_transloadit_sig(payload, key:, secret:, algorithm: nil) + output = run_transloadit_cli("sig", payload, key: key, secret: secret, algorithm: algorithm) + output && JSON.parse(output) + end +end + +Minitest::Test.include(TransloaditCliHelpers) diff --git a/test/unit/test_transloadit.rb b/test/unit/test_transloadit.rb index f55f068..867beeb 100644 --- a/test/unit/test_transloadit.rb +++ b/test/unit/test_transloadit.rb @@ -108,7 +108,10 @@ end it "must produce Transloadit-compatible JSON output" do - _(@transloadit.to_json).must_equal MultiJson.dump(@transloadit.to_hash) + fixed_time = Time.utc(2025, 10, 28, 0, 0, 0) + Time.stub :now, fixed_time do + _(@transloadit.to_json).must_equal MultiJson.dump(@transloadit.to_hash) + end end end diff --git a/test/unit/transloadit/node-smartcdn-sig.ts b/test/unit/transloadit/node-smartcdn-sig.ts deleted file mode 100644 index 14c9461..0000000 --- a/test/unit/transloadit/node-smartcdn-sig.ts +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env tsx -// Reference Smart CDN (https://transloadit.com/services/content-delivery/) Signature implementation -// And CLI tester to see if our SDK's implementation -// matches Node's - -/// - -import { createHash, createHmac } from 'crypto' - -interface SmartCDNParams { - workspace: string - template: string - input: string - expire_at_ms?: number - auth_key?: string - auth_secret?: string - url_params?: Record -} - -function signSmartCDNUrl(params: SmartCDNParams): string { - const { - workspace, - template, - input, - expire_at_ms, - auth_key = 'my-key', - auth_secret = 'my-secret', - url_params = {}, - } = params - - if (!workspace) throw new Error('workspace is required') - if (!template) throw new Error('template is required') - if (input === null || input === undefined) - throw new Error('input must be a string') - - const workspaceSlug = encodeURIComponent(workspace) - const templateSlug = encodeURIComponent(template) - const inputField = encodeURIComponent(input) - - const expireAt = expire_at_ms ?? Date.now() + 60 * 60 * 1000 // 1 hour default - - const queryParams: Record = {} - - // Handle url_params - Object.entries(url_params).forEach(([key, value]) => { - if (value === null || value === undefined) return - if (Array.isArray(value)) { - value.forEach((val) => { - if (val === null || val === undefined) return - ;(queryParams[key] ||= []).push(String(val)) - }) - } else { - queryParams[key] = [String(value)] - } - }) - - queryParams.auth_key = [auth_key] - queryParams.exp = [String(expireAt)] - - // Sort parameters to ensure consistent ordering - const sortedParams = Object.entries(queryParams) - .sort() - .map(([key, values]) => - values.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`) - ) - .flat() - .join('&') - - const stringToSign = `${workspaceSlug}/${templateSlug}/${inputField}?${sortedParams}` - const signature = createHmac('sha256', auth_secret) - .update(stringToSign) - .digest('hex') - - const finalParams = `${sortedParams}&sig=${encodeURIComponent( - `sha256:${signature}` - )}` - return `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${finalParams}` -} - -// Read JSON from stdin -let jsonInput = '' -process.stdin.on('data', (chunk) => { - jsonInput += chunk -}) - -process.stdin.on('end', () => { - const params = JSON.parse(jsonInput) - console.log(signSmartCDNUrl(params)) -}) diff --git a/test/unit/transloadit/test_request.rb b/test/unit/transloadit/test_request.rb index 572f4d0..8435eb3 100644 --- a/test/unit/transloadit/test_request.rb +++ b/test/unit/transloadit/test_request.rb @@ -1,4 +1,7 @@ require "test_helper" +require "multi_json" +require "rbconfig" +require "tmpdir" describe Transloadit::Request do it "must allow initialization" do @@ -75,4 +78,86 @@ end end end + + it "loads request when URI was not previously required" do + lib_path = File.expand_path("../../../lib", __dir__) + + Dir.mktmpdir do |stub_dir| + File.write(File.join(stub_dir, "rest-client.rb"), <<~RUBY) + module RestClient + class Response; end + + class Resource + def initialize(*); end + def [](*); self; end + def get(*); Response.new; end + def post(*); Response.new; end + def put(*); Response.new; end + def delete(*); Response.new; end + end + + module Exceptions + class OpenTimeout < StandardError; end + end + end + RUBY + + File.write(File.join(stub_dir, "multi_json.rb"), <<~RUBY) + require "json" + + module MultiJson + def self.dump(value) + JSON.dump(value) + end + + def self.load(json) + JSON.parse(json) + end + end + RUBY + + script = <<~RUBY + $LOAD_PATH.unshift #{stub_dir.inspect} + $LOAD_PATH.unshift #{lib_path.inspect} + + begin + require "transloadit/request" + Transloadit::Request.new("/") + rescue StandardError => e + warn e.full_message + exit 1 + end + RUBY + + stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-e", script) + error_output = stderr.empty? ? stdout : stderr + assert status.success?, "Expected transloadit/request to load without NameError, got: #{error_output}" + end + end +end + +describe "signature parity" do + it "matches transloadit CLI sig output" do + skip "Parity testing not enabled" unless ENV["TEST_NODE_PARITY"] == "1" + + expires = "2025-01-02T00:00:00.000Z" + params = { + auth: {key: "cli-key", expires: expires}, + steps: {encode: {robot: "/video/encode"}} + } + + cli_result = run_transloadit_sig(params, key: "cli-key", secret: "cli-secret", algorithm: "sha384") + refute_nil cli_result + + cli_params_json = cli_result["params"] + request = Transloadit::Request.new("/", "cli-secret") + ruby_signature = request.send(:signature, cli_params_json) + + assert_equal cli_result["signature"], ruby_signature + + cli_params = JSON.parse(cli_params_json) + assert_equal "cli-key", cli_params.dig("auth", "key") + assert_equal expires, cli_params.dig("auth", "expires") + assert_equal "/video/encode", cli_params.dig("steps", "encode", "robot") + end end diff --git a/test/unit/transloadit/test_smart_cdn.rb b/test/unit/transloadit/test_smart_cdn.rb index 586eaa7..332ffff 100644 --- a/test/unit/transloadit/test_smart_cdn.rb +++ b/test/unit/transloadit/test_smart_cdn.rb @@ -1,6 +1,4 @@ require "test_helper" -require "json" -require "open3" describe Transloadit do before do @@ -13,15 +11,6 @@ @expire_at = 1732550672867 end - def run_node_script(params) - return unless ENV["TEST_NODE_PARITY"] == "1" - script_path = File.expand_path("./node-smartcdn-sig", __dir__) - json_input = JSON.dump(params) - stdout, stderr, status = Open3.capture3("tsx #{script_path}", stdin_data: json_input) - raise "Node script failed: #{stderr}" unless status.success? - stdout.strip - end - describe "#signed_smart_cdn_url" do it "requires workspace" do assert_raises ArgumentError, "workspace is required" do @@ -81,8 +70,8 @@ def run_node_script(params) url = @transloadit.signed_smart_cdn_url(**params) assert_equal expected_url, url - if (node_url = run_node_script(params.merge(auth_key: "my-key", auth_secret: "my-secret"))) - assert_equal expected_url, node_url + if (cli_url = run_transloadit_smart_sig(params, key: @auth_key, secret: @auth_secret)) + assert_equal expected_url, cli_url end end @@ -98,8 +87,8 @@ def run_node_script(params) url = @transloadit.signed_smart_cdn_url(**params) assert_equal expected_url, url - if (node_url = run_node_script(params.merge(auth_key: "my-key", auth_secret: "my-secret"))) - assert_equal expected_url, node_url + if (cli_url = run_transloadit_smart_sig(params, key: @auth_key, secret: @auth_secret)) + assert_equal expected_url, cli_url end end @@ -119,8 +108,8 @@ def run_node_script(params) url = @transloadit.signed_smart_cdn_url(**params) assert_equal expected_url, url - if (node_url = run_node_script(params.merge(auth_key: "my-key", auth_secret: "my-secret"))) - assert_equal expected_url, node_url + if (cli_url = run_transloadit_smart_sig(params, key: @auth_key, secret: @auth_secret)) + assert_equal expected_url, cli_url end end @@ -140,8 +129,8 @@ def run_node_script(params) url = @transloadit.signed_smart_cdn_url(**params) assert_equal expected_url, url - if (node_url = run_node_script(params.merge(auth_key: "my-key", auth_secret: "my-secret"))) - assert_equal expected_url, node_url + if (cli_url = run_transloadit_smart_sig(params, key: @auth_key, secret: @auth_secret)) + assert_equal expected_url, cli_url end end @@ -161,8 +150,8 @@ def run_node_script(params) url = @transloadit.signed_smart_cdn_url(**params) assert_equal expected_url, url - if (node_url = run_node_script(params.merge(auth_key: "my-key", auth_secret: "my-secret"))) - assert_equal expected_url, node_url + if (cli_url = run_transloadit_smart_sig(params, key: @auth_key, secret: @auth_secret)) + assert_equal expected_url, cli_url end end end