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:
parent
ebdc590636
commit
1880dba054
5 changed files with 699 additions and 246 deletions
78
CLAUDE.md
78
CLAUDE.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
328
docs/TESTING.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue