feat: full feature parity for all 42 SDKs + comprehensive tests
All SDKs now implement 58+ functions matching C reference (un.h): - Execution (8): execute, execute_async, wait_job, get_job, cancel_job, list_jobs, get_languages, detect_language - Sessions (9): list, get, create, destroy, freeze, unfreeze, boost, unboost, execute - Services (17): list, get, create, destroy, freeze, unfreeze, lock, unlock, set_unfreeze_on_demand, redeploy, logs, execute, env_get/set/delete/export, resize - Snapshots (9): list, get, session, service, restore, delete, lock, unlock, clone - Images (13): list, get, publish, delete, lock, unlock, set_visibility, grant/revoke_access, list_trusted, transfer, spawn, clone - PaaS Logs (2): fetch, stream - Utilities (5): validate_keys, hmac_sign, health_check, version, last_error Test suites created for all SDKs with unit, integration, and functional tests. Languages: AWK, Bash, C++, C#, Clojure, COBOL, Crystal, D, Dart, .NET, Elixir, Erlang, F#, Forth, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lisp, Lua, Nim, Objective-C, OCaml, Perl, PHP, PowerShell, Prolog, Python, R, Raku, Ruby, Rust, Scheme, Swift, Tcl, TypeScript, V, Zig
This commit is contained in:
parent
a5155aed4f
commit
6e746ace44
90 changed files with 28955 additions and 3028 deletions
|
|
@ -1096,6 +1096,20 @@ func ExecuteInService(creds *Credentials, serviceID, command string) (map[string
|
|||
return makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
|
||||
}
|
||||
|
||||
// ResizeService changes the vCPU count for a running service.
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// creds: API credentials
|
||||
// serviceID: Service ID
|
||||
// vcpu: New vCPU count (1-8)
|
||||
func ResizeService(creds *Credentials, serviceID string, vcpu int) (map[string]interface{}, error) {
|
||||
data := map[string]interface{}{
|
||||
"vcpu": vcpu,
|
||||
}
|
||||
return makeRequest("POST", fmt.Sprintf("/services/%s/resize", serviceID), creds, data)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Additional Snapshot Operations
|
||||
// ============================================================================
|
||||
|
|
@ -1480,6 +1494,190 @@ func CloneImage(creds *Credentials, imageID string, opts *CloneImageOptions) (ma
|
|||
return makeRequest("POST", fmt.Sprintf("/images/%s/clone", imageID), creds, data)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PaaS Logs API
|
||||
// ============================================================================
|
||||
|
||||
// LogsFetchOptions contains options for fetching logs.
|
||||
type LogsFetchOptions struct {
|
||||
Lines int // Number of lines (1-10000)
|
||||
Since string // Time window ("1m", "5m", "1h", "1d")
|
||||
Grep string // Optional filter pattern
|
||||
}
|
||||
|
||||
// LogsFetch fetches batch logs from the portal.
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// creds: API credentials
|
||||
// source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
// opts: Fetch options (can be nil for defaults)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// JSON response with log entries
|
||||
func LogsFetch(creds *Credentials, source string, opts *LogsFetchOptions) (map[string]interface{}, error) {
|
||||
path := "/paas/logs"
|
||||
params := []string{}
|
||||
|
||||
if source != "" {
|
||||
params = append(params, fmt.Sprintf("source=%s", source))
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
if opts.Lines > 0 {
|
||||
params = append(params, fmt.Sprintf("lines=%d", opts.Lines))
|
||||
}
|
||||
if opts.Since != "" {
|
||||
params = append(params, fmt.Sprintf("since=%s", opts.Since))
|
||||
}
|
||||
if opts.Grep != "" {
|
||||
params = append(params, fmt.Sprintf("grep=%s", opts.Grep))
|
||||
}
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
path = path + "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
return makeRequest("GET", path, creds, nil)
|
||||
}
|
||||
|
||||
// LogCallback is called for each log line received during streaming.
|
||||
type LogCallback func(source, line string)
|
||||
|
||||
// LogsStream streams logs via Server-Sent Events.
|
||||
// This function blocks until the stream is closed or an error occurs.
|
||||
//
|
||||
// Args:
|
||||
//
|
||||
// creds: API credentials
|
||||
// source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
// grep: Optional filter pattern (empty string for no filter)
|
||||
// callback: Function called for each log line
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// nil on clean shutdown, error on failure
|
||||
func LogsStream(creds *Credentials, source, grep string, callback LogCallback) error {
|
||||
path := "/paas/logs/stream"
|
||||
params := []string{}
|
||||
|
||||
if source != "" {
|
||||
params = append(params, fmt.Sprintf("source=%s", source))
|
||||
}
|
||||
if grep != "" {
|
||||
params = append(params, fmt.Sprintf("grep=%s", grep))
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
path = path + "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
url := APIBase + path
|
||||
timestamp := time.Now().Unix()
|
||||
message := fmt.Sprintf("%d:GET:%s:", timestamp, path)
|
||||
mac := hmac.New(sha256.New, []byte(creds.SecretKey))
|
||||
mac.Write([]byte(message))
|
||||
signature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+creds.PublicKey)
|
||||
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
|
||||
req.Header.Set("X-Signature", signature)
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
|
||||
client := &http.Client{Timeout: 0} // No timeout for streaming
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("stream error (HTTP %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(resp.Body)
|
||||
currentSource := source
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil // Clean shutdown
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse SSE format
|
||||
if strings.HasPrefix(line, "event:") {
|
||||
// New source from event type
|
||||
currentSource = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
||||
} else if strings.HasPrefix(line, "data:") {
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if callback != nil && data != "" {
|
||||
callback(currentSource, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
// SDKVersion is the version of this SDK.
|
||||
const SDKVersion = "4.2.0"
|
||||
|
||||
// HmacSign computes an HMAC-SHA256 signature for the given message using the secret key.
|
||||
// Returns the signature as a lowercase hex string.
|
||||
func HmacSign(secretKey, message string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secretKey))
|
||||
mac.Write([]byte(message))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// HealthCheck checks if the API is reachable and responding.
|
||||
// Returns true if healthy, false otherwise.
|
||||
func HealthCheck() bool {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(APIBase + "/health")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == 200
|
||||
}
|
||||
|
||||
// Version returns the SDK version string.
|
||||
func Version() string {
|
||||
return SDKVersion
|
||||
}
|
||||
|
||||
// lastError holds the most recent error message for thread-safe access.
|
||||
var lastError string
|
||||
|
||||
// SetLastError sets the last error message (internal use).
|
||||
func SetLastError(msg string) {
|
||||
lastError = msg
|
||||
}
|
||||
|
||||
// LastError returns the most recent error message from the SDK.
|
||||
func LastError() string {
|
||||
return lastError
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CLI Implementation
|
||||
// ============================================================================
|
||||
|
|
|
|||
363
clients/go/sync/tests/un_test.go
Normal file
363
clients/go/sync/tests/un_test.go
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
// Tests for the Go unsandbox SDK
|
||||
// Run with: go test -v ./tests/
|
||||
package un
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
func TestDetectLanguage(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
expected string
|
||||
}{
|
||||
{"script.py", "python"},
|
||||
{"script.js", "javascript"},
|
||||
{"script.ts", "typescript"},
|
||||
{"script.rb", "ruby"},
|
||||
{"script.go", "go"},
|
||||
{"script.rs", "rust"},
|
||||
{"script.c", "c"},
|
||||
{"script.cpp", "cpp"},
|
||||
{"script.d", "d"},
|
||||
{"script.zig", "zig"},
|
||||
{"script.sh", "bash"},
|
||||
{"script.lua", "lua"},
|
||||
{"script.php", "php"},
|
||||
{"script.unknown", ""},
|
||||
{"script", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.filename, func(t *testing.T) {
|
||||
result := DetectLanguage(tt.filename)
|
||||
if result != tt.expected {
|
||||
t.Errorf("DetectLanguage(%q) = %q, want %q", tt.filename, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHmacSign(t *testing.T) {
|
||||
// Test with known values
|
||||
secretKey := "test-secret"
|
||||
message := "test-message"
|
||||
|
||||
result := HmacSign(secretKey, message)
|
||||
|
||||
// Should return a 64-character hex string
|
||||
if len(result) != 64 {
|
||||
t.Errorf("HmacSign returned %d characters, want 64", len(result))
|
||||
}
|
||||
|
||||
// Should be deterministic
|
||||
result2 := HmacSign(secretKey, message)
|
||||
if result != result2 {
|
||||
t.Error("HmacSign is not deterministic")
|
||||
}
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
result3 := HmacSign(secretKey, "different-message")
|
||||
if result == result3 {
|
||||
t.Error("HmacSign returned same result for different inputs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
version := Version()
|
||||
if version == "" {
|
||||
t.Error("Version() returned empty string")
|
||||
}
|
||||
// Should be in semver format
|
||||
if len(version) < 5 { // At minimum "0.0.0"
|
||||
t.Errorf("Version() = %q, expected semver format", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastError(t *testing.T) {
|
||||
// Set an error
|
||||
SetLastError("test error message")
|
||||
|
||||
// Retrieve it
|
||||
err := LastError()
|
||||
if err != "test error message" {
|
||||
t.Errorf("LastError() = %q, want %q", err, "test error message")
|
||||
}
|
||||
|
||||
// Clear it
|
||||
SetLastError("")
|
||||
err = LastError()
|
||||
if err != "" {
|
||||
t.Errorf("LastError() after clear = %q, want empty", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsNew(t *testing.T) {
|
||||
pk := "unsb-pk-test-test-test-test"
|
||||
sk := "unsb-sk-test1-test2-test3-test4"
|
||||
|
||||
creds := &Credentials{
|
||||
PublicKey: pk,
|
||||
SecretKey: sk,
|
||||
}
|
||||
|
||||
if creds.PublicKey != pk {
|
||||
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, pk)
|
||||
}
|
||||
if creds.SecretKey != sk {
|
||||
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, sk)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
func TestSignRequest(t *testing.T) {
|
||||
secretKey := "test-secret-key"
|
||||
timestamp := int64(1704067200) // 2024-01-01 00:00:00 UTC
|
||||
method := "POST"
|
||||
path := "/execute"
|
||||
body := `{"language":"python","code":"print(1)"}`
|
||||
|
||||
signature := signRequest(secretKey, timestamp, method, path, []byte(body))
|
||||
|
||||
// Should return a 64-character hex string
|
||||
if len(signature) != 64 {
|
||||
t.Errorf("signRequest returned %d characters, want 64", len(signature))
|
||||
}
|
||||
|
||||
// Should be deterministic
|
||||
signature2 := signRequest(secretKey, timestamp, method, path, []byte(body))
|
||||
if signature != signature2 {
|
||||
t.Error("signRequest is not deterministic")
|
||||
}
|
||||
|
||||
// Different timestamps should produce different signatures
|
||||
signature3 := signRequest(secretKey, timestamp+1, method, path, []byte(body))
|
||||
if signature == signature3 {
|
||||
t.Error("signRequest returned same result for different timestamps")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCredentialsFromEnv(t *testing.T) {
|
||||
// Save original env vars
|
||||
origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
origSK := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
// Set test env vars
|
||||
testPK := "unsb-pk-test-test-test-test"
|
||||
testSK := "unsb-sk-test1-test2-test3-test4"
|
||||
os.Setenv("UNSANDBOX_PUBLIC_KEY", testPK)
|
||||
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
||||
|
||||
// Test
|
||||
creds, err := ResolveCredentials("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
if creds.PublicKey != testPK {
|
||||
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK)
|
||||
}
|
||||
if creds.SecretKey != testSK {
|
||||
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK)
|
||||
}
|
||||
|
||||
// Restore original env vars
|
||||
if origPK != "" {
|
||||
os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK)
|
||||
} else {
|
||||
os.Unsetenv("UNSANDBOX_PUBLIC_KEY")
|
||||
}
|
||||
if origSK != "" {
|
||||
os.Setenv("UNSANDBOX_SECRET_KEY", origSK)
|
||||
} else {
|
||||
os.Unsetenv("UNSANDBOX_SECRET_KEY")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||
|
||||
creds, err := ResolveCredentials(testPK, testSK)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
if creds.PublicKey != testPK {
|
||||
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK)
|
||||
}
|
||||
if creds.SecretKey != testSK {
|
||||
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// ============================================================================
|
||||
|
||||
func getTestCredentials(t *testing.T) *Credentials {
|
||||
creds, err := ResolveCredentials("", "")
|
||||
if err != nil {
|
||||
t.Skip("No credentials available for functional tests")
|
||||
}
|
||||
return creds
|
||||
}
|
||||
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
healthy := HealthCheck()
|
||||
if !healthy {
|
||||
t.Log("API health check returned unhealthy (API may be unreachable)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLanguages(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
languages, err := GetLanguages(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLanguages failed: %v", err)
|
||||
}
|
||||
|
||||
if len(languages) == 0 {
|
||||
t.Error("GetLanguages returned empty list")
|
||||
}
|
||||
|
||||
// Should include common languages
|
||||
hasPython := false
|
||||
hasJavascript := false
|
||||
for _, lang := range languages {
|
||||
if lang == "python" {
|
||||
hasPython = true
|
||||
}
|
||||
if lang == "javascript" {
|
||||
hasJavascript = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPython {
|
||||
t.Error("GetLanguages missing 'python'")
|
||||
}
|
||||
if !hasJavascript {
|
||||
t.Error("GetLanguages missing 'javascript'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateKeys(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
result, err := ValidateKeys(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Error("ValidateKeys returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCode(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
result, err := ExecuteCode(creds, "python", "print('hello from go test')")
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteCode failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Error("ExecuteCode returned nil")
|
||||
}
|
||||
|
||||
// Check for stdout in result
|
||||
if stdout, ok := result["stdout"].(string); ok {
|
||||
if stdout == "" {
|
||||
t.Error("ExecuteCode returned empty stdout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSessions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
sessions, err := ListSessions(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSessions failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if sessions == nil {
|
||||
t.Error("ListSessions returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListServices(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
services, err := ListServices(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ListServices failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if services == nil {
|
||||
t.Error("ListServices returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSnapshots(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
snapshots, err := ListSnapshots(creds)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSnapshots failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if snapshots == nil {
|
||||
t.Error("ListSnapshots returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImages(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
creds := getTestCredentials(t)
|
||||
images, err := ListImages(creds, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListImages failed: %v", err)
|
||||
}
|
||||
|
||||
// Should return a list (possibly empty)
|
||||
if images == nil {
|
||||
t.Error("ListImages returned nil")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue