diff --git a/.github/workflows/inception-matrix.yml b/.github/workflows/inception-matrix.yml index 548e0df..2e68acb 100644 --- a/.github/workflows/inception-matrix.yml +++ b/.github/workflows/inception-matrix.yml @@ -18,6 +18,63 @@ env: UNSANDBOX_SECRET_KEY: ${{ secrets.UNSANDBOX_SECRET_KEY }} jobs: + # ============================================================================ + # UNIT TESTS - Test internal logic without API calls + # ============================================================================ + unit-tests: + name: "Unit Tests: ${{ matrix.lang }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - lang: Python + run: python3 tests/unit/test_python.py + - lang: JavaScript + run: node tests/unit/test_javascript.js + - lang: Ruby + run: ruby tests/unit/test_ruby.rb + - lang: Go + run: go run tests/unit/test_go.go + - lang: Bash + run: bash tests/unit/test_bash.sh + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + if: matrix.lang == 'Go' + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run ${{ matrix.lang }} unit tests + run: ${{ matrix.run }} + + unit-tests-lua: + name: "Unit Tests: Lua" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Lua + run: | + sudo apt-get update + sudo apt-get install -y lua5.4 + - name: Run Lua unit tests + run: lua tests/unit/test_lua.lua + + # ============================================================================ + # INTEGRATION TESTS - Test component interactions without real API calls + # ============================================================================ + integration-tests: + name: "Integration Tests" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run Python integration tests + run: python3 tests/integration/test_request_building.py + # ============================================================================ # TIER 1: Scripting Languages (fast, native runners) # ============================================================================ diff --git a/tests/integration/run_all.sh b/tests/integration/run_all.sh new file mode 100755 index 0000000..c5c2d52 --- /dev/null +++ b/tests/integration/run_all.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Run all integration tests for UN CLI implementations +# These tests verify component interactions without making real API calls + +set -o pipefail + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +cd "$(dirname "$0")" + +echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ UN CLI Inception - Integration Tests ║${NC}" +echo -e "${CYAN}║ Testing component interactions, no real API calls ║${NC}" +echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" +echo "" + +passed=0 +failed=0 +skipped=0 + +run_test() { + local name="$1" + local cmd="$2" + local interpreter="$3" + + printf "%-30s" "$name" + + if [ -n "$interpreter" ] && ! command -v "$interpreter" &> /dev/null; then + echo -e "${YELLOW}SKIP${NC} ($interpreter not found)" + skipped=$((skipped + 1)) + return + fi + + if $cmd > /dev/null 2>&1; then + echo -e "${GREEN}PASS${NC}" + passed=$((passed + 1)) + else + echo -e "${RED}FAIL${NC}" + failed=$((failed + 1)) + fi +} + +echo -e "${CYAN}━━━ Request Building Tests ━━━${NC}" +run_test "Python Request Building" "python3 test_request_building.py" "python3" +echo "" + +# Summary +echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" +echo "" +total=$((passed + failed + skipped)) +echo -e "Results: ${GREEN}$passed PASS${NC} | ${RED}$failed FAIL${NC} | ${YELLOW}$skipped SKIP${NC} | Total: $total" +echo "" + +if [ $failed -eq 0 ]; then + echo -e "${GREEN}All integration tests passed.${NC}" + exit 0 +else + echo -e "${RED}$failed test(s) failed.${NC}" + exit 1 +fi diff --git a/tests/integration/test_request_building.py b/tests/integration/test_request_building.py new file mode 100755 index 0000000..6e45763 --- /dev/null +++ b/tests/integration/test_request_building.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +""" +Integration tests for request building - tests component interactions without API calls +""" + +import sys +import os +import json +import hmac +import hashlib +import base64 +import tempfile +import unittest +from unittest.mock import patch, MagicMock +from io import StringIO + +# Set mock environment before importing +os.environ['UNSANDBOX_PUBLIC_KEY'] = 'test-pk-1234' +os.environ['UNSANDBOX_SECRET_KEY'] = 'test-sk-5678' + + +class TestRequestBuilding(unittest.TestCase): + """Test that requests are built correctly with all components""" + + def test_execute_request_structure(self): + """Test execute request has correct structure""" + language = "python" + code = 'print("hello world")' + env_vars = {"DEBUG": "1", "NAME": "test"} + network_mode = "zerotrust" + + # Build request body + body = { + "language": language, + "code": code, + "env": env_vars, + "network_mode": network_mode + } + + json_body = json.dumps(body) + parsed = json.loads(json_body) + + self.assertEqual(parsed["language"], "python") + self.assertEqual(parsed["code"], code) + self.assertEqual(parsed["env"]["DEBUG"], "1") + self.assertEqual(parsed["network_mode"], "zerotrust") + + def test_execute_request_with_files(self): + """Test execute request with input files""" + # Create temp file + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write("test data content") + temp_path = f.name + + try: + # Read and encode file + with open(temp_path, 'rb') as f: + content = f.read() + encoded = base64.b64encode(content).decode('utf-8') + + files = [{ + "name": os.path.basename(temp_path), + "content": encoded + }] + + body = { + "language": "python", + "code": "print(open('test.txt').read())", + "files": files + } + + json_body = json.dumps(body) + parsed = json.loads(json_body) + + self.assertIn("files", parsed) + self.assertEqual(len(parsed["files"]), 1) + self.assertIn("content", parsed["files"][0]) + + # Verify content can be decoded back + decoded = base64.b64decode(parsed["files"][0]["content"]) + self.assertEqual(decoded.decode('utf-8'), "test data content") + + finally: + os.unlink(temp_path) + + def test_hmac_signature_generation(self): + """Test HMAC signature is generated correctly for requests""" + public_key = "test-pk-1234" + secret_key = "test-sk-5678" + timestamp = "1704067200" + method = "POST" + endpoint = "/execute" + body = '{"language":"python","code":"print(1)"}' + + # Build signature input + signature_input = f"{timestamp}:{method}:{endpoint}:{body}" + + # Generate signature + signature = hmac.new( + secret_key.encode(), + signature_input.encode(), + hashlib.sha256 + ).hexdigest() + + # Verify signature properties + self.assertEqual(len(signature), 64) + self.assertTrue(all(c in '0123456789abcdef' for c in signature)) + + # Verify same input produces same signature + signature2 = hmac.new( + secret_key.encode(), + signature_input.encode(), + hashlib.sha256 + ).hexdigest() + self.assertEqual(signature, signature2) + + def test_session_request_structure(self): + """Test session request has correct structure""" + body = { + "shell": "python3", + "network_mode": "semitrusted" + } + + json_body = json.dumps(body) + parsed = json.loads(json_body) + + self.assertEqual(parsed["shell"], "python3") + self.assertEqual(parsed["network_mode"], "semitrusted") + + def test_service_request_structure(self): + """Test service request has correct structure""" + body = { + "name": "my-service", + "ports": [8080, 443], + "bootstrap": "python3 -m http.server 8080", + "network_mode": "semitrusted" + } + + json_body = json.dumps(body) + parsed = json.loads(json_body) + + self.assertEqual(parsed["name"], "my-service") + self.assertEqual(parsed["ports"], [8080, 443]) + self.assertEqual(parsed["bootstrap"], "python3 -m http.server 8080") + + def test_authorization_header_format(self): + """Test Authorization header is formatted correctly""" + public_key = "unsb-pk-1234-5678-abcd-efgh" + auth_header = f"Bearer {public_key}" + + self.assertTrue(auth_header.startswith("Bearer ")) + self.assertIn(public_key, auth_header) + + def test_request_headers_structure(self): + """Test all required headers are present""" + public_key = "unsb-pk-1234" + timestamp = "1704067200" + signature = "abc123def456" + + headers = { + "Authorization": f"Bearer {public_key}", + "X-Timestamp": timestamp, + "X-Signature": signature, + "Content-Type": "application/json" + } + + self.assertIn("Authorization", headers) + self.assertIn("X-Timestamp", headers) + self.assertIn("X-Signature", headers) + self.assertIn("Content-Type", headers) + self.assertEqual(headers["Content-Type"], "application/json") + + +class TestResponseParsing(unittest.TestCase): + """Test that responses are parsed correctly""" + + def test_execute_response_parsing(self): + """Test execute response is parsed correctly""" + response_json = { + "id": "exec-12345", + "status": "completed", + "stdout": "hello world\n", + "stderr": "", + "exit_code": 0, + "execution_time": 0.123 + } + + json_str = json.dumps(response_json) + parsed = json.loads(json_str) + + self.assertEqual(parsed["id"], "exec-12345") + self.assertEqual(parsed["status"], "completed") + self.assertEqual(parsed["stdout"], "hello world\n") + self.assertEqual(parsed["exit_code"], 0) + + def test_session_response_parsing(self): + """Test session response is parsed correctly""" + response_json = { + "id": "sess-12345", + "status": "ready", + "websocket_url": "wss://api.unsandbox.com/ws/sess-12345" + } + + json_str = json.dumps(response_json) + parsed = json.loads(json_str) + + self.assertEqual(parsed["id"], "sess-12345") + self.assertEqual(parsed["status"], "ready") + self.assertIn("websocket_url", parsed) + + def test_service_response_parsing(self): + """Test service response is parsed correctly""" + response_json = { + "id": "svc-12345", + "name": "my-service", + "status": "running", + "domains": ["my-service.unsandbox.run"], + "ports": {"8080": "https://my-service.unsandbox.run:8080"} + } + + json_str = json.dumps(response_json) + parsed = json.loads(json_str) + + self.assertEqual(parsed["id"], "svc-12345") + self.assertEqual(parsed["name"], "my-service") + self.assertEqual(parsed["status"], "running") + self.assertIsInstance(parsed["domains"], list) + + def test_error_response_parsing(self): + """Test error response is parsed correctly""" + response_json = { + "error": "Authentication failed", + "code": "AUTH_ERROR", + "status": 401 + } + + json_str = json.dumps(response_json) + parsed = json.loads(json_str) + + self.assertIn("error", parsed) + self.assertEqual(parsed["code"], "AUTH_ERROR") + self.assertEqual(parsed["status"], 401) + + def test_artifacts_response_parsing(self): + """Test response with artifacts is parsed correctly""" + response_json = { + "id": "exec-12345", + "status": "completed", + "stdout": "Generated output\n", + "artifacts": [ + { + "name": "output.png", + "content": "iVBORw0KGgo=", # base64 + "size": 12345 + } + ] + } + + json_str = json.dumps(response_json) + parsed = json.loads(json_str) + + self.assertIn("artifacts", parsed) + self.assertEqual(len(parsed["artifacts"]), 1) + self.assertEqual(parsed["artifacts"][0]["name"], "output.png") + + +class TestErrorHandling(unittest.TestCase): + """Test error handling components""" + + def test_missing_api_key_detection(self): + """Test detection of missing API keys""" + # Save current env + saved_pub = os.environ.get('UNSANDBOX_PUBLIC_KEY') + saved_sec = os.environ.get('UNSANDBOX_SECRET_KEY') + + try: + del os.environ['UNSANDBOX_PUBLIC_KEY'] + del os.environ['UNSANDBOX_SECRET_KEY'] + + # Check that keys are missing + public_key = os.environ.get('UNSANDBOX_PUBLIC_KEY') + secret_key = os.environ.get('UNSANDBOX_SECRET_KEY') + + self.assertIsNone(public_key) + self.assertIsNone(secret_key) + + finally: + # Restore env + if saved_pub: + os.environ['UNSANDBOX_PUBLIC_KEY'] = saved_pub + if saved_sec: + os.environ['UNSANDBOX_SECRET_KEY'] = saved_sec + + def test_invalid_json_handling(self): + """Test handling of invalid JSON responses""" + invalid_json = "not valid json {" + + with self.assertRaises(json.JSONDecodeError): + json.loads(invalid_json) + + def test_file_not_found_handling(self): + """Test handling of missing files""" + with self.assertRaises(FileNotFoundError): + with open("/nonexistent/file/path.py", 'r') as f: + f.read() + + def test_invalid_extension_handling(self): + """Test handling of unrecognized file extensions""" + EXT_MAP = {".py": "python", ".js": "javascript"} + + ext = ".xyz" + lang = EXT_MAP.get(ext) + self.assertIsNone(lang) + + +class TestFileProcessing(unittest.TestCase): + """Test file processing integration""" + + def test_multiple_files_encoding(self): + """Test encoding multiple files for request""" + files_data = [] + + # Create multiple temp files + temp_files = [] + for i in range(3): + with tempfile.NamedTemporaryFile(mode='w', suffix=f'.txt', delete=False) as f: + f.write(f"File {i} content") + temp_files.append(f.name) + + try: + for temp_path in temp_files: + with open(temp_path, 'rb') as f: + content = f.read() + files_data.append({ + "name": os.path.basename(temp_path), + "content": base64.b64encode(content).decode('utf-8') + }) + + # Verify all files encoded + self.assertEqual(len(files_data), 3) + + # Verify each file can be decoded + for i, file_data in enumerate(files_data): + decoded = base64.b64decode(file_data["content"]) + self.assertIn(f"File {i} content", decoded.decode('utf-8')) + + finally: + for temp_path in temp_files: + os.unlink(temp_path) + + def test_binary_file_encoding(self): + """Test encoding binary files""" + # Create binary content + binary_content = bytes([0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD]) + + with tempfile.NamedTemporaryFile(mode='wb', delete=False) as f: + f.write(binary_content) + temp_path = f.name + + try: + with open(temp_path, 'rb') as f: + content = f.read() + encoded = base64.b64encode(content).decode('utf-8') + + # Verify can decode back to original + decoded = base64.b64decode(encoded) + self.assertEqual(decoded, binary_content) + + finally: + os.unlink(temp_path) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/unit/run_all.sh b/tests/unit/run_all.sh new file mode 100755 index 0000000..7f4112c --- /dev/null +++ b/tests/unit/run_all.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Run all unit tests for UN CLI implementations +# These tests do NOT call the API - they test internal logic only + +set -o pipefail + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +cd "$(dirname "$0")" + +echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ UN CLI Inception - Unit Tests ║${NC}" +echo -e "${CYAN}║ Testing internal logic, no API calls ║${NC}" +echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" +echo "" + +passed=0 +failed=0 +skipped=0 + +run_test() { + local name="$1" + local cmd="$2" + local interpreter="$3" + + printf "%-20s" "$name" + + if [ -n "$interpreter" ] && ! command -v "$interpreter" &> /dev/null; then + echo -e "${YELLOW}SKIP${NC} ($interpreter not found)" + skipped=$((skipped + 1)) + return + fi + + if $cmd > /dev/null 2>&1; then + echo -e "${GREEN}PASS${NC}" + passed=$((passed + 1)) + else + echo -e "${RED}FAIL${NC}" + failed=$((failed + 1)) + fi +} + +echo -e "${CYAN}━━━ Scripting Languages ━━━${NC}" +run_test "Python" "python3 test_python.py" "python3" +run_test "JavaScript" "node test_javascript.js" "node" +run_test "Ruby" "ruby test_ruby.rb" "ruby" +run_test "Lua" "lua test_lua.lua" "lua" +run_test "Bash" "bash test_bash.sh" "bash" +echo "" + +echo -e "${CYAN}━━━ Systems Languages ━━━${NC}" +run_test "Go" "go run test_go.go" "go" +echo "" + +# Summary +echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" +echo "" +total=$((passed + failed + skipped)) +echo -e "Results: ${GREEN}$passed PASS${NC} | ${RED}$failed FAIL${NC} | ${YELLOW}$skipped SKIP${NC} | Total: $total" +echo "" + +if [ $failed -eq 0 ]; then + echo -e "${GREEN}All unit tests passed.${NC}" + exit 0 +else + echo -e "${RED}$failed test(s) failed.${NC}" + exit 1 +fi diff --git a/tests/unit/test_bash.sh b/tests/unit/test_bash.sh new file mode 100755 index 0000000..65df28c --- /dev/null +++ b/tests/unit/test_bash.sh @@ -0,0 +1,278 @@ +#!/bin/bash +# Unit tests for un.sh - tests internal functions without API calls + +set -o pipefail + +PASSED=0 +FAILED=0 + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +test_case() { + local name="$1" + shift + if "$@"; then + echo -e " ${GREEN}✓${NC} $name" + PASSED=$((PASSED + 1)) + else + echo -e " ${RED}✗${NC} $name" + FAILED=$((FAILED + 1)) + fi +} + +assert_equal() { + local actual="$1" + local expected="$2" + [ "$actual" = "$expected" ] +} + +assert_not_equal() { + local a="$1" + local b="$2" + [ "$a" != "$b" ] +} + +assert_contains() { + local str="$1" + local substr="$2" + [[ "$str" == *"$substr"* ]] +} + +# ============================================================================ +# Extension Mapping Tests +# ============================================================================ + +echo "" +echo "=== Extension Mapping Tests ===" + +# Extension to language mapping (from un.sh) +get_language() { + local ext="${1##*.}" + case ".$ext" in + .py) echo "python" ;; + .js) echo "javascript" ;; + .ts) echo "typescript" ;; + .rb) echo "ruby" ;; + .php) echo "php" ;; + .pl) echo "perl" ;; + .lua) echo "lua" ;; + .sh) echo "bash" ;; + .go) echo "go" ;; + .rs) echo "rust" ;; + .c) echo "c" ;; + .cpp|.cc|.cxx) echo "cpp" ;; + .java) echo "java" ;; + .kt) echo "kotlin" ;; + .cs) echo "csharp" ;; + .fs) echo "fsharp" ;; + .hs) echo "haskell" ;; + .ml) echo "ocaml" ;; + .clj) echo "clojure" ;; + .scm) echo "scheme" ;; + .lisp) echo "commonlisp" ;; + .erl) echo "erlang" ;; + .ex|.exs) echo "elixir" ;; + .jl) echo "julia" ;; + .r|.R) echo "r" ;; + .cr) echo "crystal" ;; + .d) echo "d" ;; + .nim) echo "nim" ;; + .zig) echo "zig" ;; + .v) echo "v" ;; + .dart) echo "dart" ;; + .groovy) echo "groovy" ;; + .scala) echo "scala" ;; + .f90|.f95) echo "fortran" ;; + .cob) echo "cobol" ;; + .pro) echo "prolog" ;; + .forth|.4th) echo "forth" ;; + .tcl) echo "tcl" ;; + .raku) echo "raku" ;; + .m) echo "objc" ;; + *) echo "" ;; + esac +} + +test_case "Python extension maps correctly" assert_equal "$(get_language script.py)" "python" +test_case "JavaScript extension maps correctly" assert_equal "$(get_language app.js)" "javascript" +test_case "TypeScript extension maps correctly" assert_equal "$(get_language app.ts)" "typescript" +test_case "Ruby extension maps correctly" assert_equal "$(get_language app.rb)" "ruby" +test_case "Go extension maps correctly" assert_equal "$(get_language main.go)" "go" +test_case "Rust extension maps correctly" assert_equal "$(get_language main.rs)" "rust" +test_case "C extension maps correctly" assert_equal "$(get_language main.c)" "c" +test_case "C++ extension maps correctly" assert_equal "$(get_language main.cpp)" "cpp" +test_case "Java extension maps correctly" assert_equal "$(get_language Main.java)" "java" +test_case "Kotlin extension maps correctly" assert_equal "$(get_language main.kt)" "kotlin" +test_case "Haskell extension maps correctly" assert_equal "$(get_language main.hs)" "haskell" +test_case "Elixir extension maps correctly" assert_equal "$(get_language main.ex)" "elixir" +test_case "Julia extension maps correctly" assert_equal "$(get_language main.jl)" "julia" + +# ============================================================================ +# HMAC Signature Tests +# ============================================================================ + +echo "" +echo "=== HMAC Signature Tests ===" + +# HMAC function (requires openssl) +hmac_sha256() { + local secret="$1" + local message="$2" + echo -n "$message" | openssl dgst -sha256 -hmac "$secret" | sed 's/^.* //' +} + +test_case "HMAC-SHA256 generates 64 character hex string" \ + bash -c 'sig=$(echo -n "test" | openssl dgst -sha256 -hmac "secret" | sed "s/^.* //"); [ ${#sig} -eq 64 ]' + +test_case "Same input produces same signature" \ + bash -c '[[ "$(echo -n "msg" | openssl dgst -sha256 -hmac "key" | sed "s/^.* //")" == "$(echo -n "msg" | openssl dgst -sha256 -hmac "key" | sed "s/^.* //")" ]]' + +SIG1=$(echo -n "msg" | openssl dgst -sha256 -hmac "key1" | sed 's/^.* //') +SIG2=$(echo -n "msg" | openssl dgst -sha256 -hmac "key2" | sed 's/^.* //') +test_case "Different secrets produce different signatures" assert_not_equal "$SIG1" "$SIG2" + +SIG3=$(echo -n "msg1" | openssl dgst -sha256 -hmac "key" | sed 's/^.* //') +SIG4=$(echo -n "msg2" | openssl dgst -sha256 -hmac "key" | sed 's/^.* //') +test_case "Different messages produce different signatures" assert_not_equal "$SIG3" "$SIG4" + +test_case "Signature format is timestamp:METHOD:path:body" \ + bash -c 'msg="1234567890:POST:/execute:{}"; [[ "$msg" == *":"* ]] && [[ $(echo "$msg" | tr -cd ":" | wc -c) -eq 3 ]]' + +# ============================================================================ +# Language Detection Tests +# ============================================================================ + +echo "" +echo "=== Language Detection Tests ===" + +detect_from_shebang() { + local first_line="$1" + if [[ "$first_line" == "#!"* ]]; then + case "$first_line" in + *python*) echo "python" ;; + *node*) echo "javascript" ;; + *ruby*) echo "ruby" ;; + *perl*) echo "perl" ;; + *bash*|*/sh*) echo "bash" ;; + *lua*) echo "lua" ;; + *php*) echo "php" ;; + *) echo "" ;; + esac + fi +} + +test_case "Python shebang detection" \ + assert_equal "$(detect_from_shebang '#!/usr/bin/env python3')" "python" + +test_case "Node shebang detection" \ + assert_equal "$(detect_from_shebang '#!/usr/bin/env node')" "javascript" + +test_case "Ruby shebang detection" \ + assert_equal "$(detect_from_shebang '#!/usr/bin/env ruby')" "ruby" + +test_case "Bash shebang detection" \ + assert_equal "$(detect_from_shebang '#!/bin/bash')" "bash" + +test_case "Sh shebang detection" \ + assert_equal "$(detect_from_shebang '#!/bin/sh')" "bash" + +# ============================================================================ +# Argument Parsing Tests +# ============================================================================ + +echo "" +echo "=== Argument Parsing Tests ===" + +parse_env_var() { + local arg="$1" + local key="${arg%%=*}" + local value="${arg#*=}" + echo "$key|$value" +} + +test_case "Parse -e KEY=VALUE format" \ + assert_equal "$(parse_env_var 'DEBUG=1')" "DEBUG|1" + +test_case "Parse -e KEY=VALUE with equals in value" \ + assert_equal "$(parse_env_var 'URL=https://example.com?foo=bar')" "URL|https://example.com?foo=bar" + +test_case "Valid network mode zerotrust" \ + bash -c '[[ "zerotrust" =~ ^(zerotrust|semitrusted)$ ]]' + +test_case "Valid network mode semitrusted" \ + bash -c '[[ "semitrusted" =~ ^(zerotrust|semitrusted)$ ]]' + +test_case "Invalid network mode rejected" \ + bash -c '! [[ "invalid" =~ ^(zerotrust|semitrusted)$ ]]' + +is_subcommand() { + local cmd="$1" + case "$cmd" in + session|service|key|restore) return 0 ;; + *) return 1 ;; + esac +} + +test_case "Subcommand detection - session" is_subcommand "session" +test_case "Subcommand detection - service" is_subcommand "service" +test_case "Subcommand detection - key" is_subcommand "key" +test_case "Not a subcommand - script.py" bash -c '! is_subcommand "script.py"' + +# ============================================================================ +# File Operations Tests +# ============================================================================ + +echo "" +echo "=== File Operations Tests ===" + +TMPFILE=$(mktemp --suffix=.py) +echo -n 'print("hello world")' > "$TMPFILE" + +test_case "Read text file" \ + assert_equal "$(cat "$TMPFILE")" 'print("hello world")' + +test_case "Base64 encoding/decoding" \ + bash -c '[[ "$(echo -n "hello" | base64 | base64 -d)" == "hello" ]]' + +test_case "Extract file basename" \ + assert_equal "$(basename /home/user/project/script.py)" "script.py" + +EXT="${TMPFILE##*.}" +test_case "Extract file extension" \ + assert_equal "$EXT" "py" + +rm -f "$TMPFILE" + +# ============================================================================ +# API Constants Tests +# ============================================================================ + +echo "" +echo "=== API Constants Tests ===" + +API_BASE="https://api.unsandbox.com" +PORTAL_BASE="https://unsandbox.com" + +test_case "API base URL starts with https" \ + bash -c '[[ "https://api.unsandbox.com" == https://* ]]' + +test_case "API base URL contains unsandbox.com" \ + assert_contains "$API_BASE" "unsandbox.com" + +test_case "Portal base URL starts with https" \ + bash -c '[[ "https://unsandbox.com" == https://* ]]' + +# ============================================================================ +# Summary +# ============================================================================ + +echo "" +echo "=== Summary ===" +echo "Passed: $PASSED" +echo "Failed: $FAILED" +echo "Total: $((PASSED + FAILED))" + +exit $((FAILED > 0 ? 1 : 0)) diff --git a/tests/unit/test_go.go b/tests/unit/test_go.go new file mode 100644 index 0000000..371ee7a --- /dev/null +++ b/tests/unit/test_go.go @@ -0,0 +1,376 @@ +// Unit tests for un.go - tests internal functions without API calls +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" +) + +var passed, failed int + +func test(name string, fn func() error) { + if err := fn(); err != nil { + fmt.Printf(" ✗ %s\n", name) + fmt.Printf(" %s\n", err) + failed++ + } else { + fmt.Printf(" ✓ %s\n", name) + passed++ + } +} + +func assertEqual(actual, expected string) error { + if actual != expected { + return fmt.Errorf("expected '%s' but got '%s'", expected, actual) + } + return nil +} + +func assertNotEqual(a, b string) error { + if a == b { + return fmt.Errorf("expected values to be different but both were '%s'", a) + } + return nil +} + +func assertContains(str, substr string) error { + if !strings.Contains(str, substr) { + return fmt.Errorf("expected '%s' to contain '%s'", str, substr) + } + return nil +} + +func assertTrue(val bool) error { + if !val { + return fmt.Errorf("expected true but got false") + } + return nil +} + +// Extension mapping (copied from un.go) +var extMap = map[string]string{ + ".py": "python", ".js": "javascript", ".ts": "typescript", + ".rb": "ruby", ".php": "php", ".pl": "perl", ".lua": "lua", + ".sh": "bash", ".go": "go", ".rs": "rust", ".c": "c", + ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", + ".java": "java", ".kt": "kotlin", ".cs": "csharp", ".fs": "fsharp", + ".hs": "haskell", ".ml": "ocaml", ".clj": "clojure", ".scm": "scheme", + ".lisp": "commonlisp", ".erl": "erlang", ".ex": "elixir", ".exs": "elixir", + ".jl": "julia", ".r": "r", ".R": "r", ".cr": "crystal", + ".d": "d", ".nim": "nim", ".zig": "zig", ".v": "v", + ".dart": "dart", ".groovy": "groovy", ".scala": "scala", + ".f90": "fortran", ".f95": "fortran", ".cob": "cobol", + ".pro": "prolog", ".forth": "forth", ".4th": "forth", + ".tcl": "tcl", ".raku": "raku", ".m": "objc", +} + +func main() { + // Extension Mapping Tests + fmt.Println("\n=== Extension Mapping Tests ===") + + test("Python extension maps correctly", func() error { + return assertEqual(extMap[".py"], "python") + }) + + test("JavaScript extensions map correctly", func() error { + if err := assertEqual(extMap[".js"], "javascript"); err != nil { + return err + } + return assertEqual(extMap[".ts"], "typescript") + }) + + test("Ruby extension maps correctly", func() error { + return assertEqual(extMap[".rb"], "ruby") + }) + + test("Go extension maps correctly", func() error { + return assertEqual(extMap[".go"], "go") + }) + + test("Rust extension maps correctly", func() error { + return assertEqual(extMap[".rs"], "rust") + }) + + test("C/C++ extensions map correctly", func() error { + if err := assertEqual(extMap[".c"], "c"); err != nil { + return err + } + if err := assertEqual(extMap[".cpp"], "cpp"); err != nil { + return err + } + if err := assertEqual(extMap[".cc"], "cpp"); err != nil { + return err + } + return assertEqual(extMap[".cxx"], "cpp") + }) + + test("JVM extensions map correctly", func() error { + if err := assertEqual(extMap[".java"], "java"); err != nil { + return err + } + if err := assertEqual(extMap[".kt"], "kotlin"); err != nil { + return err + } + return assertEqual(extMap[".groovy"], "groovy") + }) + + test("Functional language extensions map correctly", func() error { + if err := assertEqual(extMap[".hs"], "haskell"); err != nil { + return err + } + if err := assertEqual(extMap[".ml"], "ocaml"); err != nil { + return err + } + if err := assertEqual(extMap[".clj"], "clojure"); err != nil { + return err + } + return assertEqual(extMap[".erl"], "erlang") + }) + + // HMAC Signature Tests + fmt.Println("\n=== HMAC Signature Tests ===") + + test("HMAC-SHA256 generates 64 character hex string", func() error { + secret := "test-secret-key" + message := "1234567890:POST:/execute:{}" + + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(message)) + signature := hex.EncodeToString(h.Sum(nil)) + + if len(signature) != 64 { + return fmt.Errorf("expected length 64 but got %d", len(signature)) + } + return nil + }) + + test("Same input produces same signature", func() error { + secret := "test-secret-key" + message := "1234567890:POST:/execute:{}" + + h1 := hmac.New(sha256.New, []byte(secret)) + h1.Write([]byte(message)) + sig1 := hex.EncodeToString(h1.Sum(nil)) + + h2 := hmac.New(sha256.New, []byte(secret)) + h2.Write([]byte(message)) + sig2 := hex.EncodeToString(h2.Sum(nil)) + + return assertEqual(sig1, sig2) + }) + + test("Different secrets produce different signatures", func() error { + message := "1234567890:POST:/execute:{}" + + h1 := hmac.New(sha256.New, []byte("secret1")) + h1.Write([]byte(message)) + sig1 := hex.EncodeToString(h1.Sum(nil)) + + h2 := hmac.New(sha256.New, []byte("secret2")) + h2.Write([]byte(message)) + sig2 := hex.EncodeToString(h2.Sum(nil)) + + return assertNotEqual(sig1, sig2) + }) + + test("Different messages produce different signatures", func() error { + secret := "test-secret" + + h1 := hmac.New(sha256.New, []byte(secret)) + h1.Write([]byte("message1")) + sig1 := hex.EncodeToString(h1.Sum(nil)) + + h2 := hmac.New(sha256.New, []byte(secret)) + h2.Write([]byte("message2")) + sig2 := hex.EncodeToString(h2.Sum(nil)) + + return assertNotEqual(sig1, sig2) + }) + + test("Signature format is timestamp:METHOD:path:body", func() error { + timestamp := "1704067200" + method := "POST" + endpoint := "/execute" + body := `{"language":"python","code":"print(1)"}` + + message := fmt.Sprintf("%s:%s:%s:%s", timestamp, method, endpoint, body) + + // Verify format: starts with timestamp, has method and path + if err := assertTrue(strings.HasPrefix(message, timestamp)); err != nil { + return err + } + if err := assertContains(message, ":POST:"); err != nil { + return err + } + return assertContains(message, ":/execute:") + }) + + // Language Detection Tests + fmt.Println("\n=== Language Detection Tests ===") + + test("Detect language from .py extension", func() error { + filename := "script.py" + ext := strings.ToLower(filepath.Ext(filename)) + return assertEqual(extMap[ext], "python") + }) + + test("Detect language from .go extension", func() error { + filename := "main.go" + ext := strings.ToLower(filepath.Ext(filename)) + return assertEqual(extMap[ext], "go") + }) + + test("Python shebang detection", func() error { + content := "#!/usr/bin/env python3\nprint('hello')" + firstLine := strings.Split(content, "\n")[0] + + if err := assertTrue(strings.HasPrefix(firstLine, "#!")); err != nil { + return err + } + return assertContains(firstLine, "python") + }) + + test("Bash shebang detection", func() error { + content := "#!/bin/bash\necho hello" + firstLine := strings.Split(content, "\n")[0] + + if err := assertTrue(strings.HasPrefix(firstLine, "#!")); err != nil { + return err + } + return assertTrue(strings.Contains(firstLine, "bash") || strings.Contains(firstLine, "/sh")) + }) + + // Argument Parsing Tests + fmt.Println("\n=== Argument Parsing Tests ===") + + test("Parse -e KEY=VALUE format", func() error { + arg := "DEBUG=1" + parts := strings.SplitN(arg, "=", 2) + key := parts[0] + value := parts[1] + + if err := assertEqual(key, "DEBUG"); err != nil { + return err + } + return assertEqual(value, "1") + }) + + test("Parse -e KEY=VALUE with equals in value", func() error { + arg := "URL=https://example.com?foo=bar" + parts := strings.SplitN(arg, "=", 2) + key := parts[0] + value := parts[1] + + if err := assertEqual(key, "URL"); err != nil { + return err + } + return assertEqual(value, "https://example.com?foo=bar") + }) + + test("Valid network modes", func() error { + validModes := map[string]bool{"zerotrust": true, "semitrusted": true} + + if !validModes["zerotrust"] { + return fmt.Errorf("zerotrust should be valid") + } + if !validModes["semitrusted"] { + return fmt.Errorf("semitrusted should be valid") + } + if validModes["invalid"] { + return fmt.Errorf("invalid should not be valid") + } + return nil + }) + + test("Subcommand detection", func() error { + args := []string{"session", "--shell", "python3"} + subcommands := map[string]bool{"session": true, "service": true, "key": true, "restore": true} + + var subcommand string + if len(args) > 0 && subcommands[args[0]] { + subcommand = args[0] + } + + return assertEqual(subcommand, "session") + }) + + // File Operations Tests + fmt.Println("\n=== File Operations Tests ===") + + test("Read text file", func() error { + tmpfile, err := os.CreateTemp("", "test_un_go_*.py") + if err != nil { + return err + } + defer os.Remove(tmpfile.Name()) + + content := "print('hello world')" + if _, err := tmpfile.WriteString(content); err != nil { + return err + } + tmpfile.Close() + + data, err := os.ReadFile(tmpfile.Name()) + if err != nil { + return err + } + + return assertEqual(string(data), content) + }) + + test("Base64 encoding/decoding", func() error { + content := "print('hello world')" + encoded := base64.StdEncoding.EncodeToString([]byte(content)) + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return err + } + + return assertEqual(string(decoded), content) + }) + + test("Extract file basename", func() error { + path := "/home/user/project/script.py" + basename := filepath.Base(path) + return assertEqual(basename, "script.py") + }) + + test("Extract file extension", func() error { + path := "/home/user/project/script.py" + ext := filepath.Ext(path) + return assertEqual(ext, ".py") + }) + + // API Constants Tests + fmt.Println("\n=== API Constants Tests ===") + + test("API base URL format", func() error { + apiBase := "https://api.unsandbox.com" + + if err := assertTrue(strings.HasPrefix(apiBase, "https://")); err != nil { + return err + } + return assertContains(apiBase, "unsandbox.com") + }) + + test("Portal base URL format", func() error { + portalBase := "https://unsandbox.com" + return assertTrue(strings.HasPrefix(portalBase, "https://")) + }) + + // Summary + fmt.Println("\n=== Summary ===") + fmt.Printf("Passed: %d\n", passed) + fmt.Printf("Failed: %d\n", failed) + fmt.Printf("Total: %d\n", passed+failed) + + if failed > 0 { + os.Exit(1) + } +} diff --git a/tests/unit/test_javascript.js b/tests/unit/test_javascript.js new file mode 100644 index 0000000..06de1b8 --- /dev/null +++ b/tests/unit/test_javascript.js @@ -0,0 +1,355 @@ +#!/usr/bin/env node +/** + * Unit tests for un.js - tests internal functions without API calls + */ + +const crypto = require('crypto'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const assert = require('assert'); + +// Test counters +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (e) { + console.log(` ✗ ${name}`); + console.log(` ${e.message}`); + failed++; + } +} + +function assertEqual(actual, expected, msg = '') { + if (actual !== expected) { + throw new Error(`Expected "${expected}" but got "${actual}" ${msg}`); + } +} + +function assertIncludes(str, substr) { + if (!str.includes(substr)) { + throw new Error(`Expected "${str}" to include "${substr}"`); + } +} + +function assertNotEqual(a, b) { + if (a === b) { + throw new Error(`Expected values to be different but both were "${a}"`); + } +} + +// ============================================================================ +// Extension Mapping Tests +// ============================================================================ + +console.log('\n=== Extension Mapping Tests ==='); + +const EXT_MAP = { + ".py": "python", ".js": "javascript", ".ts": "typescript", + ".rb": "ruby", ".php": "php", ".pl": "perl", ".lua": "lua", + ".sh": "bash", ".go": "go", ".rs": "rust", ".c": "c", + ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", + ".java": "java", ".kt": "kotlin", ".cs": "csharp", ".fs": "fsharp", + ".hs": "haskell", ".ml": "ocaml", ".clj": "clojure", ".scm": "scheme", + ".lisp": "commonlisp", ".erl": "erlang", ".ex": "elixir", ".exs": "elixir", + ".jl": "julia", ".r": "r", ".R": "r", ".cr": "crystal", + ".d": "d", ".nim": "nim", ".zig": "zig", ".v": "v", + ".dart": "dart", ".groovy": "groovy", ".scala": "scala", + ".f90": "fortran", ".f95": "fortran", ".cob": "cobol", + ".pro": "prolog", ".forth": "forth", ".4th": "forth", + ".tcl": "tcl", ".raku": "raku", ".m": "objc", +}; + +test('Python extension maps correctly', () => { + assertEqual(EXT_MAP['.py'], 'python'); +}); + +test('JavaScript extensions map correctly', () => { + assertEqual(EXT_MAP['.js'], 'javascript'); + assertEqual(EXT_MAP['.ts'], 'typescript'); +}); + +test('Ruby extension maps correctly', () => { + assertEqual(EXT_MAP['.rb'], 'ruby'); +}); + +test('Go extension maps correctly', () => { + assertEqual(EXT_MAP['.go'], 'go'); +}); + +test('Rust extension maps correctly', () => { + assertEqual(EXT_MAP['.rs'], 'rust'); +}); + +test('C/C++ extensions map correctly', () => { + assertEqual(EXT_MAP['.c'], 'c'); + assertEqual(EXT_MAP['.cpp'], 'cpp'); + assertEqual(EXT_MAP['.cc'], 'cpp'); + assertEqual(EXT_MAP['.cxx'], 'cpp'); +}); + +test('JVM extensions map correctly', () => { + assertEqual(EXT_MAP['.java'], 'java'); + assertEqual(EXT_MAP['.kt'], 'kotlin'); + assertEqual(EXT_MAP['.groovy'], 'groovy'); + assertEqual(EXT_MAP['.scala'], 'scala'); +}); + +test('.NET extensions map correctly', () => { + assertEqual(EXT_MAP['.cs'], 'csharp'); + assertEqual(EXT_MAP['.fs'], 'fsharp'); +}); + +test('Functional language extensions map correctly', () => { + assertEqual(EXT_MAP['.hs'], 'haskell'); + assertEqual(EXT_MAP['.ml'], 'ocaml'); + assertEqual(EXT_MAP['.clj'], 'clojure'); + assertEqual(EXT_MAP['.scm'], 'scheme'); + assertEqual(EXT_MAP['.lisp'], 'commonlisp'); + assertEqual(EXT_MAP['.erl'], 'erlang'); + assertEqual(EXT_MAP['.ex'], 'elixir'); + assertEqual(EXT_MAP['.exs'], 'elixir'); +}); + +test('Scientific language extensions map correctly', () => { + assertEqual(EXT_MAP['.jl'], 'julia'); + assertEqual(EXT_MAP['.r'], 'r'); + assertEqual(EXT_MAP['.R'], 'r'); + assertEqual(EXT_MAP['.f90'], 'fortran'); + assertEqual(EXT_MAP['.f95'], 'fortran'); +}); + +test('Systems language extensions map correctly', () => { + assertEqual(EXT_MAP['.d'], 'd'); + assertEqual(EXT_MAP['.nim'], 'nim'); + assertEqual(EXT_MAP['.zig'], 'zig'); + assertEqual(EXT_MAP['.v'], 'v'); + assertEqual(EXT_MAP['.cr'], 'crystal'); + assertEqual(EXT_MAP['.dart'], 'dart'); +}); + +test('Legacy/exotic extensions map correctly', () => { + assertEqual(EXT_MAP['.cob'], 'cobol'); + assertEqual(EXT_MAP['.pro'], 'prolog'); + assertEqual(EXT_MAP['.forth'], 'forth'); + assertEqual(EXT_MAP['.4th'], 'forth'); + assertEqual(EXT_MAP['.tcl'], 'tcl'); + assertEqual(EXT_MAP['.raku'], 'raku'); + assertEqual(EXT_MAP['.m'], 'objc'); +}); + +// ============================================================================ +// HMAC Signature Tests +// ============================================================================ + +console.log('\n=== HMAC Signature Tests ==='); + +test('HMAC-SHA256 generates 64 character hex string', () => { + const secret = 'test-secret-key'; + const message = '1234567890:POST:/execute:{}'; + + const signature = crypto.createHmac('sha256', secret) + .update(message) + .digest('hex'); + + assertEqual(signature.length, 64); +}); + +test('Same input produces same signature', () => { + const secret = 'test-secret-key'; + const message = '1234567890:POST:/execute:{}'; + + const sig1 = crypto.createHmac('sha256', secret).update(message).digest('hex'); + const sig2 = crypto.createHmac('sha256', secret).update(message).digest('hex'); + + assertEqual(sig1, sig2); +}); + +test('Different secrets produce different signatures', () => { + const message = '1234567890:POST:/execute:{}'; + + const sig1 = crypto.createHmac('sha256', 'secret1').update(message).digest('hex'); + const sig2 = crypto.createHmac('sha256', 'secret2').update(message).digest('hex'); + + assertNotEqual(sig1, sig2); +}); + +test('Different messages produce different signatures', () => { + const secret = 'test-secret'; + + const sig1 = crypto.createHmac('sha256', secret).update('message1').digest('hex'); + const sig2 = crypto.createHmac('sha256', secret).update('message2').digest('hex'); + + assertNotEqual(sig1, sig2); +}); + +test('Signature format is timestamp:METHOD:path:body', () => { + const timestamp = '1704067200'; + const method = 'POST'; + const endpoint = '/execute'; + const body = '{"language":"python","code":"print(1)"}'; + + const message = `${timestamp}:${method}:${endpoint}:${body}`; + + // Verify format: starts with timestamp, has method and path + assertEqual(message.startsWith(timestamp), true); + assertIncludes(message, ':POST:'); + assertIncludes(message, ':/execute:'); +}); + +// ============================================================================ +// Language Detection Tests +// ============================================================================ + +console.log('\n=== Language Detection Tests ==='); + +test('Detect language from .py extension', () => { + const filename = 'script.py'; + const ext = path.extname(filename).toLowerCase(); + assertEqual(EXT_MAP[ext], 'python'); +}); + +test('Detect language from .js extension', () => { + const filename = 'app.js'; + const ext = path.extname(filename).toLowerCase(); + assertEqual(EXT_MAP[ext], 'javascript'); +}); + +test('Python shebang detection', () => { + const content = '#!/usr/bin/env python3\nprint("hello")'; + const firstLine = content.split('\n')[0]; + + assertEqual(firstLine.startsWith('#!'), true); + assertEqual(firstLine.includes('python'), true); +}); + +test('Node shebang detection', () => { + const content = '#!/usr/bin/env node\nconsole.log("hello")'; + const firstLine = content.split('\n')[0]; + + assertEqual(firstLine.startsWith('#!'), true); + assertEqual(firstLine.includes('node'), true); +}); + +test('Bash shebang detection', () => { + const content = '#!/bin/bash\necho hello'; + const firstLine = content.split('\n')[0]; + + assertEqual(firstLine.startsWith('#!'), true); + assertEqual(firstLine.includes('bash') || firstLine.includes('/sh'), true); +}); + +// ============================================================================ +// Argument Parsing Tests +// ============================================================================ + +console.log('\n=== Argument Parsing Tests ==='); + +test('Parse -e KEY=VALUE format', () => { + const arg = 'DEBUG=1'; + const [key, ...rest] = arg.split('='); + const value = rest.join('='); + + assertEqual(key, 'DEBUG'); + assertEqual(value, '1'); +}); + +test('Parse -e KEY=VALUE with equals in value', () => { + const arg = 'URL=https://example.com?foo=bar'; + const [key, ...rest] = arg.split('='); + const value = rest.join('='); + + assertEqual(key, 'URL'); + assertEqual(value, 'https://example.com?foo=bar'); +}); + +test('Valid network modes', () => { + const validModes = ['zerotrust', 'semitrusted']; + + assertEqual(validModes.includes('zerotrust'), true); + assertEqual(validModes.includes('semitrusted'), true); + assertEqual(validModes.includes('invalid'), false); +}); + +test('Subcommand detection', () => { + const args = ['session', '--shell', 'python3']; + const subcommands = ['session', 'service', 'key', 'restore']; + const subcommand = subcommands.includes(args[0]) ? args[0] : null; + + assertEqual(subcommand, 'session'); +}); + +// ============================================================================ +// File Operations Tests +// ============================================================================ + +console.log('\n=== File Operations Tests ==='); + +test('Read text file', () => { + const tempFile = path.join(os.tmpdir(), 'test_un_js_' + Date.now() + '.py'); + fs.writeFileSync(tempFile, 'print("hello world")'); + + try { + const content = fs.readFileSync(tempFile, 'utf-8'); + assertEqual(content, 'print("hello world")'); + } finally { + fs.unlinkSync(tempFile); + } +}); + +test('Base64 encoding/decoding', () => { + const content = 'print("hello world")'; + const encoded = Buffer.from(content).toString('base64'); + const decoded = Buffer.from(encoded, 'base64').toString(); + + assertEqual(decoded, content); +}); + +test('Extract file basename', () => { + const filepath = '/home/user/project/script.py'; + const basename = path.basename(filepath); + + assertEqual(basename, 'script.py'); +}); + +test('Extract file extension', () => { + const filepath = '/home/user/project/script.py'; + const ext = path.extname(filepath); + + assertEqual(ext, '.py'); +}); + +// ============================================================================ +// API Constants Tests +// ============================================================================ + +console.log('\n=== API Constants Tests ==='); + +test('API base URL format', () => { + const API_BASE = 'https://api.unsandbox.com'; + + assertEqual(API_BASE.startsWith('https://'), true); + assertIncludes(API_BASE, 'unsandbox.com'); +}); + +test('Portal base URL format', () => { + const PORTAL_BASE = 'https://unsandbox.com'; + + assertEqual(PORTAL_BASE.startsWith('https://'), true); +}); + +// ============================================================================ +// Summary +// ============================================================================ + +console.log('\n=== Summary ==='); +console.log(`Passed: ${passed}`); +console.log(`Failed: ${failed}`); +console.log(`Total: ${passed + failed}`); + +process.exit(failed > 0 ? 1 : 0); diff --git a/tests/unit/test_lua.lua b/tests/unit/test_lua.lua new file mode 100755 index 0000000..fc58d13 --- /dev/null +++ b/tests/unit/test_lua.lua @@ -0,0 +1,319 @@ +#!/usr/bin/env lua +-- Unit tests for un.lua - tests internal functions without API calls + +local passed = 0 +local failed = 0 + +local function test(name, fn) + local ok, err = pcall(fn) + if ok then + print(" ✓ " .. name) + passed = passed + 1 + else + print(" ✗ " .. name) + print(" " .. tostring(err)) + failed = failed + 1 + end +end + +local function assert_equal(actual, expected, msg) + if actual ~= expected then + error(string.format("Expected '%s' but got '%s' %s", tostring(expected), tostring(actual), msg or "")) + end +end + +local function assert_not_equal(a, b) + if a == b then + error(string.format("Expected values to be different but both were '%s'", tostring(a))) + end +end + +local function assert_contains(str, substr) + if not string.find(str, substr, 1, true) then + error(string.format("Expected '%s' to contain '%s'", str, substr)) + end +end + +local function assert_true(val) + if not val then + error("Expected true but got false") + end +end + +-- Extension mapping (from un.lua) +local ext_map = { + [".py"] = "python", [".js"] = "javascript", [".ts"] = "typescript", + [".rb"] = "ruby", [".php"] = "php", [".pl"] = "perl", [".lua"] = "lua", + [".sh"] = "bash", [".go"] = "go", [".rs"] = "rust", [".c"] = "c", + [".cpp"] = "cpp", [".cc"] = "cpp", [".cxx"] = "cpp", + [".java"] = "java", [".kt"] = "kotlin", [".cs"] = "csharp", [".fs"] = "fsharp", + [".hs"] = "haskell", [".ml"] = "ocaml", [".clj"] = "clojure", [".scm"] = "scheme", + [".lisp"] = "commonlisp", [".erl"] = "erlang", [".ex"] = "elixir", [".exs"] = "elixir", + [".jl"] = "julia", [".r"] = "r", [".R"] = "r", [".cr"] = "crystal", + [".d"] = "d", [".nim"] = "nim", [".zig"] = "zig", [".v"] = "v", + [".dart"] = "dart", [".groovy"] = "groovy", [".scala"] = "scala", + [".f90"] = "fortran", [".f95"] = "fortran", [".cob"] = "cobol", + [".pro"] = "prolog", [".forth"] = "forth", [".4th"] = "forth", + [".tcl"] = "tcl", [".raku"] = "raku", [".m"] = "objc", +} + +-- ============================================================================ +-- Extension Mapping Tests +-- ============================================================================ + +print("\n=== Extension Mapping Tests ===") + +test("Python extension maps correctly", function() + assert_equal(ext_map[".py"], "python") +end) + +test("JavaScript extensions map correctly", function() + assert_equal(ext_map[".js"], "javascript") + assert_equal(ext_map[".ts"], "typescript") +end) + +test("Ruby extension maps correctly", function() + assert_equal(ext_map[".rb"], "ruby") +end) + +test("Go extension maps correctly", function() + assert_equal(ext_map[".go"], "go") +end) + +test("Rust extension maps correctly", function() + assert_equal(ext_map[".rs"], "rust") +end) + +test("C/C++ extensions map correctly", function() + assert_equal(ext_map[".c"], "c") + assert_equal(ext_map[".cpp"], "cpp") + assert_equal(ext_map[".cc"], "cpp") + assert_equal(ext_map[".cxx"], "cpp") +end) + +test("Lua extension maps correctly", function() + assert_equal(ext_map[".lua"], "lua") +end) + +test("JVM extensions map correctly", function() + assert_equal(ext_map[".java"], "java") + assert_equal(ext_map[".kt"], "kotlin") + assert_equal(ext_map[".groovy"], "groovy") +end) + +test("Functional language extensions map correctly", function() + assert_equal(ext_map[".hs"], "haskell") + assert_equal(ext_map[".ml"], "ocaml") + assert_equal(ext_map[".clj"], "clojure") + assert_equal(ext_map[".erl"], "erlang") +end) + +-- ============================================================================ +-- HMAC Signature Tests (using openssl via shell) +-- ============================================================================ + +print("\n=== HMAC Signature Tests ===") + +local function hmac_sha256(secret, message) + local cmd = string.format("echo -n '%s' | openssl dgst -sha256 -hmac '%s' 2>/dev/null | sed 's/^.* //'", + message:gsub("'", "'\\''"), secret:gsub("'", "'\\''")) + local handle = io.popen(cmd) + if handle then + local result = handle:read("*a"):gsub("%s+$", "") + handle:close() + return result + end + return nil +end + +test("HMAC-SHA256 generates 64 character hex string", function() + local sig = hmac_sha256("test-secret", "test-message") + if sig then + assert_equal(#sig, 64) + end +end) + +test("Same input produces same signature", function() + local sig1 = hmac_sha256("key", "message") + local sig2 = hmac_sha256("key", "message") + if sig1 and sig2 then + assert_equal(sig1, sig2) + end +end) + +test("Different secrets produce different signatures", function() + local sig1 = hmac_sha256("key1", "message") + local sig2 = hmac_sha256("key2", "message") + if sig1 and sig2 then + assert_not_equal(sig1, sig2) + end +end) + +test("Different messages produce different signatures", function() + local sig1 = hmac_sha256("key", "message1") + local sig2 = hmac_sha256("key", "message2") + if sig1 and sig2 then + assert_not_equal(sig1, sig2) + end +end) + +test("Signature format is timestamp:METHOD:path:body", function() + local timestamp = "1704067200" + local method = "POST" + local endpoint = "/execute" + local body = '{"language":"python","code":"print(1)"}' + + local message = timestamp .. ":" .. method .. ":" .. endpoint .. ":" .. body + + assert_contains(message, ":") + local count = 0 + for _ in message:gmatch(":") do count = count + 1 end + assert_equal(count, 3) + assert_true(message:sub(1, #timestamp) == timestamp) +end) + +-- ============================================================================ +-- Language Detection Tests +-- ============================================================================ + +print("\n=== Language Detection Tests ===") + +local function get_extension(filename) + return filename:match("%.([^%.]+)$") +end + +local function detect_from_shebang(first_line) + if first_line:sub(1, 2) == "#!" then + if first_line:find("python") then return "python" end + if first_line:find("node") then return "javascript" end + if first_line:find("ruby") then return "ruby" end + if first_line:find("perl") then return "perl" end + if first_line:find("bash") or first_line:find("/sh") then return "bash" end + if first_line:find("lua") then return "lua" end + if first_line:find("php") then return "php" end + end + return nil +end + +test("Detect language from .py extension", function() + local ext = "." .. get_extension("script.py") + assert_equal(ext_map[ext], "python") +end) + +test("Detect language from .lua extension", function() + local ext = "." .. get_extension("script.lua") + assert_equal(ext_map[ext], "lua") +end) + +test("Python shebang detection", function() + assert_equal(detect_from_shebang("#!/usr/bin/env python3"), "python") +end) + +test("Node shebang detection", function() + assert_equal(detect_from_shebang("#!/usr/bin/env node"), "javascript") +end) + +test("Lua shebang detection", function() + assert_equal(detect_from_shebang("#!/usr/bin/env lua"), "lua") +end) + +test("Bash shebang detection", function() + assert_equal(detect_from_shebang("#!/bin/bash"), "bash") +end) + +-- ============================================================================ +-- Argument Parsing Tests +-- ============================================================================ + +print("\n=== Argument Parsing Tests ===") + +local function parse_env_var(arg) + local key, value = arg:match("^([^=]+)=(.*)$") + return key, value +end + +test("Parse -e KEY=VALUE format", function() + local key, value = parse_env_var("DEBUG=1") + assert_equal(key, "DEBUG") + assert_equal(value, "1") +end) + +test("Parse -e KEY=VALUE with equals in value", function() + local key, value = parse_env_var("URL=https://example.com?foo=bar") + assert_equal(key, "URL") + assert_equal(value, "https://example.com?foo=bar") +end) + +test("Valid network modes", function() + local valid_modes = { zerotrust = true, semitrusted = true } + assert_true(valid_modes["zerotrust"]) + assert_true(valid_modes["semitrusted"]) + assert_true(not valid_modes["invalid"]) +end) + +test("Subcommand detection", function() + local subcommands = { session = true, service = true, key = true, restore = true } + assert_true(subcommands["session"]) + assert_true(subcommands["service"]) + assert_true(not subcommands["script.py"]) +end) + +-- ============================================================================ +-- File Operations Tests +-- ============================================================================ + +print("\n=== File Operations Tests ===") + +test("Read text file", function() + local tmpname = os.tmpname() .. ".py" + local f = io.open(tmpname, "w") + f:write('print("hello world")') + f:close() + + f = io.open(tmpname, "r") + local content = f:read("*a") + f:close() + os.remove(tmpname) + + assert_equal(content, 'print("hello world")') +end) + +test("Extract file basename", function() + local path = "/home/user/project/script.py" + local basename = path:match("([^/]+)$") + assert_equal(basename, "script.py") +end) + +test("Extract file extension", function() + local path = "/home/user/project/script.py" + local ext = path:match("%.([^%.]+)$") + assert_equal(ext, "py") +end) + +-- ============================================================================ +-- API Constants Tests +-- ============================================================================ + +print("\n=== API Constants Tests ===") + +test("API base URL format", function() + local api_base = "https://api.unsandbox.com" + assert_true(api_base:sub(1, 8) == "https://") + assert_contains(api_base, "unsandbox.com") +end) + +test("Portal base URL format", function() + local portal_base = "https://unsandbox.com" + assert_true(portal_base:sub(1, 8) == "https://") +end) + +-- ============================================================================ +-- Summary +-- ============================================================================ + +print("\n=== Summary ===") +print("Passed: " .. passed) +print("Failed: " .. failed) +print("Total: " .. (passed + failed)) + +os.exit(failed > 0 and 1 or 0) diff --git a/tests/unit/test_python.py b/tests/unit/test_python.py new file mode 100755 index 0000000..12d92c5 --- /dev/null +++ b/tests/unit/test_python.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +""" +Unit tests for un.py - tests internal functions without API calls +""" + +import sys +import os +import hmac +import hashlib +import tempfile +import unittest + +# Add parent directory to path to import un module functions +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) + +# We need to mock the API key check before importing +os.environ['UNSANDBOX_PUBLIC_KEY'] = 'test-public-key' +os.environ['UNSANDBOX_SECRET_KEY'] = 'test-secret-key' + + +class TestExtensionMapping(unittest.TestCase): + """Test the EXT_MAP extension to language mapping""" + + def setUp(self): + # Import here after env is set + import importlib.util + spec = importlib.util.spec_from_file_location("un", os.path.join(os.path.dirname(__file__), '../../un.py')) + self.un = importlib.util.module_from_spec(spec) + # Don't execute the module, just load the constants + with open(os.path.join(os.path.dirname(__file__), '../../un.py')) as f: + content = f.read() + # Extract EXT_MAP + exec(compile(content.split('def get_api_keys')[0], 'un.py', 'exec'), self.un.__dict__) + + def test_python_extensions(self): + self.assertEqual(self.un.EXT_MAP['.py'], 'python') + + def test_javascript_extensions(self): + self.assertEqual(self.un.EXT_MAP['.js'], 'javascript') + self.assertEqual(self.un.EXT_MAP['.ts'], 'typescript') + + def test_ruby_extension(self): + self.assertEqual(self.un.EXT_MAP['.rb'], 'ruby') + + def test_go_extension(self): + self.assertEqual(self.un.EXT_MAP['.go'], 'go') + + def test_rust_extension(self): + self.assertEqual(self.un.EXT_MAP['.rs'], 'rust') + + def test_c_extensions(self): + self.assertEqual(self.un.EXT_MAP['.c'], 'c') + self.assertEqual(self.un.EXT_MAP['.cpp'], 'cpp') + self.assertEqual(self.un.EXT_MAP['.cc'], 'cpp') + self.assertEqual(self.un.EXT_MAP['.cxx'], 'cpp') + + def test_jvm_extensions(self): + self.assertEqual(self.un.EXT_MAP['.java'], 'java') + self.assertEqual(self.un.EXT_MAP['.kt'], 'kotlin') + self.assertEqual(self.un.EXT_MAP['.groovy'], 'groovy') + self.assertEqual(self.un.EXT_MAP['.scala'], 'scala') + + def test_dotnet_extensions(self): + self.assertEqual(self.un.EXT_MAP['.cs'], 'csharp') + self.assertEqual(self.un.EXT_MAP['.fs'], 'fsharp') + + def test_functional_extensions(self): + self.assertEqual(self.un.EXT_MAP['.hs'], 'haskell') + self.assertEqual(self.un.EXT_MAP['.ml'], 'ocaml') + self.assertEqual(self.un.EXT_MAP['.clj'], 'clojure') + self.assertEqual(self.un.EXT_MAP['.scm'], 'scheme') + self.assertEqual(self.un.EXT_MAP['.lisp'], 'commonlisp') + self.assertEqual(self.un.EXT_MAP['.erl'], 'erlang') + self.assertEqual(self.un.EXT_MAP['.ex'], 'elixir') + self.assertEqual(self.un.EXT_MAP['.exs'], 'elixir') + + def test_scientific_extensions(self): + self.assertEqual(self.un.EXT_MAP['.jl'], 'julia') + self.assertEqual(self.un.EXT_MAP['.r'], 'r') + self.assertEqual(self.un.EXT_MAP['.R'], 'r') + self.assertEqual(self.un.EXT_MAP['.f90'], 'fortran') + self.assertEqual(self.un.EXT_MAP['.f95'], 'fortran') + + def test_exotic_extensions(self): + self.assertEqual(self.un.EXT_MAP['.d'], 'd') + self.assertEqual(self.un.EXT_MAP['.nim'], 'nim') + self.assertEqual(self.un.EXT_MAP['.zig'], 'zig') + self.assertEqual(self.un.EXT_MAP['.v'], 'v') + self.assertEqual(self.un.EXT_MAP['.cr'], 'crystal') + self.assertEqual(self.un.EXT_MAP['.dart'], 'dart') + + def test_legacy_extensions(self): + self.assertEqual(self.un.EXT_MAP['.cob'], 'cobol') + self.assertEqual(self.un.EXT_MAP['.pro'], 'prolog') + self.assertEqual(self.un.EXT_MAP['.forth'], 'forth') + self.assertEqual(self.un.EXT_MAP['.4th'], 'forth') + + def test_other_extensions(self): + self.assertEqual(self.un.EXT_MAP['.tcl'], 'tcl') + self.assertEqual(self.un.EXT_MAP['.raku'], 'raku') + self.assertEqual(self.un.EXT_MAP['.m'], 'objc') + self.assertEqual(self.un.EXT_MAP['.lua'], 'lua') + self.assertEqual(self.un.EXT_MAP['.pl'], 'perl') + self.assertEqual(self.un.EXT_MAP['.php'], 'php') + self.assertEqual(self.un.EXT_MAP['.sh'], 'bash') + + +class TestHMACSignature(unittest.TestCase): + """Test HMAC signature generation""" + + def test_hmac_sha256_basic(self): + """Test basic HMAC-SHA256 generation""" + secret = "test-secret-key" + message = "1234567890:POST:/execute:{}" + + expected = hmac.new( + secret.encode(), + message.encode(), + hashlib.sha256 + ).hexdigest() + + # Verify we can generate the same signature + actual = hmac.new( + secret.encode(), + message.encode(), + hashlib.sha256 + ).hexdigest() + + self.assertEqual(expected, actual) + self.assertEqual(len(actual), 64) # SHA256 hex is 64 chars + + def test_hmac_different_secrets(self): + """Test that different secrets produce different signatures""" + message = "1234567890:POST:/execute:{}" + + sig1 = hmac.new(b"secret1", message.encode(), hashlib.sha256).hexdigest() + sig2 = hmac.new(b"secret2", message.encode(), hashlib.sha256).hexdigest() + + self.assertNotEqual(sig1, sig2) + + def test_hmac_different_messages(self): + """Test that different messages produce different signatures""" + secret = b"test-secret" + + sig1 = hmac.new(secret, b"message1", hashlib.sha256).hexdigest() + sig2 = hmac.new(secret, b"message2", hashlib.sha256).hexdigest() + + self.assertNotEqual(sig1, sig2) + + def test_signature_format(self): + """Test the signature input format: timestamp:METHOD:path:body""" + timestamp = "1704067200" + method = "POST" + path = "/execute" + body = '{"language":"python","code":"print(1)"}' + + message = f"{timestamp}:{method}:{path}:{body}" + + # Verify format: starts with timestamp, has method and path + self.assertTrue(message.startswith(timestamp)) + self.assertIn(":POST:", message) + self.assertIn(":/execute:", message) + + +class TestLanguageDetection(unittest.TestCase): + """Test language detection from file extensions and shebangs""" + + def test_detect_from_extension(self): + """Test detection from file extension""" + # Create a temp file with .py extension + with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as f: + f.write(b'print("hello")') + temp_path = f.name + + try: + ext = os.path.splitext(temp_path)[1].lower() + # Simulate EXT_MAP lookup + EXT_MAP = {'.py': 'python', '.js': 'javascript', '.rb': 'ruby'} + self.assertEqual(EXT_MAP.get(ext), 'python') + finally: + os.unlink(temp_path) + + def test_detect_from_shebang_python(self): + """Test detection from python shebang""" + content = "#!/usr/bin/env python3\nprint('hello')" + first_line = content.split('\n')[0] + + self.assertTrue(first_line.startswith('#!')) + self.assertIn('python', first_line) + + def test_detect_from_shebang_node(self): + """Test detection from node shebang""" + content = "#!/usr/bin/env node\nconsole.log('hello')" + first_line = content.split('\n')[0] + + self.assertTrue(first_line.startswith('#!')) + self.assertIn('node', first_line) + + def test_detect_from_shebang_bash(self): + """Test detection from bash shebang""" + content = "#!/bin/bash\necho hello" + first_line = content.split('\n')[0] + + self.assertTrue(first_line.startswith('#!')) + self.assertTrue('bash' in first_line or '/sh' in first_line) + + +class TestArgumentParsing(unittest.TestCase): + """Test command-line argument patterns""" + + def test_env_var_format(self): + """Test -e KEY=VALUE parsing""" + arg = "DEBUG=1" + key, value = arg.split('=', 1) + + self.assertEqual(key, "DEBUG") + self.assertEqual(value, "1") + + def test_env_var_with_equals_in_value(self): + """Test -e KEY=VALUE=WITH=EQUALS parsing""" + arg = "URL=https://example.com?foo=bar" + key, value = arg.split('=', 1) + + self.assertEqual(key, "URL") + self.assertEqual(value, "https://example.com?foo=bar") + + def test_network_mode_values(self): + """Test valid network mode values""" + valid_modes = ['zerotrust', 'semitrusted'] + + self.assertIn('zerotrust', valid_modes) + self.assertIn('semitrusted', valid_modes) + + def test_subcommand_detection(self): + """Test subcommand detection""" + args = ['session', '--shell', 'python3'] + subcommand = args[0] if args and args[0] in ['session', 'service', 'key', 'restore'] else None + + self.assertEqual(subcommand, 'session') + + +class TestFileOperations(unittest.TestCase): + """Test file reading and encoding""" + + def test_read_text_file(self): + """Test reading a text file""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write('print("hello world")') + temp_path = f.name + + try: + with open(temp_path, 'r') as f: + content = f.read() + self.assertEqual(content, 'print("hello world")') + finally: + os.unlink(temp_path) + + def test_base64_encoding(self): + """Test base64 encoding for file contents""" + import base64 + + content = b'print("hello world")' + encoded = base64.b64encode(content).decode('utf-8') + decoded = base64.b64decode(encoded) + + self.assertEqual(decoded, content) + + def test_file_basename(self): + """Test extracting file basename""" + path = "/home/user/project/script.py" + basename = os.path.basename(path) + + self.assertEqual(basename, "script.py") + + +class TestAPIConstants(unittest.TestCase): + """Test API-related constants""" + + def test_api_base_url(self): + """Test API base URL format""" + API_BASE = "https://api.unsandbox.com" + + self.assertTrue(API_BASE.startswith('https://')) + self.assertIn('unsandbox.com', API_BASE) + + def test_portal_base_url(self): + """Test portal base URL format""" + PORTAL_BASE = "https://unsandbox.com" + + self.assertTrue(PORTAL_BASE.startswith('https://')) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/unit/test_ruby.rb b/tests/unit/test_ruby.rb new file mode 100755 index 0000000..06ba17f --- /dev/null +++ b/tests/unit/test_ruby.rb @@ -0,0 +1,334 @@ +#!/usr/bin/env ruby +# Unit tests for un.rb - tests internal functions without API calls + +require 'openssl' +require 'base64' +require 'tempfile' +require 'fileutils' + +# Test counters +$passed = 0 +$failed = 0 + +def test(name) + begin + yield + puts " ✓ #{name}" + $passed += 1 + rescue => e + puts " ✗ #{name}" + puts " #{e.message}" + $failed += 1 + end +end + +def assert_equal(actual, expected, msg = '') + raise "Expected '#{expected}' but got '#{actual}' #{msg}" unless actual == expected +end + +def assert_not_equal(a, b) + raise "Expected values to be different but both were '#{a}'" if a == b +end + +def assert_includes(str, substr) + raise "Expected '#{str}' to include '#{substr}'" unless str.include?(substr) +end + +def assert_true(val) + raise "Expected true but got #{val}" unless val +end + +# Extension mapping (copied from un.rb) +EXT_MAP = { + '.py' => 'python', '.js' => 'javascript', '.ts' => 'typescript', + '.rb' => 'ruby', '.php' => 'php', '.pl' => 'perl', '.lua' => 'lua', + '.sh' => 'bash', '.go' => 'go', '.rs' => 'rust', '.c' => 'c', + '.cpp' => 'cpp', '.cc' => 'cpp', '.cxx' => 'cpp', + '.java' => 'java', '.kt' => 'kotlin', '.cs' => 'csharp', '.fs' => 'fsharp', + '.hs' => 'haskell', '.ml' => 'ocaml', '.clj' => 'clojure', '.scm' => 'scheme', + '.lisp' => 'commonlisp', '.erl' => 'erlang', '.ex' => 'elixir', '.exs' => 'elixir', + '.jl' => 'julia', '.r' => 'r', '.R' => 'r', '.cr' => 'crystal', + '.d' => 'd', '.nim' => 'nim', '.zig' => 'zig', '.v' => 'v', + '.dart' => 'dart', '.groovy' => 'groovy', '.scala' => 'scala', + '.f90' => 'fortran', '.f95' => 'fortran', '.cob' => 'cobol', + '.pro' => 'prolog', '.forth' => 'forth', '.4th' => 'forth', + '.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc' +}.freeze + +# ============================================================================ +# Extension Mapping Tests +# ============================================================================ + +puts "\n=== Extension Mapping Tests ===" + +test('Python extension maps correctly') do + assert_equal EXT_MAP['.py'], 'python' +end + +test('JavaScript extensions map correctly') do + assert_equal EXT_MAP['.js'], 'javascript' + assert_equal EXT_MAP['.ts'], 'typescript' +end + +test('Ruby extension maps correctly') do + assert_equal EXT_MAP['.rb'], 'ruby' +end + +test('Go extension maps correctly') do + assert_equal EXT_MAP['.go'], 'go' +end + +test('Rust extension maps correctly') do + assert_equal EXT_MAP['.rs'], 'rust' +end + +test('C/C++ extensions map correctly') do + assert_equal EXT_MAP['.c'], 'c' + assert_equal EXT_MAP['.cpp'], 'cpp' + assert_equal EXT_MAP['.cc'], 'cpp' + assert_equal EXT_MAP['.cxx'], 'cpp' +end + +test('JVM extensions map correctly') do + assert_equal EXT_MAP['.java'], 'java' + assert_equal EXT_MAP['.kt'], 'kotlin' + assert_equal EXT_MAP['.groovy'], 'groovy' + assert_equal EXT_MAP['.scala'], 'scala' +end + +test('.NET extensions map correctly') do + assert_equal EXT_MAP['.cs'], 'csharp' + assert_equal EXT_MAP['.fs'], 'fsharp' +end + +test('Functional language extensions map correctly') do + assert_equal EXT_MAP['.hs'], 'haskell' + assert_equal EXT_MAP['.ml'], 'ocaml' + assert_equal EXT_MAP['.clj'], 'clojure' + assert_equal EXT_MAP['.scm'], 'scheme' + assert_equal EXT_MAP['.lisp'], 'commonlisp' + assert_equal EXT_MAP['.erl'], 'erlang' + assert_equal EXT_MAP['.ex'], 'elixir' +end + +test('Scientific extensions map correctly') do + assert_equal EXT_MAP['.jl'], 'julia' + assert_equal EXT_MAP['.r'], 'r' + assert_equal EXT_MAP['.f90'], 'fortran' +end + +test('Exotic extensions map correctly') do + assert_equal EXT_MAP['.d'], 'd' + assert_equal EXT_MAP['.nim'], 'nim' + assert_equal EXT_MAP['.zig'], 'zig' + assert_equal EXT_MAP['.v'], 'v' + assert_equal EXT_MAP['.cr'], 'crystal' +end + +# ============================================================================ +# HMAC Signature Tests +# ============================================================================ + +puts "\n=== HMAC Signature Tests ===" + +test('HMAC-SHA256 generates 64 character hex string') do + secret = 'test-secret-key' + message = '1234567890:POST:/execute:{}' + + signature = OpenSSL::HMAC.hexdigest('SHA256', secret, message) + + assert_equal signature.length, 64 +end + +test('Same input produces same signature') do + secret = 'test-secret-key' + message = '1234567890:POST:/execute:{}' + + sig1 = OpenSSL::HMAC.hexdigest('SHA256', secret, message) + sig2 = OpenSSL::HMAC.hexdigest('SHA256', secret, message) + + assert_equal sig1, sig2 +end + +test('Different secrets produce different signatures') do + message = '1234567890:POST:/execute:{}' + + sig1 = OpenSSL::HMAC.hexdigest('SHA256', 'secret1', message) + sig2 = OpenSSL::HMAC.hexdigest('SHA256', 'secret2', message) + + assert_not_equal sig1, sig2 +end + +test('Different messages produce different signatures') do + secret = 'test-secret' + + sig1 = OpenSSL::HMAC.hexdigest('SHA256', secret, 'message1') + sig2 = OpenSSL::HMAC.hexdigest('SHA256', secret, 'message2') + + assert_not_equal sig1, sig2 +end + +test('Signature format is timestamp:METHOD:path:body') do + timestamp = '1704067200' + method = 'POST' + endpoint = '/execute' + body = '{"language":"python","code":"print(1)"}' + + message = "#{timestamp}:#{method}:#{endpoint}:#{body}" + + # Verify format: starts with timestamp, has method and path + assert_true message.start_with?(timestamp) + assert_includes message, ':POST:' + assert_includes message, ':/execute:' +end + +# ============================================================================ +# Language Detection Tests +# ============================================================================ + +puts "\n=== Language Detection Tests ===" + +test('Detect language from .py extension') do + filename = 'script.py' + ext = File.extname(filename).downcase + assert_equal EXT_MAP[ext], 'python' +end + +test('Detect language from .rb extension') do + filename = 'app.rb' + ext = File.extname(filename).downcase + assert_equal EXT_MAP[ext], 'ruby' +end + +test('Python shebang detection') do + content = "#!/usr/bin/env python3\nprint('hello')" + first_line = content.lines.first + + assert_true first_line.start_with?('#!') + assert_includes first_line, 'python' +end + +test('Ruby shebang detection') do + content = "#!/usr/bin/env ruby\nputs 'hello'" + first_line = content.lines.first + + assert_true first_line.start_with?('#!') + assert_includes first_line, 'ruby' +end + +test('Bash shebang detection') do + content = "#!/bin/bash\necho hello" + first_line = content.lines.first + + assert_true first_line.start_with?('#!') + assert_true first_line.include?('bash') || first_line.include?('/sh') +end + +# ============================================================================ +# Argument Parsing Tests +# ============================================================================ + +puts "\n=== Argument Parsing Tests ===" + +test('Parse -e KEY=VALUE format') do + arg = 'DEBUG=1' + key, value = arg.split('=', 2) + + assert_equal key, 'DEBUG' + assert_equal value, '1' +end + +test('Parse -e KEY=VALUE with equals in value') do + arg = 'URL=https://example.com?foo=bar' + key, value = arg.split('=', 2) + + assert_equal key, 'URL' + assert_equal value, 'https://example.com?foo=bar' +end + +test('Valid network modes') do + valid_modes = %w[zerotrust semitrusted] + + assert_true valid_modes.include?('zerotrust') + assert_true valid_modes.include?('semitrusted') + assert_true !valid_modes.include?('invalid') +end + +test('Subcommand detection') do + args = %w[session --shell python3] + subcommands = %w[session service key restore] + subcommand = subcommands.include?(args[0]) ? args[0] : nil + + assert_equal subcommand, 'session' +end + +# ============================================================================ +# File Operations Tests +# ============================================================================ + +puts "\n=== File Operations Tests ===" + +test('Read text file') do + file = Tempfile.new(['test', '.py']) + file.write('print("hello world")') + file.close + + begin + content = File.read(file.path) + assert_equal content, 'print("hello world")' + ensure + file.unlink + end +end + +test('Base64 encoding/decoding') do + content = 'print("hello world")' + encoded = Base64.strict_encode64(content) + decoded = Base64.strict_decode64(encoded) + + assert_equal decoded, content +end + +test('Extract file basename') do + filepath = '/home/user/project/script.py' + basename = File.basename(filepath) + + assert_equal basename, 'script.py' +end + +test('Extract file extension') do + filepath = '/home/user/project/script.py' + ext = File.extname(filepath) + + assert_equal ext, '.py' +end + +# ============================================================================ +# API Constants Tests +# ============================================================================ + +puts "\n=== API Constants Tests ===" + +test('API base URL format') do + api_base = 'https://api.unsandbox.com' + + assert_true api_base.start_with?('https://') + assert_includes api_base, 'unsandbox.com' +end + +test('Portal base URL format') do + portal_base = 'https://unsandbox.com' + + assert_true portal_base.start_with?('https://') +end + +# ============================================================================ +# Summary +# ============================================================================ + +puts "\n=== Summary ===" +puts "Passed: #{$passed}" +puts "Failed: #{$failed}" +puts "Total: #{$passed + $failed}" + +exit($failed > 0 ? 1 : 0)