un-inception/clients/ruby/sync/test/un_test.rb
russell@unturf.com 331cba42aa feat: Complete 6 additional SDK implementations with fixes and examples
Go Async SDK (clients/go/async/):
- Fixed case-sensitive language detection bug (.R for R language)
- Created go.mod for module management
- Added comprehensive test suite
- Created 3 examples with expected output comments
- Added README with full documentation

Java Sync SDK (clients/java/sync/):
- RENAMED: Unsandbox.java -> Un.java (matches naming convention)
- Updated class name from Unsandbox to Un
- Created pom.xml for Maven build
- Added 6 examples (simple + SDK client versions)
- Created test suite with JUnit 5
- Added README with API documentation

JavaScript Async SDK (clients/javascript/async/):
- Fixed unused import
- Created package.json with ES module support
- Added 5 examples covering all async patterns
- Created 71 tests (all passing)
- Added comprehensive README

PHP Sync SDK (clients/php/sync/):
- RENAMED: Unsandbox.php -> un.php (matches naming convention)
- Created composer.json with PSR-4 autoloading
- Created phpunit.xml for testing
- Added 4 examples with expected output comments
- Created 54 tests across 4 test files
- Added README with full documentation

Ruby Sync SDK (clients/ruby/sync/):
- Created Gemfile and un.gemspec
- Created Rakefile with test task
- Updated examples to actually use the SDK
- Added 4 examples (hello_world, async_job, language_detection, snapshots)
- Created comprehensive test suite with 30+ tests
- Added README with documentation

Rust Sync SDK (clients/rust/sync/):
- Updated Cargo.toml with example declarations
- Created 4 examples (hello_world, fibonacci, multi_language, async_polling)
- Added comprehensive README with API reference
- All dependencies verified correct

All SDKs verified:
- HMAC-SHA256 authentication implemented
- 4-tier credential system (args > env > ~/.unsandbox > ./accounts.csv)
- Expected output comments for pipeline validation
- Proper error handling
- Language detection support
2026-01-15 17:32:24 -05:00

518 lines
16 KiB
Ruby

