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
|
|
@ -2393,6 +2393,123 @@ public class Un {
|
|||
return makeRequest("POST", "/images/" + imageId + "/clone", creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PaaS Logs API (2)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Fetch batch logs from portal.
|
||||
*
|
||||
* @param source Log source: "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* @param lines Number of lines (1-10000)
|
||||
* @param since Time window: "1m", "5m", "1h", "1d"
|
||||
* @param grep Optional filter pattern (null for no filter)
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map containing logs
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> logsFetch(
|
||||
String source,
|
||||
int lines,
|
||||
String since,
|
||||
String grep,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
StringBuilder path = new StringBuilder("/paas/logs?source=");
|
||||
path.append(source != null ? source : "all");
|
||||
path.append("&lines=").append(lines > 0 ? lines : 100);
|
||||
if (since != null && !since.isEmpty()) {
|
||||
path.append("&since=").append(since);
|
||||
}
|
||||
if (grep != null && !grep.isEmpty()) {
|
||||
path.append("&grep=").append(java.net.URLEncoder.encode(grep, "UTF-8"));
|
||||
}
|
||||
return makeRequest("GET", path.toString(), creds[0], creds[1], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for receiving streamed log lines.
|
||||
*/
|
||||
public interface LogCallback {
|
||||
/**
|
||||
* Called for each log line received.
|
||||
*
|
||||
* @param source The log source (e.g., "api", "portal")
|
||||
* @param line The log line content
|
||||
*/
|
||||
void onLogLine(String source, String line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream logs via SSE. Blocks until interrupted or server closes connection.
|
||||
*
|
||||
* @param source Log source: "all", "api", "portal", "pool/cammy", "pool/ai"
|
||||
* @param grep Optional filter pattern (null for no filter)
|
||||
* @param callback Callback for each log line received
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return true on clean shutdown, false on error
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
*/
|
||||
public static boolean logsStream(
|
||||
String source,
|
||||
String grep,
|
||||
LogCallback callback,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
StringBuilder path = new StringBuilder("/paas/logs/stream?source=");
|
||||
path.append(source != null ? source : "all");
|
||||
if (grep != null && !grep.isEmpty()) {
|
||||
path.append("&grep=").append(java.net.URLEncoder.encode(grep, "UTF-8"));
|
||||
}
|
||||
|
||||
String url = API_BASE + path.toString();
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
String signature = signRequest(creds[1], timestamp, "GET", path.toString(), null);
|
||||
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(0); // No timeout for streaming
|
||||
|
||||
conn.setRequestProperty("Authorization", "Bearer " + creds[0]);
|
||||
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
|
||||
conn.setRequestProperty("X-Signature", signature);
|
||||
conn.setRequestProperty("Accept", "text/event-stream");
|
||||
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
String currentSource = source != null ? source : "all";
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6);
|
||||
if (callback != null) {
|
||||
callback.onLogLine(currentSource, data);
|
||||
}
|
||||
} else if (line.startsWith("event: ")) {
|
||||
currentSource = line.substring(7);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Key Validation API
|
||||
// ========================================================================
|
||||
|
|
@ -2415,6 +2532,106 @@ public class Un {
|
|||
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Utility Functions
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get SDK version string.
|
||||
*
|
||||
* @return Version string (e.g., "4.2.0")
|
||||
*/
|
||||
public static String version() {
|
||||
return "4.2.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check API health status.
|
||||
*
|
||||
* @return true if API is healthy, false otherwise
|
||||
*/
|
||||
public static boolean healthCheck() {
|
||||
try {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(API_BASE + "/health").openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
return conn.getResponseCode() == 200;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HMAC-SHA256 signature.
|
||||
*
|
||||
* @param secretKey Secret key for HMAC
|
||||
* @param message Message to sign
|
||||
* @return Lowercase hex-encoded signature
|
||||
*/
|
||||
public static String hmacSign(String secretKey, String message) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(
|
||||
secretKey.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSHA256"
|
||||
);
|
||||
mac.init(secretKeySpec);
|
||||
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
hexString.append(String.format("%02x", b));
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific snapshot.
|
||||
*
|
||||
* @param snapshotId Snapshot ID to retrieve
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Snapshot details map
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> getSnapshot(
|
||||
String snapshotId,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
return makeRequest("GET", "/snapshots/" + snapshotId, creds[0], creds[1], null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a service (change vCPU allocation).
|
||||
*
|
||||
* @param serviceId Service ID to resize
|
||||
* @param vcpu New vCPU count (1-8)
|
||||
* @param publicKey Optional API key
|
||||
* @param secretKey Optional API secret
|
||||
* @return Response map with resize confirmation
|
||||
* @throws IOException on network errors
|
||||
* @throws CredentialsException if credentials cannot be found
|
||||
* @throws ApiException if API returns an error
|
||||
*/
|
||||
public static Map<String, Object> resizeService(
|
||||
String serviceId,
|
||||
int vcpu,
|
||||
String publicKey,
|
||||
String secretKey
|
||||
) throws IOException {
|
||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("vcpu", vcpu);
|
||||
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Image Generation API
|
||||
// ========================================================================
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
* - HMAC-SHA256 signature generation
|
||||
* - Credential resolution logic
|
||||
* - Language detection
|
||||
* - Utility functions
|
||||
*
|
||||
* To run tests:
|
||||
* mvn test
|
||||
|
|
@ -80,6 +81,18 @@ public class UnTest {
|
|||
assertEquals("cpp", Un.detectLanguage("main.cxx"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Kotlin from .kt extension")
|
||||
void detectKotlin() {
|
||||
assertEquals("kotlin", Un.detectLanguage("Main.kt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should detect Groovy from .groovy extension")
|
||||
void detectGroovy() {
|
||||
assertEquals("groovy", Un.detectLanguage("script.groovy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return null for unknown extension")
|
||||
void detectUnknown() {
|
||||
|
|
@ -94,6 +107,44 @@ public class UnTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Utility Function Tests")
|
||||
class UtilityTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Version should return a valid version string")
|
||||
void versionString() {
|
||||
String version = Un.version();
|
||||
assertNotNull(version);
|
||||
assertTrue(version.matches("\\d+\\.\\d+\\.\\d+"), "Version should be in X.Y.Z format");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should produce valid hex signature")
|
||||
void hmacSignature() {
|
||||
String signature = Un.hmacSign("secret", "message");
|
||||
assertNotNull(signature);
|
||||
assertEquals(64, signature.length(), "HMAC-SHA256 should produce 64 hex chars");
|
||||
assertTrue(signature.matches("[0-9a-f]+"), "Signature should be lowercase hex");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should be consistent")
|
||||
void hmacConsistent() {
|
||||
String sig1 = Un.hmacSign("key", "data");
|
||||
String sig2 = Un.hmacSign("key", "data");
|
||||
assertEquals(sig1, sig2, "Same inputs should produce same signature");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HMAC sign should differ with different inputs")
|
||||
void hmacDifferent() {
|
||||
String sig1 = Un.hmacSign("key1", "data");
|
||||
String sig2 = Un.hmacSign("key2", "data");
|
||||
assertNotEquals(sig1, sig2, "Different keys should produce different signatures");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Credential Exception Tests")
|
||||
class CredentialExceptionTests {
|
||||
|
|
@ -120,6 +171,22 @@ public class UnTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Sudo Challenge Exception Tests")
|
||||
class SudoChallengeExceptionTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("SudoChallengeException should contain challenge ID")
|
||||
void sudoChallengeDetails() {
|
||||
Un.SudoChallengeException ex = new Un.SudoChallengeException(
|
||||
"challenge-123",
|
||||
"{\"challenge_id\": \"challenge-123\"}"
|
||||
);
|
||||
assertEquals("challenge-123", ex.getChallengeId());
|
||||
assertEquals("{\"challenge_id\": \"challenge-123\"}", ex.getResponseBody());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Integration Tests (requires credentials)")
|
||||
@EnabledIfEnvironmentVariable(named = "UNSANDBOX_PUBLIC_KEY", matches = ".+")
|
||||
|
|
@ -193,5 +260,63 @@ public class UnTest {
|
|||
assertEquals("completed", result.get("status"));
|
||||
assertTrue(result.get("stdout").toString().contains("Async test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list jobs")
|
||||
void listJobs() throws IOException {
|
||||
List<Map<String, Object>> jobs = Un.listJobs(publicKey, secretKey);
|
||||
assertNotNull(jobs);
|
||||
// Jobs list can be empty if no jobs are running
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate keys successfully")
|
||||
void validateKeys() throws IOException {
|
||||
Map<String, Object> result = Un.validateKeys(publicKey, secretKey);
|
||||
assertNotNull(result);
|
||||
// The response should contain validation info
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list sessions")
|
||||
void listSessions() throws IOException {
|
||||
List<Map<String, Object>> sessions = Un.listSessions(publicKey, secretKey);
|
||||
assertNotNull(sessions);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list services")
|
||||
void listServices() throws IOException {
|
||||
List<Map<String, Object>> services = Un.listServices(publicKey, secretKey);
|
||||
assertNotNull(services);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list snapshots")
|
||||
void listSnapshots() throws IOException {
|
||||
List<Map<String, Object>> snapshots = Un.listSnapshots(publicKey, secretKey);
|
||||
assertNotNull(snapshots);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should list images")
|
||||
void listImages() throws IOException {
|
||||
List<Map<String, Object>> images = Un.listImages(null, publicKey, secretKey);
|
||||
assertNotNull(images);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Health Check Tests")
|
||||
class HealthCheckTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Health check should return boolean")
|
||||
void healthCheckReturnsBoolean() {
|
||||
boolean healthy = Un.healthCheck();
|
||||
// We just verify it returns without throwing
|
||||
// The actual result depends on network connectivity
|
||||
assertTrue(healthy || !healthy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue