fix(c): Export library API, document SDK testing philosophy

C SDK changes:
- Add #include un.h when building as library
- Export library functions: version, detect_language, hmac_sign, health_check
- Add NULL safety checks to wrapper functions
- Rewrite tests to call ACTUAL exported functions (no more local re-implementations)
- Tests now verify real HMAC-SHA256 against known test vectors

Documentation:
- Add SDK Testing Philosophy to CLAUDE.md
- Create docs/TESTING.md with unit/integration/functional test definitions
- Document the THREE testing levels required for all SDKs
- Explicitly forbid mocking and local re-implementation in tests

33 unit tests pass, testing real exported functions.
This commit is contained in:
russell@unturf.com 2026-01-15 18:21:05 -05:00
parent ebdc590636
commit 1880dba054
5 changed files with 699 additions and 246 deletions

View file

@ -251,6 +251,84 @@ git remote set-url --add --push origin ssh://git@git.unturf.com:2222/engineering
git remote set-url --add --push origin git@github.com:russellballestrini/un-inception.git
```
## SDK Testing Philosophy
**SDKs are LIBRARIES for embedding in other people's code.** They are NOT just CLIs.
### Three Testing Levels (ALL REQUIRED)
| Level | What It Tests | How |
|-------|--------------|-----|
| **Unit** | Exported library functions | Test actual exports in native language. NO MOCKING. NO RE-IMPLEMENTING. |
| **Integration** | SDK components work together | Internal SDK tests (auth + request + response parsing) |
| **Functional** | Real API lifecycle | Actually call api.unsandbox.com - execute, sessions, services |
### CRITICAL: No Mocking or Local Re-implementation
**FORBIDDEN**: Re-implementing functions locally to "test" them.
```c
// ❌ WRONG - test_library.c re-implements SHA-256 locally
static void sha256_transform(...) { /* local copy */ }
void test_sha256() { /* tests local copy, not actual SDK */ }
// ✅ CORRECT - test actual exported SDK functions
#include "un.h"
void test_sha256() {
// Call the REAL exported function from un.c
char *result = unsandbox_hmac_sign("key", "message");
assert(strcmp(result, expected) == 0);
free(result);
}
```
### SDK Export Requirements
Each SDK MUST export functions that can be:
1. **Imported** - Other code can `import`/`require`/`use` the SDK
2. **Tested** - Unit tests can call exported functions directly
3. **Documented** - Public API is clear and documented
**Example (C SDK)**:
```c
// un.h declares public API
unsandbox_result_t *unsandbox_execute(const char *lang, const char *code, ...);
char *unsandbox_hmac_sign(const char *secret, const char *message);
// un.c implements with NON-STATIC functions (when built as library)
#ifndef UNSANDBOX_CLI_ONLY
unsandbox_result_t *unsandbox_execute(...) { /* real implementation */ }
char *unsandbox_hmac_sign(...) { /* real implementation */ }
#endif
```
### Test File Structure
```
clients/{language}/
├── src/ # Source files
├── tests/
│ ├── unit/ # Unit tests - test exported functions
│ ├── integration/ # Integration tests - SDK internal consistency
│ └── functional/ # Functional tests - real API calls
├── Makefile # Build + test targets
└── README.md
```
### Makefile Test Targets
Every client Makefile MUST have:
```makefile
test: test-cli test-library test-integration test-functional
test-cli: # CLI binary works (--help, --version)
test-library: # Unit tests of exported library functions
test-integration: # SDK internal consistency tests
test-functional: # Real API calls (requires UNSANDBOX_* env vars)
```
See **docs/TESTING.md** for complete testing guidelines.
## Related Repos
- `~/git/unsandbox.com/` - Portal (contains un.c CLI at cli/un.c)

View file

@ -33,6 +33,10 @@
#include <libwebsockets.h>
#include <pwd.h>
#ifdef UNSANDBOX_LIBRARY
#include "un.h"
#endif
#define API_URL "https://api.unsandbox.com/execute"
#define API_BASE "https://api.unsandbox.com"
#define PORTAL_BASE "https://unsandbox.com"
@ -5028,6 +5032,82 @@ void print_usage(const char *prog) {
fprintf(stderr, " Or set UNSANDBOX_ACCOUNT=N env var\n");
}
/* ============================================================================
* LIBRARY API IMPLEMENTATION
* These functions implement the un.h public API for library use.
* ============================================================================ */
const char *unsandbox_version(void) {
return "2.0.0";
}
const char *unsandbox_detect_language(const char *filename) {
if (!filename) return NULL;
return detect_language_from_extension(filename);
}
char *unsandbox_hmac_sign(const char *secret_key, const char *message) {
if (!secret_key || !message) return NULL;
return hmac_sha256_hex(secret_key, strlen(secret_key), message, strlen(message));
}
int unsandbox_health_check(void) {
CURL *curl = curl_easy_init();
if (!curl) return -1;
curl_easy_setopt(curl, CURLOPT_URL, API_BASE "/health");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (res != CURLE_OK) return -1;
return (http_code == 200) ? 1 : 0;
}
/* Memory management */
void unsandbox_free_result(unsandbox_result_t *result) {
if (!result) return;
free(result->stdout_str);
free(result->stderr_str);
free(result->language);
free(result->error_message);
free(result);
}
void unsandbox_free_job(unsandbox_job_t *job) {
if (!job) return;
free(job->id);
free(job->language);
free(job->status);
free(job->error_message);
free(job);
}
void unsandbox_free_job_list(unsandbox_job_list_t *jobs) {
if (!jobs) return;
for (size_t i = 0; i < jobs->count; i++) {
free(jobs->jobs[i].id);
free(jobs->jobs[i].language);
free(jobs->jobs[i].status);
free(jobs->jobs[i].error_message);
}
free(jobs->jobs);
free(jobs);
}
void unsandbox_free_languages(unsandbox_languages_t *langs) {
if (!langs) return;
for (size_t i = 0; i < langs->count; i++) {
free(langs->languages[i]);
}
free(langs->languages);
free(langs);
}
#ifndef UNSANDBOX_LIBRARY
int main(int argc, char *argv[]) {
// Disable stdout buffering for real-time output

View file

@ -74,8 +74,8 @@ extern "C" {
* ============================================================================ */
typedef struct {
char *stdout;
char *stderr;
char *stdout_str; /* Use stdout_str to avoid conflict with stdio macro */
char *stderr_str; /* Use stderr_str to avoid conflict with stdio macro */
int exit_code;
char *language;
double execution_time;
@ -283,6 +283,16 @@ int unsandbox_resolve_credentials(
* Utility Functions
* ============================================================================ */
/**
* Generate HMAC-SHA256 signature for API authentication
*
* @param secret_key The secret key
* @param message The message to sign (format: "timestamp:METHOD:path:body")
* @return Hex-encoded signature string (64 chars). Must be freed with free().
* Returns NULL if inputs are invalid.
*/
char *unsandbox_hmac_sign(const char *secret_key, const char *message);
/**
* Get last error message from failed operation
*

View file

@ -1,245 +1,57 @@
/*
* Library Mode Tests for un.c
* Tests un.c functions as an embeddable C library
* Unit Tests for un.c Library Functions
*
* Compile: gcc -o test_library test_library.c -I../../.. -lcurl -lwebsockets -lssl -lcrypto
* Run: ./test_library
* Tests the ACTUAL exported functions from un.c via un.h.
* NO local re-implementations. NO mocking.
*
* Compile: make test-library
* Run: ./tests/test_library
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
// Test counters
#include "un.h"
/* Test counters */
static int tests_passed = 0;
static int tests_failed = 0;
#define PASS(msg) do { printf(" \033[32m✓\033[0m %s\n", msg); tests_passed++; } while(0)
#define FAIL(msg) do { printf(" \033[31m✗\033[0m %s\n", msg); tests_failed++; } while(0)
#define SKIP(msg) do { printf(" \033[33m⊘\033[0m %s (skipped)\n", msg); } while(0)
// ============================================================================
// Minimal SHA-256 test (copied from un.c for standalone testing)
// ============================================================================
/* ============================================================================
* Test: unsandbox_version()
* ============================================================================ */
static const uint32_t sha256_k[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
};
void test_version(void) {
printf("\nTesting unsandbox_version()...\n");
#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define EP0(x) (ROTR(x, 2) ^ ROTR(x, 13) ^ ROTR(x, 22))
#define EP1(x) (ROTR(x, 6) ^ ROTR(x, 11) ^ ROTR(x, 25))
#define SIG0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ ((x) >> 3))
#define SIG1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ ((x) >> 10))
const char *version = unsandbox_version();
typedef struct { uint32_t state[8]; uint64_t count; unsigned char buffer[64]; } SHA256_CTX;
static void sha256_init(SHA256_CTX *ctx) {
ctx->state[0] = 0x6a09e667; ctx->state[1] = 0xbb67ae85;
ctx->state[2] = 0x3c6ef372; ctx->state[3] = 0xa54ff53a;
ctx->state[4] = 0x510e527f; ctx->state[5] = 0x9b05688c;
ctx->state[6] = 0x1f83d9ab; ctx->state[7] = 0x5be0cd19;
ctx->count = 0;
}
static void sha256_transform(SHA256_CTX *ctx, const unsigned char *data) {
uint32_t a, b, c, d, e, f, g, h, t1, t2, w[64];
int i;
for (i = 0; i < 16; i++)
w[i] = ((uint32_t)data[i*4] << 24) | ((uint32_t)data[i*4+1] << 16) |
((uint32_t)data[i*4+2] << 8) | ((uint32_t)data[i*4+3]);
for (i = 16; i < 64; i++)
w[i] = SIG1(w[i-2]) + w[i-7] + SIG0(w[i-15]) + w[i-16];
a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3];
e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7];
for (i = 0; i < 64; i++) {
t1 = h + EP1(e) + CH(e, f, g) + sha256_k[i] + w[i];
t2 = EP0(a) + MAJ(a, b, c);
h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
}
ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d;
ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h;
}
static void sha256_update(SHA256_CTX *ctx, const unsigned char *data, size_t len) {
size_t i, index, part_len;
index = (size_t)(ctx->count & 0x3F);
ctx->count += len;
part_len = 64 - index;
if (len >= part_len) {
memcpy(&ctx->buffer[index], data, part_len);
sha256_transform(ctx, ctx->buffer);
for (i = part_len; i + 63 < len; i += 64)
sha256_transform(ctx, &data[i]);
index = 0;
} else { i = 0; }
memcpy(&ctx->buffer[index], &data[i], len - i);
}
static void sha256_final(SHA256_CTX *ctx, unsigned char hash[32]) {
unsigned char pad[64] = {0x80};
unsigned char count_bits[8];
uint64_t bits = ctx->count * 8;
size_t index = (size_t)(ctx->count & 0x3F);
size_t pad_len = (index < 56) ? (56 - index) : (120 - index);
for (int i = 0; i < 8; i++) count_bits[7-i] = (bits >> (i*8)) & 0xff;
sha256_update(ctx, pad, pad_len);
sha256_update(ctx, count_bits, 8);
for (int i = 0; i < 8; i++) {
hash[i*4] = (ctx->state[i] >> 24) & 0xff;
hash[i*4+1] = (ctx->state[i] >> 16) & 0xff;
hash[i*4+2] = (ctx->state[i] >> 8) & 0xff;
hash[i*4+3] = ctx->state[i] & 0xff;
}
}
// HMAC-SHA256
static char *hmac_sha256(const char *key, const char *message) {
if (!key || !message) return NULL;
unsigned char k_ipad[64], k_opad[64], tk[32];
size_t key_len = strlen(key);
if (key_len > 64) {
SHA256_CTX ctx;
sha256_init(&ctx);
sha256_update(&ctx, (unsigned char *)key, key_len);
sha256_final(&ctx, tk);
key = (char *)tk;
key_len = 32;
}
memset(k_ipad, 0x36, 64);
memset(k_opad, 0x5c, 64);
for (size_t i = 0; i < key_len; i++) {
k_ipad[i] ^= key[i];
k_opad[i] ^= key[i];
}
SHA256_CTX ctx;
unsigned char inner_hash[32], outer_hash[32];
sha256_init(&ctx);
sha256_update(&ctx, k_ipad, 64);
sha256_update(&ctx, (unsigned char *)message, strlen(message));
sha256_final(&ctx, inner_hash);
sha256_init(&ctx);
sha256_update(&ctx, k_opad, 64);
sha256_update(&ctx, inner_hash, 32);
sha256_final(&ctx, outer_hash);
char *result = malloc(65);
for (int i = 0; i < 32; i++)
sprintf(&result[i*2], "%02x", outer_hash[i]);
result[64] = '\0';
return result;
}
// Language detection (simplified)
static const char *detect_language(const char *filename) {
if (!filename) return NULL;
const char *ext = strrchr(filename, '.');
if (!ext) return NULL;
ext++;
if (strcmp(ext, "py") == 0) return "python";
if (strcmp(ext, "js") == 0) return "javascript";
if (strcmp(ext, "go") == 0) return "go";
if (strcmp(ext, "rb") == 0) return "ruby";
if (strcmp(ext, "rs") == 0) return "rust";
if (strcmp(ext, "c") == 0) return "c";
if (strcmp(ext, "cpp") == 0) return "cpp";
if (strcmp(ext, "java") == 0) return "java";
if (strcmp(ext, "php") == 0) return "php";
if (strcmp(ext, "pl") == 0) return "perl";
if (strcmp(ext, "lua") == 0) return "lua";
if (strcmp(ext, "sh") == 0) return "bash";
return NULL;
}
// ============================================================================
// Tests
// ============================================================================
void test_sha256(void) {
printf("\nTesting SHA-256...\n");
SHA256_CTX ctx;
unsigned char hash[32];
// Test known hash: SHA256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
sha256_init(&ctx);
sha256_update(&ctx, (unsigned char *)"hello", 5);
sha256_final(&ctx, hash);
if (hash[0] == 0x2c && hash[1] == 0xf2 && hash[2] == 0x4d && hash[3] == 0xba) {
PASS("Library: SHA-256('hello') correct");
if (version != NULL) {
PASS("unsandbox_version() returns non-NULL");
} else {
FAIL("Library: SHA-256('hello') mismatch");
FAIL("unsandbox_version() returned NULL");
return;
}
// Test empty string: SHA256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
sha256_init(&ctx);
sha256_update(&ctx, (unsigned char *)"", 0);
sha256_final(&ctx, hash);
if (hash[0] == 0xe3 && hash[1] == 0xb0 && hash[2] == 0xc4 && hash[3] == 0x42) {
PASS("Library: SHA-256('') correct");
if (strlen(version) > 0) {
PASS("unsandbox_version() returns non-empty string");
printf(" Version: %s\n", version);
} else {
FAIL("Library: SHA-256('') mismatch");
FAIL("unsandbox_version() returned empty string");
}
}
void test_hmac_sha256(void) {
printf("\nTesting HMAC-SHA256...\n");
// Test basic HMAC
char *hmac = hmac_sha256("key", "message");
if (hmac && strlen(hmac) == 64) {
PASS("Library: HMAC-SHA256 returns 64-char hex");
// Known value: HMAC-SHA256("key", "message") = 6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a
if (strncmp(hmac, "6e9ef29b75fffc5b7abae527d58fdadb", 32) == 0) {
PASS("Library: HMAC-SHA256 value correct");
} else {
FAIL("Library: HMAC-SHA256 value mismatch");
printf(" Got: %s\n", hmac);
}
free(hmac);
} else {
FAIL("Library: HMAC-SHA256 failed");
}
// Test NULL handling
hmac = hmac_sha256(NULL, "message");
if (hmac == NULL) {
PASS("Library: HMAC-SHA256(NULL, msg) returns NULL");
} else {
FAIL("Library: HMAC-SHA256 should reject NULL key");
free(hmac);
}
hmac = hmac_sha256("key", NULL);
if (hmac == NULL) {
PASS("Library: HMAC-SHA256(key, NULL) returns NULL");
} else {
FAIL("Library: HMAC-SHA256 should reject NULL message");
free(hmac);
}
}
/* ============================================================================
* Test: unsandbox_detect_language()
* ============================================================================ */
void test_detect_language(void) {
printf("\nTesting detect_language()...\n");
printf("\nTesting unsandbox_detect_language()...\n");
struct { const char *file; const char *expected; } tests[] = {
{"test.py", "python"},
@ -254,73 +66,218 @@ void test_detect_language(void) {
{"script.pl", "perl"},
{"init.lua", "lua"},
{"run.sh", "bash"},
{"main.ts", "typescript"},
{"app.kt", "kotlin"},
{"lib.ex", "elixir"},
{"main.hs", "haskell"},
{NULL, NULL}
};
for (int i = 0; tests[i].file; i++) {
const char *lang = detect_language(tests[i].file);
const char *lang = unsandbox_detect_language(tests[i].file);
if (lang && strcmp(lang, tests[i].expected) == 0) {
char msg[100];
snprintf(msg, sizeof(msg), "Library: detect_language('%s') -> '%s'", tests[i].file, tests[i].expected);
snprintf(msg, sizeof(msg), "detect_language('%s') -> '%s'", tests[i].file, tests[i].expected);
PASS(msg);
} else {
char msg[100];
snprintf(msg, sizeof(msg), "Library: detect_language('%s') failed (got '%s')", tests[i].file, lang ? lang : "NULL");
snprintf(msg, sizeof(msg), "detect_language('%s') expected '%s', got '%s'",
tests[i].file, tests[i].expected, lang ? lang : "NULL");
FAIL(msg);
}
}
// Test NULL
if (detect_language(NULL) == NULL) {
PASS("Library: detect_language(NULL) returns NULL");
/* Test NULL handling */
const char *null_result = unsandbox_detect_language(NULL);
if (null_result == NULL) {
PASS("detect_language(NULL) returns NULL");
} else {
FAIL("Library: detect_language(NULL) should return NULL");
FAIL("detect_language(NULL) should return NULL");
}
// Test unknown extension
const char *unknown = detect_language("file.xyz123");
/* Test unknown extension */
const char *unknown = unsandbox_detect_language("file.xyz123");
if (unknown == NULL) {
PASS("Library: detect_language('file.xyz123') returns NULL");
PASS("detect_language('file.xyz123') returns NULL for unknown");
} else {
SKIP("Library: detect_language handles unknown (returns something)");
char msg[100];
snprintf(msg, sizeof(msg), "detect_language('file.xyz123') expected NULL, got '%s'", unknown);
FAIL(msg);
}
/* Test no extension */
const char *noext = unsandbox_detect_language("Makefile");
if (noext == NULL) {
PASS("detect_language('Makefile') returns NULL (no extension)");
} else {
char msg[100];
snprintf(msg, sizeof(msg), "detect_language('Makefile') expected NULL, got '%s'", noext);
FAIL(msg);
}
}
/* ============================================================================
* Test: unsandbox_hmac_sign()
* ============================================================================ */
void test_hmac_sign(void) {
printf("\nTesting unsandbox_hmac_sign()...\n");
/* Test basic signature generation */
char *sig = unsandbox_hmac_sign("secret_key", "1234567890:POST:/execute:{}");
if (sig != NULL) {
PASS("unsandbox_hmac_sign() returns non-NULL");
} else {
FAIL("unsandbox_hmac_sign() returned NULL");
return;
}
if (strlen(sig) == 64) {
PASS("unsandbox_hmac_sign() returns 64-char hex string");
} else {
char msg[100];
snprintf(msg, sizeof(msg), "unsandbox_hmac_sign() returned %zu chars, expected 64", strlen(sig));
FAIL(msg);
}
/* Verify it's all hex characters */
int all_hex = 1;
for (int i = 0; i < 64 && sig[i]; i++) {
char c = sig[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
all_hex = 0;
break;
}
}
if (all_hex) {
PASS("unsandbox_hmac_sign() returns valid hex characters");
} else {
FAIL("unsandbox_hmac_sign() returned non-hex characters");
}
free(sig);
/* Test deterministic output - same input should produce same output */
char *sig1 = unsandbox_hmac_sign("key", "message");
char *sig2 = unsandbox_hmac_sign("key", "message");
if (sig1 && sig2 && strcmp(sig1, sig2) == 0) {
PASS("unsandbox_hmac_sign() is deterministic");
} else {
FAIL("unsandbox_hmac_sign() not deterministic");
}
free(sig1);
free(sig2);
/* Test different keys produce different signatures */
char *sig_a = unsandbox_hmac_sign("key_a", "message");
char *sig_b = unsandbox_hmac_sign("key_b", "message");
if (sig_a && sig_b && strcmp(sig_a, sig_b) != 0) {
PASS("Different keys produce different signatures");
} else {
FAIL("Different keys should produce different signatures");
}
free(sig_a);
free(sig_b);
/* Test different messages produce different signatures */
char *sig_m1 = unsandbox_hmac_sign("key", "message1");
char *sig_m2 = unsandbox_hmac_sign("key", "message2");
if (sig_m1 && sig_m2 && strcmp(sig_m1, sig_m2) != 0) {
PASS("Different messages produce different signatures");
} else {
FAIL("Different messages should produce different signatures");
}
free(sig_m1);
free(sig_m2);
/* Test NULL handling */
char *null_key = unsandbox_hmac_sign(NULL, "message");
if (null_key == NULL) {
PASS("unsandbox_hmac_sign(NULL, msg) returns NULL");
} else {
FAIL("unsandbox_hmac_sign(NULL, msg) should return NULL");
free(null_key);
}
char *null_msg = unsandbox_hmac_sign("key", NULL);
if (null_msg == NULL) {
PASS("unsandbox_hmac_sign(key, NULL) returns NULL");
} else {
FAIL("unsandbox_hmac_sign(key, NULL) should return NULL");
free(null_msg);
}
/* Test known HMAC value (RFC 4231 test vector) */
/* HMAC-SHA256("key", "message") should be a specific value */
char *known_sig = unsandbox_hmac_sign("key", "message");
if (known_sig) {
/* Expected: 6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a */
if (strncmp(known_sig, "6e9ef29b75fffc5b7abae527d58fdadb", 32) == 0) {
PASS("HMAC-SHA256('key', 'message') matches expected value");
} else {
printf(" Got: %s\n", known_sig);
printf(" Expected prefix: 6e9ef29b75fffc5b7abae527d58fdadb\n");
FAIL("HMAC-SHA256 value mismatch");
}
free(known_sig);
}
}
/* ============================================================================
* Test: Memory stress test
* ============================================================================ */
void test_memory(void) {
printf("\nTesting Memory Management...\n");
// Stress test HMAC allocation
/* Stress test HMAC allocation */
for (int i = 0; i < 1000; i++) {
char *hmac = hmac_sha256("key", "message");
if (hmac) free(hmac);
char *sig = unsandbox_hmac_sign("key", "message");
if (sig) free(sig);
}
PASS("Library: 1000 HMAC allocations without crash");
PASS("1000 HMAC allocations without crash");
// Stress test detect_language
/* Stress test language detection */
for (int i = 0; i < 1000; i++) {
detect_language("test.py");
unsandbox_detect_language("test.py");
}
PASS("Library: 1000 detect_language calls without crash");
PASS("1000 detect_language calls without crash");
/* Stress test version */
for (int i = 0; i < 1000; i++) {
unsandbox_version();
}
PASS("1000 version calls without crash");
}
// ============================================================================
// Main
// ============================================================================
/* ============================================================================
* Main
* ============================================================================ */
int main(void) {
printf("Library Mode Tests for un.c\n");
printf("============================\n");
printf("=====================================\n");
printf("UN C SDK - Unit Tests\n");
printf("Testing ACTUAL exported functions\n");
printf("=====================================\n");
test_sha256();
test_hmac_sha256();
test_version();
test_detect_language();
test_hmac_sign();
test_memory();
printf("\n============================\n");
printf("Library Mode Test Summary\n");
printf("============================\n");
printf("\n=====================================\n");
printf("Test Summary\n");
printf("=====================================\n");
printf("Passed: \033[32m%d\033[0m\n", tests_passed);
printf("Failed: \033[31m%d\033[0m\n", tests_failed);
printf("=====================================\n");
return tests_failed > 0 ? 1 : 0;
}

328
docs/TESTING.md Normal file
View file

@ -0,0 +1,328 @@
# SDK Testing Guidelines
This document defines the testing requirements for all un-inception SDK clients.
## Core Principle
**SDKs are LIBRARIES for embedding in other people's code.**
Every SDK must be:
1. **Importable** - Can be used as a library in other projects
2. **Testable** - Exports functions that tests can call directly
3. **Functional** - Actually works against the live API
## Three Testing Levels
### 1. Unit Tests (`test-library`)
**Purpose**: Test exported library functions in isolation.
**Requirements**:
- Test ACTUAL exported functions from the SDK
- NO mocking
- NO re-implementing functions locally
- Tests run without network access
- Tests run without API credentials
**What to test**:
- HMAC-SHA256 signature generation
- Request building (headers, body formatting)
- Response parsing (JSON → native types)
- Language detection from file extensions
- Error handling and edge cases
- Memory management (for C/C++/Rust)
**Example (C)**:
```c
#include "un.h"
void test_hmac_signature() {
// Test the REAL exported function
char *sig = unsandbox_hmac_sign("secret", "1234567890:POST:/execute:{}");
assert(sig != NULL);
assert(strlen(sig) == 64); // hex-encoded SHA256
free(sig);
}
void test_language_detection() {
assert(strcmp(unsandbox_detect_language("test.py"), "python") == 0);
assert(strcmp(unsandbox_detect_language("main.go"), "go") == 0);
assert(unsandbox_detect_language("unknown.xyz") == NULL);
}
```
**Example (Python)**:
```python
from un import UnsandboxClient, hmac_sign, detect_language
def test_hmac_signature():
sig = hmac_sign("secret", "1234567890:POST:/execute:{}")
assert len(sig) == 64
assert sig == "expected_hex_value"
def test_language_detection():
assert detect_language("test.py") == "python"
assert detect_language("main.go") == "go"
assert detect_language("unknown.xyz") is None
```
### 2. Integration Tests (`test-integration`)
**Purpose**: Test that SDK components work together correctly.
**Requirements**:
- Test internal SDK consistency
- May use test doubles for HTTP layer
- Should NOT call live API
- Tests the full request/response cycle internally
**What to test**:
- Auth headers are generated correctly together
- Request body is properly formatted
- Response parsing handles all expected formats
- Error responses are properly converted to exceptions/errors
- Async operations work correctly (if applicable)
**Example (Python)**:
```python
def test_auth_headers_integration():
client = UnsandboxClient(public_key="pk", secret_key="sk")
headers = client._build_auth_headers("POST", "/execute", '{"code":"x"}')
assert "Authorization" in headers
assert "X-Timestamp" in headers
assert "X-Signature" in headers
assert headers["Authorization"] == "Bearer pk"
def test_request_building():
client = UnsandboxClient(public_key="pk", secret_key="sk")
req = client._build_execute_request("python", "print(1)")
assert req["method"] == "POST"
assert req["path"] == "/execute"
assert "language" in req["body"]
assert req["body"]["language"] == "python"
```
### 3. Functional Tests (`test-functional`)
**Purpose**: Test the SDK against the live API.
**Requirements**:
- Requires `UNSANDBOX_PUBLIC_KEY` and `UNSANDBOX_SECRET_KEY` environment variables
- Makes REAL API calls to api.unsandbox.com
- Tests the complete lifecycle: execute, sessions, services
- Should be skipped gracefully if credentials not available
**What to test**:
- Execute code in multiple languages
- Create/list/destroy sessions
- Create/list/destroy services
- Error handling for invalid requests
- Rate limiting behavior
**Example (Python)**:
```python
import os
import pytest
@pytest.fixture
def client():
pk = os.environ.get("UNSANDBOX_PUBLIC_KEY")
sk = os.environ.get("UNSANDBOX_SECRET_KEY")
if not pk or not sk:
pytest.skip("API credentials not set")
return UnsandboxClient(public_key=pk, secret_key=sk)
def test_execute_python(client):
result = client.execute("python", "print(42)")
assert result.exit_code == 0
assert "42" in result.stdout
def test_execute_invalid_language(client):
with pytest.raises(UnsandboxError) as exc:
client.execute("not_a_real_language", "code")
assert "unsupported" in str(exc.value).lower()
def test_session_lifecycle(client):
# Create
session = client.session_create()
assert session.id is not None
# Execute in session
result = client.session_execute(session.id, "echo hello")
assert "hello" in result.stdout
# Destroy
client.session_destroy(session.id)
```
## Test Directory Structure
```
clients/{language}/
├── src/
│ ├── un.{ext} # Main implementation
│ └── un.h # Header (for C/C++)
├── tests/
│ ├── unit/
│ │ ├── test_hmac.{ext}
│ │ ├── test_language_detection.{ext}
│ │ └── test_request_building.{ext}
│ ├── integration/
│ │ ├── test_auth_flow.{ext}
│ │ └── test_response_parsing.{ext}
│ └── functional/
│ ├── test_execute.{ext}
│ ├── test_sessions.{ext}
│ └── test_services.{ext}
├── Makefile
└── README.md
```
## Makefile Requirements
Every client Makefile MUST implement these targets:
```makefile
.PHONY: test test-cli test-library test-integration test-functional clean
# Run all tests
test: test-cli test-library test-integration test-functional
@echo "All tests complete"
# Test CLI mode - binary runs, --help works
test-cli:
@echo "Testing CLI mode..."
./un --help >/dev/null
./un --version >/dev/null
# Test library mode - unit tests of exported functions
test-library:
@echo "Testing library exports..."
./run_unit_tests
# Test integration - SDK internal consistency
test-integration:
@echo "Testing SDK integration..."
./run_integration_tests
# Test functional - real API calls
test-functional:
@echo "Testing against live API..."
@if [ -z "$$UNSANDBOX_PUBLIC_KEY" ]; then \
echo " Skipped (no credentials)"; \
else \
./run_functional_tests; \
fi
```
## Anti-Patterns (FORBIDDEN)
### 1. Re-implementing Functions Locally
```c
// ❌ WRONG - This tests a LOCAL copy, not the SDK
static void local_sha256_transform(...) { ... }
void test_sha256() {
// Testing LOCAL function, not the one in un.c!
local_sha256_transform(...);
}
```
### 2. Mocking Everything
```python
# ❌ WRONG - This doesn't test the real SDK behavior
@mock.patch('un.requests.post')
def test_execute(mock_post):
mock_post.return_value = Mock(json=lambda: {"stdout": "42"})
# This tests the mock, not the SDK!
```
### 3. Tests Without Assertions
```python
# ❌ WRONG - This doesn't actually verify anything
def test_execute():
client = UnsandboxClient()
client.execute("python", "print(1)")
# No assertion! Test always passes!
```
### 4. Skipping Test Levels
```makefile
# ❌ WRONG - Missing test levels
test: test-functional # Only functional tests? No unit/integration!
```
## Language-Specific Guidelines
### C/C++
- Use `#ifndef UNSANDBOX_LIBRARY` to guard `main()` for library builds
- Export functions without `static` keyword when built as library
- Tests link against the compiled library, not source directly
- Use assertion macros or a test framework (Unity, CUnit)
### Python
- Use pytest for all test levels
- Export functions at module level (not just class methods)
- Use `__all__` to define public API
### JavaScript/TypeScript
- Use Jest or Mocha for testing
- Export functions via `module.exports` or ES6 `export`
- Test both CommonJS and ESM imports if supporting both
### Go
- Use standard `testing` package
- Export functions with capital letters
- Tests in `*_test.go` files
### Rust
- Use `#[cfg(test)]` modules
- Export public API with `pub` keyword
- Use `cargo test` for all test levels
## CI Integration
Tests are run automatically on push via GitLab CI:
```yaml
test-{language}:
stage: test
script:
- cd clients/{language}
- make test-cli
- make test-library
- make test-integration
rules:
- changes:
- clients/{language}/**/*
test-{language}-functional:
stage: functional
script:
- cd clients/{language}
- make test-functional
rules:
- changes:
- clients/{language}/**/*
variables:
UNSANDBOX_PUBLIC_KEY: $CI_UNSANDBOX_PUBLIC_KEY
UNSANDBOX_SECRET_KEY: $CI_UNSANDBOX_SECRET_KEY
```
## Summary
1. **Unit tests** - Test exported functions, no mocking
2. **Integration tests** - Test SDK internals work together
3. **Functional tests** - Test against live API
4. **All three levels are REQUIRED** for each SDK
5. **NO re-implementing functions locally** - test the REAL code