Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.gitignore
.docker-cache
.env
vendor
node_modules
transloadit-*.tgz
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ env.sh
.phpunit.cache
.aider*
.env
.docker-cache/
27 changes: 27 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1

FROM php:8.3-cli AS base

ENV COMPOSER_ALLOW_SUPERUSER=1

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
unzip \
zip \
libzip-dev \
curl \
ca-certificates \
&& docker-php-ext-configure zip \
&& docker-php-ext-install zip \
&& rm -rf /var/lib/apt/lists/*

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Install Node.js (for transloadit CLI 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
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,28 +555,56 @@ make test-all

#### Node.js Reference Implementation Parity Assertions

The SDK includes assertions that compare URL signing with our reference Node.js implementation. To run these tests:
The SDK includes assertions that compare Smart CDN URL signatures and regular request signatures with our reference Node.js implementation. To run these tests:

1. Requirements:

- Node.js installed
- tsx installed globally (`npm install -g tsx`)
- Node.js 20+ with npm
- Ability to execute `npx transloadit smart_sig` (the CLI is downloaded on demand)
- Ability to execute `npx transloadit sig` (the CLI is downloaded on demand)

2. Install dependencies:
2. Run the tests:

```bash
npm install -g tsx
export TRANSLOADIT_KEY='your-auth-key'
export TRANSLOADIT_SECRET='your-auth-secret'
TEST_NODE_PARITY=1 make test-all
```

3. Run the test:
If you want to warm the CLI cache ahead of time you can run:

```bash
export TRANSLOADIT_KEY='your-auth-key'
export TRANSLOADIT_SECRET='your-auth-secret'
TEST_NODE_PARITY=1 make test-all
npx --yes transloadit smart_sig --help
```

CI opts-into `TEST_NODE_PARITY=1`, and you can optionally do this locally as well.
For regular request signatures, you can also prime the CLI by running:

```bash
TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \
npx --yes transloadit sig --algorithm sha1 --help
```

CI opts into `TEST_NODE_PARITY=1`, and you can optionally do this locally as well.

#### Run Tests in Docker

Use `scripts/test-in-docker.sh` for a reproducible environment:

```bash
./scripts/test-in-docker.sh
```

This builds the local image, runs `composer install`, and executes `make test-all` (unit + integration tests). Pass a custom command to run something else (composer install still runs first):

```bash
./scripts/test-in-docker.sh vendor/bin/phpunit --filter signedSmartCDNUrl
```

Environment variables such as `TEST_NODE_PARITY` or the credentials in `.env` are forwarded, so you can combine parity checks and integration tests with Docker:

```bash
TEST_NODE_PARITY=1 ./scripts/test-in-docker.sh
```

### Releasing a new version

Expand Down
75 changes: 75 additions & 0 deletions scripts/test-in-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail

IMAGE_NAME=${IMAGE_NAME:-transloadit-php-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; composer install --no-interaction --prefer-dist; make test-all'
else
printf -v USER_CMD '%q ' "$@"
RUN_CMD="set -e; composer install --no-interaction --prefer-dist; ${USER_CMD}"
fi

mkdir -p "$CACHE_DIR/composer-cache" "$CACHE_DIR/npm-cache" "$CACHE_DIR/composer-home"

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 COMPOSER_HOME=/workspace/$CACHE_DIR/composer-home
-e COMPOSER_CACHE_DIR=/workspace/$CACHE_DIR/composer-cache
-e npm_config_cache=/workspace/$CACHE_DIR/npm-cache
-e TEST_NODE_PARITY="${TEST_NODE_PARITY:-0}"
-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

exec docker run "${DOCKER_ARGS[@]}" "$IMAGE_NAME" bash -lc "$RUN_CMD"
139 changes: 139 additions & 0 deletions test/simple/TransloaditRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,149 @@ public function testGetParamsString() {
$this->assertEquals($PARAMS['foo'], $params['foo']);
}

public function testSignatureParityWithNodeCli(): void {
if (getenv('TEST_NODE_PARITY') !== '1') {
$this->markTestSkipped('Parity testing not enabled');
}

$request = new TransloaditRequest();
$request->key = 'cli-key';
$request->secret = 'cli-secret';
$request->expires = '2025-01-02 00:00:00+00:00';
$request->params = [
'auth' => ['expires' => '2025-01-02 00:00:00+00:00'],
'steps' => [
'resize' => [
'robot' => '/image/resize',
'width' => 320,
],
],
];

$cliResult = $this->getCliSignature([
'auth' => ['expires' => '2025-01-02 00:00:00+00:00'],
'steps' => [
'resize' => [
'robot' => '/image/resize',
'width' => 320,
],
],
], 'cli-key', 'cli-secret', 'sha1');

$this->assertNotNull($cliResult);
$this->assertArrayHasKey('signature', $cliResult);
$this->assertArrayHasKey('params', $cliResult);

$cliParams = json_decode($cliResult['params'], true);
$phpParams = json_decode($request->getParamsString(), true);

$this->assertEquals('cli-key', $cliParams['auth']['key']);
$this->assertEquals($phpParams['auth']['expires'], $cliParams['auth']['expires']);
$this->assertEquals(
$phpParams['steps']['resize']['robot'],
$cliParams['steps']['resize']['robot']
);
$this->assertEquals(
$phpParams['steps']['resize']['width'],
$cliParams['steps']['resize']['width']
);

$expectedSignature = hash_hmac('sha1', $cliResult['params'], 'cli-secret');
$this->assertEquals('sha1:' . $expectedSignature, $cliResult['signature']);
}

public function testExecute() {
// Can't test this method because PHP doesn't allow stubbing the calls
// to curl easily. However, the method hardly contains any logic as all
// of that is located in other methods.
$this->assertTrue(true);
}

private function getCliSignature(array $params, string $key, string $secret, ?string $algorithm = null): ?array {
if (getenv('TEST_NODE_PARITY') !== '1') {
return null;
}

exec('command -v npm 2>/dev/null', $output, $returnVar);
if ($returnVar !== 0) {
throw new \RuntimeException('npm command not found. Please install Node.js (which includes npm).');
}

try {
$jsonInput = json_encode($params, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \RuntimeException('Failed to encode parameters for Node parity test: ' . $e->getMessage(), 0, $e);
}

$command = 'npm exec --yes --package [email protected] -- transloadit sig';
if ($algorithm !== null) {
$command .= ' --algorithm ' . escapeshellarg($algorithm);
}

$descriptorspec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"], // stderr
];

$originalKey = getenv('TRANSLOADIT_KEY');
$originalSecret = getenv('TRANSLOADIT_SECRET');
$originalAuthKey = getenv('TRANSLOADIT_AUTH_KEY');
$originalAuthSecret = getenv('TRANSLOADIT_AUTH_SECRET');

putenv('TRANSLOADIT_KEY=' . $key);
putenv('TRANSLOADIT_SECRET=' . $secret);
putenv('TRANSLOADIT_AUTH_KEY=' . $key);
putenv('TRANSLOADIT_AUTH_SECRET=' . $secret);

try {
$process = proc_open($command, $descriptorspec, $pipes);

if (!is_resource($process)) {
throw new \RuntimeException('Failed to start transloadit CLI sig command');
}

fwrite($pipes[0], $jsonInput);
fclose($pipes[0]);

$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);

fclose($pipes[1]);
fclose($pipes[2]);

$exitCode = proc_close($process);

if ($exitCode !== 0) {
$message = trim($stderr) !== '' ? trim($stderr) : 'Command exited with status ' . $exitCode;
throw new \RuntimeException('transloadit CLI sig command failed: ' . $message);
}

return json_decode(trim($stdout), true, 512, JSON_THROW_ON_ERROR);
} finally {
if ($originalKey !== false) {
putenv('TRANSLOADIT_KEY=' . $originalKey);
} else {
putenv('TRANSLOADIT_KEY');
}

if ($originalSecret !== false) {
putenv('TRANSLOADIT_SECRET=' . $originalSecret);
} else {
putenv('TRANSLOADIT_SECRET');
}

if ($originalAuthKey !== false) {
putenv('TRANSLOADIT_AUTH_KEY=' . $originalAuthKey);
} else {
putenv('TRANSLOADIT_AUTH_KEY');
}

if ($originalAuthSecret !== false) {
putenv('TRANSLOADIT_AUTH_SECRET=' . $originalAuthSecret);
} else {
putenv('TRANSLOADIT_AUTH_SECRET');
}
}
}
}
Loading
Loading