# frozen_string_literal: true
require_relative 'test_helper'
require 'json'
require 'openssl'
class UnTest < Minitest::Test
API_BASE = 'https://api.unsandbox.com'
def setup
WebMock.reset!
# Clear environment variables
ENV.delete('UNSANDBOX_PUBLIC_KEY')
ENV.delete('UNSANDBOX_SECRET_KEY')
ENV.delete('UNSANDBOX_ACCOUNT')
end
# =========================================================================
# Credential Resolution Tests
# =========================================================================
def test_credentials_from_method_arguments
stub_execute_request(status: 'completed', stdout: 'hello')
result = Un.execute_code(
'python',
'print("hello")',
public_key: 'pk-test',
secret_key: 'sk-test'
)
assert_equal 'completed', result['status']
assert_equal 'hello', result['stdout']
end
def test_credentials_from_environment
ENV['UNSANDBOX_PUBLIC_KEY'] = 'env-pk'
ENV['UNSANDBOX_SECRET_KEY'] = 'env-sk'
stub_execute_request(status: 'completed', stdout: 'env-test')
result = Un.execute_code('python', 'print("env")')
assert_equal 'completed', result['status']
assert_requested :post, "#{API_BASE}/execute" do |req|
req.headers['Authorization'] == 'Bearer env-pk'
end
end
def test_credentials_error_when_none_found
error = assert_raises(Un::CredentialsError) do
Un.execute_code('python', 'print("test")')
end
assert_match(/No credentials found/, error.message)
end
# =========================================================================
# HMAC-SHA256 Signature Tests
# =========================================================================
def test_signature_format
ENV['UNSANDBOX_PUBLIC_KEY'] = 'pk-sig'
ENV['UNSANDBOX_SECRET_KEY'] = 'sk-sig-secret'
captured_signature = nil
captured_timestamp = nil
stub_request(:post, "#{API_BASE}/execute")
.with { |req|
captured_signature = req.headers['X-Signature']
captured_timestamp = req.headers['X-Timestamp']
true
}
.to_return(
status: 200,
body: JSON.generate(status: 'completed', stdout: 'test'),
headers: { 'Content-Type' => 'application/json' }
)
Un.execute_code('python', 'print("test")')
# Verify signature is 64-char hex
assert_match(/\A[0-9a-f]{64}\z/, captured_signature)
# Verify timestamp is numeric
assert_match(/\A\d+\z/, captured_timestamp)
# Verify signature is correct HMAC-SHA256
body = JSON.generate(language: 'python', code: 'print("test")')
message = "#{captured_timestamp}:POST:/execute:#{body}"
expected_sig = OpenSSL::HMAC.hexdigest('SHA256', 'sk-sig-secret', message)
assert_equal expected_sig, captured_signature
end
def test_signature_with_empty_body_for_get
ENV['UNSANDBOX_PUBLIC_KEY'] = 'pk-get'
ENV['UNSANDBOX_SECRET_KEY'] = 'sk-get-secret'
captured_signature = nil
captured_timestamp = nil
stub_request(:get, "#{API_BASE}/languages")
.with { |req|
captured_signature = req.headers['X-Signature']
captured_timestamp = req.headers['X-Timestamp']
true
}
.to_return(
status: 200,
body: JSON.generate(languages: %w[python javascript]),
headers: { 'Content-Type' => 'application/json' }
)
# Clear language cache to force API call
cache_path = File.join(Dir.home, '.unsandbox', 'languages.json')
File.delete(cache_path) if File.exist?(cache_path)
Un.get_languages
# Verify signature for GET (empty body)
message = "#{captured_timestamp}:GET:/languages:"
expected_sig = OpenSSL::HMAC.hexdigest('SHA256', 'sk-get-secret', message)
assert_equal expected_sig, captured_signature
end
# =========================================================================
# Execute Code Tests
# =========================================================================
def test_execute_code_sync_returns_immediately_on_completed
stub_execute_request(status: 'completed', stdout: 'hello world')
result = Un.execute_code(
'python',
'print("hello world")',
public_key: 'pk',
secret_key: 'sk'
)
assert_equal 'completed', result['status']
assert_equal 'hello world', result['stdout']
end
def test_execute_code_polls_when_pending
# First request returns pending with job_id
stub_request(:post, "#{API_BASE}/execute")
.to_return(
status: 200,
body: JSON.generate(status: 'pending', job_id: 'job-123'),
headers: { 'Content-Type' => 'application/json' }
)
# Subsequent polls return completed
stub_request(:get, "#{API_BASE}/jobs/job-123")
.to_return(
status: 200,
body: JSON.generate(status: 'completed', stdout: 'polled result'),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.execute_code('python', 'print("test")', public_key: 'pk', secret_key: 'sk')
assert_equal 'completed', result['status']
assert_equal 'polled result', result['stdout']
end
# =========================================================================
# Execute Async Tests
# =========================================================================
def test_execute_async_returns_job_id
stub_request(:post, "#{API_BASE}/execute")
.to_return(
status: 200,
body: JSON.generate(job_id: 'async-job-456', status: 'pending'),
headers: { 'Content-Type' => 'application/json' }
)
job_id = Un.execute_async('javascript', 'console.log("async")', public_key: 'pk', secret_key: 'sk')
assert_equal 'async-job-456', job_id
end
# =========================================================================
# Job Management Tests
# =========================================================================
def test_get_job
stub_request(:get, "#{API_BASE}/jobs/job-789")
.to_return(
status: 200,
body: JSON.generate(job_id: 'job-789', status: 'running'),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.get_job('job-789', public_key: 'pk', secret_key: 'sk')
assert_equal 'job-789', result['job_id']
assert_equal 'running', result['status']
end
def test_list_jobs
stub_request(:get, "#{API_BASE}/jobs")
.to_return(
status: 200,
body: JSON.generate(jobs: [
{ job_id: 'j1', status: 'completed' },
{ job_id: 'j2', status: 'running' }
]),
headers: { 'Content-Type' => 'application/json' }
)
jobs = Un.list_jobs(public_key: 'pk', secret_key: 'sk')
assert_equal 2, jobs.length
assert_equal 'j1', jobs[0]['job_id']
end
def test_cancel_job
stub_request(:delete, "#{API_BASE}/jobs/cancel-me")
.to_return(
status: 200,
body: JSON.generate(status: 'cancelled'),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.cancel_job('cancel-me', public_key: 'pk', secret_key: 'sk')
assert_equal 'cancelled', result['status']
end
# =========================================================================
# Wait for Job Tests
# =========================================================================
def test_wait_for_job_returns_on_completed
stub_request(:get, "#{API_BASE}/jobs/wait-job")
.to_return(
status: 200,
body: JSON.generate(status: 'completed', stdout: 'done'),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.wait_for_job('wait-job', public_key: 'pk', secret_key: 'sk')
assert_equal 'completed', result['status']
assert_equal 'done', result['stdout']
end
def test_wait_for_job_returns_on_failed
stub_request(:get, "#{API_BASE}/jobs/fail-job")
.to_return(
status: 200,
body: JSON.generate(status: 'failed', error: 'execution error'),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.wait_for_job('fail-job', public_key: 'pk', secret_key: 'sk')
assert_equal 'failed', result['status']
end
def test_wait_for_job_polls_until_done
call_count = 0
stub_request(:get, "#{API_BASE}/jobs/poll-job")
.to_return do |_request|
call_count += 1
if call_count < 3
{
status: 200,
body: JSON.generate(status: 'running'),
headers: { 'Content-Type' => 'application/json' }
}
else
{
status: 200,
body: JSON.generate(status: 'completed', stdout: 'finally'),
headers: { 'Content-Type' => 'application/json' }
}
end
end
result = Un.wait_for_job('poll-job', public_key: 'pk', secret_key: 'sk')
assert_equal 'completed', result['status']
assert_equal 'finally', result['stdout']
assert_equal 3, call_count
end
# =========================================================================
# Language Detection Tests
# =========================================================================
def test_detect_language_python
assert_equal 'python', Un.detect_language('script.py')
end
def test_detect_language_javascript
assert_equal 'javascript', Un.detect_language('app.js')
end
def test_detect_language_typescript
assert_equal 'typescript', Un.detect_language('index.ts')
end
def test_detect_language_ruby
assert_equal 'ruby', Un.detect_language('file.rb')
end
def test_detect_language_go
assert_equal 'go', Un.detect_language('main.go')
end
def test_detect_language_rust
assert_equal 'rust', Un.detect_language('lib.rs')
end
def test_detect_language_cpp_variants
assert_equal 'cpp', Un.detect_language('file.cpp')
assert_equal 'cpp', Un.detect_language('file.cc')
assert_equal 'cpp', Un.detect_language('file.cxx')
end
def test_detect_language_unknown
assert_nil Un.detect_language('unknown.xyz')
assert_nil Un.detect_language('noextension')
assert_nil Un.detect_language(nil)
end
# =========================================================================
# Languages API Tests
# =========================================================================
def test_get_languages
# Clear cache first
cache_path = File.join(Dir.home, '.unsandbox', 'languages.json')
File.delete(cache_path) if File.exist?(cache_path)
stub_request(:get, "#{API_BASE}/languages")
.to_return(
status: 200,
body: JSON.generate(languages: %w[python javascript ruby go]),
headers: { 'Content-Type' => 'application/json' }
)
languages = Un.get_languages(public_key: 'pk', secret_key: 'sk')
assert_includes languages, 'python'
assert_includes languages, 'javascript'
assert_equal 4, languages.length
# Clean up
File.delete(cache_path) if File.exist?(cache_path)
end
# =========================================================================
# Snapshot Tests
# =========================================================================
def test_session_snapshot
stub_request(:post, "#{API_BASE}/snapshots")
.to_return(
status: 200,
body: JSON.generate(snapshot_id: 'snap-session-123'),
headers: { 'Content-Type' => 'application/json' }
)
snapshot_id = Un.session_snapshot('sess-456', public_key: 'pk', secret_key: 'sk')
assert_equal 'snap-session-123', snapshot_id
assert_requested :post, "#{API_BASE}/snapshots" do |req|
body = JSON.parse(req.body)
body['session_id'] == 'sess-456' && body['hot'] == false
end
end
def test_session_snapshot_ephemeral
stub_request(:post, "#{API_BASE}/snapshots")
.to_return(
status: 200,
body: JSON.generate(snapshot_id: 'snap-ephemeral'),
headers: { 'Content-Type' => 'application/json' }
)
Un.session_snapshot('sess-789', public_key: 'pk', secret_key: 'sk', ephemeral: true)
assert_requested :post, "#{API_BASE}/snapshots" do |req|
body = JSON.parse(req.body)
body['hot'] == true
end
end
def test_service_snapshot
stub_request(:post, "#{API_BASE}/snapshots")
.to_return(
status: 200,
body: JSON.generate(snapshot_id: 'snap-service-456'),
headers: { 'Content-Type' => 'application/json' }
)
snapshot_id = Un.service_snapshot('svc-123', public_key: 'pk', secret_key: 'sk', name: 'backup')
assert_equal 'snap-service-456', snapshot_id
assert_requested :post, "#{API_BASE}/snapshots" do |req|
body = JSON.parse(req.body)
body['service_id'] == 'svc-123' && body['name'] == 'backup'
end
end
def test_list_snapshots
stub_request(:get, "#{API_BASE}/snapshots")
.to_return(
status: 200,
body: JSON.generate(snapshots: [
{ snapshot_id: 's1', name: 'first' },
{ snapshot_id: 's2', name: 'second' }
]),
headers: { 'Content-Type' => 'application/json' }
)
snapshots = Un.list_snapshots(public_key: 'pk', secret_key: 'sk')
assert_equal 2, snapshots.length
end
def test_restore_snapshot
stub_request(:post, "#{API_BASE}/snapshots/snap-restore/restore")
.to_return(
status: 200,
body: JSON.generate(session_id: 'new-session'),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.restore_snapshot('snap-restore', public_key: 'pk', secret_key: 'sk')
assert_equal 'new-session', result['session_id']
end
def test_delete_snapshot
stub_request(:delete, "#{API_BASE}/snapshots/snap-delete")
.to_return(
status: 200,
body: JSON.generate(deleted: true),
headers: { 'Content-Type' => 'application/json' }
)
result = Un.delete_snapshot('snap-delete', public_key: 'pk', secret_key: 'sk')
assert result['deleted']
end
# =========================================================================
# Error Handling Tests
# =========================================================================
def test_api_error_on_401
stub_request(:post, "#{API_BASE}/execute")
.to_return(
status: 401,
body: JSON.generate(error: 'unauthorized'),
headers: { 'Content-Type' => 'application/json' }
)
error = assert_raises(Un::APIError) do
Un.execute_code('python', 'print()', public_key: 'bad', secret_key: 'creds')
end
assert_equal 401, error.status_code
assert_match(/401/, error.message)
end
def test_api_error_on_429
stub_request(:post, "#{API_BASE}/execute")
.to_return(
status: 429,
body: JSON.generate(error: 'rate_limit_exceeded'),
headers: { 'Content-Type' => 'application/json' }
)
error = assert_raises(Un::APIError) do
Un.execute_code('python', 'print()', public_key: 'pk', secret_key: 'sk')
end
assert_equal 429, error.status_code
end
def test_api_error_on_500
stub_request(:post, "#{API_BASE}/execute")
.to_return(
status: 500,
body: 'Internal Server Error',
headers: { 'Content-Type' => 'text/plain' }
)
error = assert_raises(Un::APIError) do
Un.execute_code('python', 'print()', public_key: 'pk', secret_key: 'sk')
end
assert_equal 500, error.status_code
end
private
def stub_execute_request(status:, stdout: '', stderr: '', exit_code: 0)
stub_request(:post, "#{API_BASE}/execute")
.to_return(
status: 200,
body: JSON.generate(
status: status,
stdout: stdout,
stderr: stderr,
exit_code: exit_code
),
headers: { 'Content-Type' => 'application/json' }
)
end
end