diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df4591b..71b0d53 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,9 +1,12 @@
name: CI
on:
push:
- branches: [ main ]
+ branches:
+ - main
pull_request:
- branches: [ main ]
+ types:
+ - opened
+ - synchronize
jobs:
ci:
runs-on: ubuntu-latest
@@ -13,15 +16,19 @@ jobs:
- 3.0
- 3.1
- 3.2
+ - 3.3
- jruby
- # - jruby-head
- truffleruby
- # - truffleruby-head
+ fail-fast: false
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ - run: npm install -g tsx
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec standardrb
- - run: COVERAGE=false bundle exec rake test
+ - run: COVERAGE=0 TEST_NODE_PARITY=1 bundle exec rake test
diff --git a/.gitignore b/.gitignore
index ce5d33d..7397957 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,5 @@ transloadit-*.gem
.DS_Store
env.sh
-
-# Bundle directory
+.env
vendor/bundle/
diff --git a/.vscode/ruby-sdk.code-workspace b/.vscode/ruby-sdk.code-workspace
new file mode 100644
index 0000000..ee9fe3c
--- /dev/null
+++ b/.vscode/ruby-sdk.code-workspace
@@ -0,0 +1,13 @@
+{
+ "folders": [
+ {
+ "path": ".."
+ }
+ ],
+ "settings": {
+ "workbench.colorCustomizations": {
+ "titleBar.activeForeground": "#ffffff",
+ "titleBar.activeBackground": "#cc0000"
+ },
+ }
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..03f7d67
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+.PHONY: all test fix
+
+all: fix test
+
+# Run tests
+test:
+ bundle exec rake test
+
+# Fix code formatting
+fix:
+ bundle exec standardrb --fix
+
+# Install dependencies
+install:
+ bundle install
+
+# Run both fix and test
+check: fix test
diff --git a/README.md b/README.md
index b5ec516..09f0742 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ A **Ruby** Integration for [Transloadit](https://transloadit.com)'s file uploadi
This is a **Ruby** SDK to make it easy to talk to the [Transloadit](https://transloadit.com) REST API.
-*If you run Ruby on Rails and are looking to integrate with the browser for file uploads, checkout the [rails-sdk](https://github.com/transloadit/rails-sdk).*
+_If you run Ruby on Rails and are looking to integrate with the browser for file uploads, checkout the [rails-sdk](https://github.com/transloadit/rails-sdk)._
## Install
@@ -29,14 +29,14 @@ $ irb -rubygems
=> true
```
-Then create a Transloadit instance, which will maintain your
-[authentication credentials](https://transloadit.com/accounts/credentials)
+Then create a Transloadit instance, which will maintain your
+[authentication credentials](https://transloadit.com/accounts/credentials)
and allow us to make requests to [the API](https://transloadit.com/docs/api/).
```ruby
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
```
@@ -49,8 +49,8 @@ and store the result on [Amazon S3](https://aws.amazon.com/s3/).
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
# First, we create two steps: one to resize the image to 320x240, and another to
@@ -60,9 +60,9 @@ resize = transloadit.step 'resize', '/image/resize',
:height => 240
store = transloadit.step 'store', '/s3/store',
- :key => 'YOUR_AWS_KEY',
- :secret => 'YOUR_AWS_SECRET',
- :bucket => 'YOUR_S3_BUCKET'
+ :key => 'MY_AWS_KEY',
+ :secret => 'MY_AWS_SECRET',
+ :bucket => 'MY_S3_BUCKET'
# Now that we have the steps, we create an assembly (which is just a request to
# process a file or set of files) and let Transloadit do the rest.
@@ -125,7 +125,7 @@ API at the time the Assembly was created. You have to explicitly ask
# reloads the response's contents from the REST API
response.reload!
-# reloads once per second until all processing is finished, up to number of
+# reloads once per second until all processing is finished, up to number of
# times specified in :tries option, otherwise will raise ReloadLimitReached
response.reload_until_finished! tries: 300 # default is 600
```
@@ -147,8 +147,8 @@ than one file in the same request. You can also pass a single Step fo
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
assembly = transloadit.assembly(steps: store)
@@ -160,7 +160,7 @@ response = assembly.create!(
)
```
-You can also pass an array of files to the `create!` method.
+You can also pass an array of files to the `create!` method.
Just unpack the array using the splat `*` operator.
```ruby
@@ -178,8 +178,8 @@ simply need to `use` other Steps. Following
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
encode = transloadit.step 'encode', '/video/encode', { ... }
@@ -208,17 +208,17 @@ for recurring encoding tasks. In order to use these do the following:
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
transloadit.assembly(
- :template_id => 'YOUR_TEMPLATE_ID'
+ :template_id => 'MY_TEMPLATE_ID'
).create! open('/PATH/TO/FILE.mpg')
```
You can use your steps together with this template and even use variables.
-The [Transloadit documentation](https://transloadit.com/docs/#passing-variables-into-a-template)
+The [Transloadit documentation](https://transloadit.com/docs/#passing-variables-into-a-template)
has some nice examples for that.
### 5. Using fields
@@ -231,8 +231,8 @@ to the upload itself. You can use fields like the following:
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
transloadit.assembly(
@@ -252,8 +252,8 @@ a Notify URL for the Assembly.
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
transloadit.assembly(
@@ -271,8 +271,8 @@ Transloadit also provides methods to retrieve/replay Assemblies and t
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
assembly = transloadit.assembly
@@ -281,10 +281,10 @@ assembly = transloadit.assembly
assembly.list
# returns a specific assembly
-assembly.get 'YOUR_ASSEMBLY_ID'
+assembly.get 'MY_ASSEMBLY_ID'
# replays a specific assembly
-response = assembly.replay 'YOUR_ASSEMBLY_ID'
+response = assembly.replay 'MY_ASSEMBLY_ID'
# should return true if assembly is replaying and false otherwise.
response.replaying?
@@ -292,7 +292,7 @@ response.replaying?
assembly.get_notifications
# replays an assembly notification
-assembly.replay_notification 'YOUR_ASSEMBLY_ID'
+assembly.replay_notification 'MY_ASSEMBLY_ID'
```
### 8. Templates
@@ -304,8 +304,8 @@ for recurring encoding tasks. Here's how you would create a Template:
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
template = transloadit.template
@@ -331,8 +331,8 @@ There are also some other methods to retrieve, update and delete a Template
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
template = transloadit.template
@@ -341,11 +341,11 @@ template = transloadit.template
template.list
# returns a specific template.
-template.get 'YOUR_TEMPLATE_ID'
+template.get 'MY_TEMPLATE_ID'
# updates the template whose id is specified.
template.update(
- 'YOUR_TEMPLATE_ID',
+ 'MY_TEMPLATE_ID',
:name => 'CHANGED_TEMPLATE_NAME',
:template => {
:steps => {
@@ -358,7 +358,7 @@ template.update(
)
# deletes a specific template
-template.delete 'YOUR_TEMPLATE_ID'
+template.delete 'MY_TEMPLATE_ID'
```
### 9. Getting Bill reports
@@ -370,8 +370,8 @@ you can use the `bill` method passing the required month and year like the follo
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
# returns bill report for February, 2016.
@@ -380,7 +380,48 @@ transloadit.bill(2, 2016)
Not specifying the `month` or `year` would default to the current month or year.
-### 10. Rate limits
+### 10. Signing Smart CDN URLs
+
+You can generate signed [Smart CDN](https://transloadit.com/services/content-delivery/) URLs using your Transloadit instance:
+
+```ruby
+require 'transloadit'
+
+transloadit = Transloadit.new(
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
+)
+
+# Generate a signed URL using instance credentials
+url = transloadit.signed_smart_cdn_url(
+ workspace: "MY_WORKSPACE",
+ template: "MY_TEMPLATE",
+ input: "avatars/jane.jpg"
+)
+
+# Add URL parameters
+url = transloadit.signed_smart_cdn_url(
+ workspace: "MY_WORKSPACE",
+ template: "MY_TEMPLATE",
+ input: "avatars/jane.jpg",
+ url_params: {
+ width: 100,
+ height: 200
+ }
+)
+
+# Set expiration time
+url = transloadit.signed_smart_cdn_url(
+ workspace: "MY_WORKSPACE",
+ template: "MY_TEMPLATE",
+ input: "avatars/jane.jpg",
+ expire_at_ms: 1732550672867 # Specific timestamp
+)
+```
+
+The generated URL will be signed using your Transloadit credentials and can be used to access files through the Smart CDN in a secure manner.
+
+### 11. Rate limits
Transloadit enforces rate limits to guarantee that no customers are adversely affected by the usage
of any given customer. See [Rate Limiting](https://transloadit.com/docs/api/#rate-limiting).
@@ -393,8 +434,8 @@ To change the number of attempts that will be made when creating an Assembl
require 'transloadit'
transloadit = Transloadit.new(
- :key => 'YOUR_TRANSLOADIT_KEY',
- :secret => 'YOUR_TRANSLOADIT_SECRET'
+ :key => 'MY_TRANSLOADIT_KEY',
+ :secret => 'MY_TRANSLOADIT_SECRET'
)
# would make one extra attempt after a failed attempt.
@@ -423,3 +464,24 @@ Please see [ci.yml](https://github.com/transloadit/ruby-sdk/tree/main/.github/wo
### Ruby 2.x
If you still need support for Ruby 2.x, 2.0.1 is the last version that supports it.
+
+## Contributing
+
+### 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
+```
diff --git a/lib/transloadit.rb b/lib/transloadit.rb
index d2a2869..9cda61d 100644
--- a/lib/transloadit.rb
+++ b/lib/transloadit.rb
@@ -1,5 +1,9 @@
require "multi_json"
require "date"
+require "json"
+require "openssl"
+require "uri"
+require "cgi"
#
# Implements the Transloadit REST API in Ruby. Check the {file:README.md README}
@@ -11,6 +15,7 @@ class Transloadit
autoload :Exception, "transloadit/exception"
autoload :Request, "transloadit/request"
autoload :Response, "transloadit/response"
+ autoload :SmartCDN, "transloadit/smart_cdn"
autoload :Step, "transloadit/step"
autoload :Template, "transloadit/template"
autoload :VERSION, "transloadit/version"
@@ -130,6 +135,54 @@ def to_json
MultiJson.dump(to_hash)
end
+ # @param workspace [String] Workspace slug
+ # @param template [String] Template slug or template ID
+ # @param input [String] Input value that is provided as `${fields.input}` in the template
+ # @param url_params [Hash] Additional parameters for the URL query string (optional)
+ # @param expire_at_ms [Integer] Expiration time as Unix timestamp in milliseconds (optional)
+ # @return [String] Signed Smart CDN URL
+ def signed_smart_cdn_url(
+ workspace:,
+ template:,
+ input:,
+ expire_at_ms: nil,
+ url_params: {}
+ )
+ raise ArgumentError, "workspace is required" if workspace.nil? || workspace.empty?
+ raise ArgumentError, "template is required" if template.nil? || template.empty?
+ raise ArgumentError, "input is required" if input.nil?
+
+ workspace_slug = CGI.escape(workspace)
+ template_slug = CGI.escape(template)
+ input_field = CGI.escape(input)
+
+ expire_at = expire_at_ms || (Time.now.to_i * 1000 + 60 * 60 * 1000) # 1 hour default
+
+ query_params = {}
+ url_params.each do |key, value|
+ next if value.nil?
+ Array(value).each do |val|
+ next if val.nil?
+ (query_params[key.to_s] ||= []) << val.to_s
+ end
+ end
+
+ query_params["auth_key"] = [key]
+ query_params["exp"] = [expire_at.to_s]
+
+ # Sort parameters to ensure consistent ordering
+ sorted_params = query_params.sort.flat_map do |key, values|
+ values.map { |v| "#{CGI.escape(key)}=#{CGI.escape(v)}" }
+ end.join("&")
+
+ string_to_sign = "#{workspace_slug}/#{template_slug}/#{input_field}?#{sorted_params}"
+
+ signature = OpenSSL::HMAC.hexdigest("sha256", secret, string_to_sign)
+
+ final_params = "#{sorted_params}&sig=#{CGI.escape("sha256:#{signature}")}"
+ "https://#{workspace_slug}.tlcdn.com/#{template_slug}/#{input_field}?#{final_params}"
+ end
+
private
#
diff --git a/test/test_helper.rb b/test/test_helper.rb
index ae9fa98..8ff789f 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,7 +1,7 @@
$:.unshift File.dirname(__FILE__)
$:.unshift File.expand_path("../../lib", __FILE__)
-if ENV["COVERAGE"] != "false"
+if ENV["COVERAGE"] != "0"
require "simplecov"
SimpleCov.start { add_filter "/test/" }
end
diff --git a/test/unit/transloadit/node-smartcdn-sig.ts b/test/unit/transloadit/node-smartcdn-sig.ts
new file mode 100644
index 0000000..14c9461
--- /dev/null
+++ b/test/unit/transloadit/node-smartcdn-sig.ts
@@ -0,0 +1,89 @@
+#!/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_smart_cdn.rb b/test/unit/transloadit/test_smart_cdn.rb
new file mode 100644
index 0000000..586eaa7
--- /dev/null
+++ b/test/unit/transloadit/test_smart_cdn.rb
@@ -0,0 +1,169 @@
+require "test_helper"
+require "json"
+require "open3"
+
+describe Transloadit do
+ before do
+ @auth_key = "my-key"
+ @auth_secret = "my-secret"
+ @transloadit = Transloadit.new(key: @auth_key, secret: @auth_secret)
+ @workspace = "my-app"
+ @template = "test-smart-cdn"
+ @input = "inputs/prinsengracht.jpg"
+ @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
+ @transloadit.signed_smart_cdn_url(
+ workspace: nil,
+ template: @template,
+ input: @input
+ )
+ end
+
+ assert_raises ArgumentError, "workspace is required" do
+ @transloadit.signed_smart_cdn_url(
+ workspace: "",
+ template: @template,
+ input: @input
+ )
+ end
+ end
+
+ it "requires template" do
+ assert_raises ArgumentError, "template is required" do
+ @transloadit.signed_smart_cdn_url(
+ workspace: @workspace,
+ template: nil,
+ input: @input
+ )
+ end
+
+ assert_raises ArgumentError, "template is required" do
+ @transloadit.signed_smart_cdn_url(
+ workspace: @workspace,
+ template: "",
+ input: @input
+ )
+ end
+ end
+
+ it "requires input" do
+ assert_raises ArgumentError, "input is required" do
+ @transloadit.signed_smart_cdn_url(
+ workspace: @workspace,
+ template: @template,
+ input: nil
+ )
+ end
+ end
+
+ it "allows empty input string" do
+ params = {
+ workspace: @workspace,
+ template: @template,
+ input: "",
+ expire_at_ms: @expire_at
+ }
+ expected_url = "https://my-app.tlcdn.com/test-smart-cdn/?auth_key=my-key&exp=1732550672867&sig=sha256%3Ad5e13df4acde8d4aaa0f34534489e54098b5128c54392600ed96dd77669a533e"
+
+ 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
+ end
+ end
+
+ it "uses instance credentials" do
+ params = {
+ workspace: @workspace,
+ template: @template,
+ input: @input,
+ expire_at_ms: @expire_at
+ }
+ expected_url = "https://my-app.tlcdn.com/test-smart-cdn/inputs%2Fprinsengracht.jpg?auth_key=my-key&exp=1732550672867&sig=sha256%3A8620fc2a22aec6081cde730b7f3f29c0d8083f58a68f62739e642b3c03709139"
+
+ 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
+ end
+ end
+
+ it "includes empty width parameter" do
+ params = {
+ workspace: @workspace,
+ template: @template,
+ input: @input,
+ expire_at_ms: @expire_at,
+ url_params: {
+ width: "",
+ height: 200
+ }
+ }
+ expected_url = "https://my-app.tlcdn.com/test-smart-cdn/inputs%2Fprinsengracht.jpg?auth_key=my-key&exp=1732550672867&height=200&width=&sig=sha256%3Aebf562722c504839db97165e657583f74192ac4ab580f1a0dd67d3d868b4ced3"
+
+ 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
+ end
+ end
+
+ it "handles nil values in parameters" do
+ params = {
+ workspace: @workspace,
+ template: @template,
+ input: @input,
+ expire_at_ms: @expire_at,
+ url_params: {
+ width: nil,
+ height: 200
+ }
+ }
+ expected_url = "https://my-app.tlcdn.com/test-smart-cdn/inputs%2Fprinsengracht.jpg?auth_key=my-key&exp=1732550672867&height=200&sig=sha256%3Ad6897a0cb527a14eaab13c54b06f53527797c553d8b7e5d0b1a5df237212f083"
+
+ 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
+ end
+ end
+
+ it "handles array values in parameters" do
+ params = {
+ workspace: @workspace,
+ template: @template,
+ input: @input,
+ expire_at_ms: @expire_at,
+ url_params: {
+ tags: ["landscape", "amsterdam", nil, ""],
+ height: 200
+ }
+ }
+ expected_url = "https://my-app.tlcdn.com/test-smart-cdn/inputs%2Fprinsengracht.jpg?auth_key=my-key&exp=1732550672867&height=200&tags=landscape&tags=amsterdam&tags=&sig=sha256%3Aff46eb0083d64b250b2e4510380e333f67da855b2401493dee7a706a47957d3f"
+
+ 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
+ end
+ end
+ end
+end