diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6632b92..9e78ded 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,3 +6,7 @@ add_subdirectory(afp) add_subdirectory(salvage) + +if(ENABLE_DIRECTORY) + add_subdirectory(flaim) +endif() diff --git a/tests/flaim/CMakeLists.txt b/tests/flaim/CMakeLists.txt new file mode 100644 index 0000000..eeaccdb --- /dev/null +++ b/tests/flaim/CMakeLists.txt @@ -0,0 +1,45 @@ +# mars-nwe FLAIM integration tests. +# +# These tests live in the mars-nwe root test tree instead of the imported FLAIM +# submodule because they validate the complete mars-nwe integration contract: +# nw-prefixed libraries, nwssl-backed OpenSSL compatibility, MatrixSSL crypto +# configuration propagation, and the FLAIM stack used by nwdirectory. + +if(NOT ENABLE_DIRECTORY) + message(STATUS "mars-nwe FLAIM tests disabled: ENABLE_DIRECTORY is OFF") + return() +endif() + +if(NOT TARGET flaim) + message(FATAL_ERROR "mars-nwe FLAIM tests require the flaim target") +endif() + +function(mars_nwe_add_flaim_test target source library) + add_executable(${target} ${source}) + target_link_libraries(${target} PRIVATE ${library}) + set_target_properties(${target} PROPERTIES CXX_STANDARD 98 CXX_STANDARD_REQUIRED YES) +endfunction() + +mars_nwe_add_flaim_test(mars_nwe_flaim_api_smoke flaim_api_smoke.cpp flaim) +add_test(NAME mars_nwe.flaim.api-create-query-encrypt + COMMAND mars_nwe_flaim_api_smoke ${CMAKE_CURRENT_BINARY_DIR}/flaim-api) +set_tests_properties(mars_nwe.flaim.api-create-query-encrypt PROPERTIES + LABELS "mars_nwe;flaim" + ENVIRONMENT "TERM=xterm") + +if(TARGET xflaim) + mars_nwe_add_flaim_test(mars_nwe_xflaim_api_smoke xflaim_api_smoke.cpp xflaim) + add_test(NAME mars_nwe.xflaim.api-alloc + COMMAND mars_nwe_xflaim_api_smoke) + set_tests_properties(mars_nwe.xflaim.api-alloc PROPERTIES + LABELS "mars_nwe;flaim;xflaim" + ENVIRONMENT "TERM=xterm") +endif() + +if(TARGET flaimsql) + mars_nwe_add_flaim_test(mars_nwe_flaimsql_header_link_smoke flaimsql_header_link_smoke.cpp flaimsql) + add_test(NAME mars_nwe.flaimsql.header-link + COMMAND mars_nwe_flaimsql_header_link_smoke) + set_tests_properties(mars_nwe.flaimsql.header-link PROPERTIES + LABELS "mars_nwe;flaim;flaimsql") +endif() diff --git a/tests/flaim/flaim_api_smoke.cpp b/tests/flaim/flaim_api_smoke.cpp new file mode 100644 index 0000000..c1a606f --- /dev/null +++ b/tests/flaim/flaim_api_smoke.cpp @@ -0,0 +1,287 @@ +#include "flaim.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define PERSON_TAG 1 +#define LAST_NAME_TAG 2 +#define FIRST_NAME_TAG 3 +#define SECRET_TAG 4 +#define AGE_TAG 5 +#define SECRET_ENCDEF 10 + +static const char *kDictionary = + "0 @10@ EncDef SecretKey\n" + " 1 type aes\n" + "0 @1@ field Person\n" + " 1 type text\n" + "0 @2@ field LastName\n" + " 1 type text\n" + "0 @3@ field FirstName\n" + " 1 type text\n" + "0 @4@ field Secret\n" + " 1 type text\n" + " 1 encdef 10\n" + "0 @5@ field Age\n" + " 1 type number\n" + "0 @100@ index LastFirst_IX\n" + " 1 language US\n" + " 1 key\n" + " 2 field 2\n" + " 3 required\n" + " 2 field 3\n" + " 3 required\n"; + +static const char *kSecret = "mars-nwe-flaim-at-rest-secret-value"; + +static void fail(const char *what, RCODE rc) +{ + if (rc) + { + fprintf(stderr, "FAIL: %s: 0x%04x %s\n", what, (unsigned)rc, + (const char *)FlmErrorString(rc)); + } + else + { + fprintf(stderr, "FAIL: %s\n", what); + } + exit(1); +} + +static void ensure_dir(const char *path) +{ + if (mkdir(path, 0700) != 0 && errno != EEXIST) + { + perror(path); + exit(1); + } +} + +static void rm_rf(const char *path) +{ + DIR *dir = opendir(path); + if (!dir) + { + unlink(path); + return; + } + + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) + { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + { + continue; + } + char child[4096]; + snprintf(child, sizeof(child), "%s/%s", path, ent->d_name); + rm_rf(child); + } + closedir(dir); + rmdir(path); +} + +static int file_contains(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) + { + return 0; + } + + const size_t nlen = strlen(needle); + unsigned char buf[8192]; + size_t overlap = 0; + int found = 0; + + while (!found) + { + size_t got = fread(buf + overlap, 1, sizeof(buf) - overlap, f); + if (got == 0) + { + break; + } + size_t total = overlap + got; + for (size_t i = 0; i + nlen <= total; ++i) + { + if (memcmp(buf + i, needle, nlen) == 0) + { + found = 1; + break; + } + } + if (nlen > 1 && total >= nlen - 1) + { + overlap = nlen - 1; + memmove(buf, buf + total - overlap, overlap); + } + else + { + overlap = total; + memmove(buf, buf, overlap); + } + } + + fclose(f); + return found; +} + +static void assert_secret_not_plaintext(const char *dir) +{ + DIR *d = opendir(dir); + if (!d) + { + perror(dir); + exit(1); + } + + struct dirent *ent; + while ((ent = readdir(d)) != NULL) + { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) + { + continue; + } + char path[4096]; + snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); + struct stat st; + if (stat(path, &st) != 0) + { + continue; + } + if (S_ISDIR(st.st_mode)) + { + assert_secret_not_plaintext(path); + } + else if (S_ISREG(st.st_mode) && file_contains(path, kSecret)) + { + fprintf(stderr, "FAIL: encrypted FLAIM payload appears as plaintext in %s\n", path); + exit(1); + } + } + closedir(d); +} + +static void add_person(HFDB hDb, FLMUINT *drn_out) +{ + FlmRecord *rec = NULL; + void *field = NULL; + RCODE rc; + + rec = f_new FlmRecord; + if (!rec) + { + fail("alloc FlmRecord", FERR_MEM); + } + + if (RC_BAD(rc = rec->insertLast(0, PERSON_TAG, FLM_TEXT_TYPE, NULL))) fail("insert root", rc); + if (RC_BAD(rc = rec->insertLast(1, FIRST_NAME_TAG, FLM_TEXT_TYPE, &field))) fail("insert first", rc); + if (RC_BAD(rc = rec->setNative(field, "Mars"))) fail("set first", rc); + if (RC_BAD(rc = rec->insertLast(1, LAST_NAME_TAG, FLM_TEXT_TYPE, &field))) fail("insert last", rc); + if (RC_BAD(rc = rec->setNative(field, "NWE"))) fail("set last", rc); + if (RC_BAD(rc = rec->insertLast(1, SECRET_TAG, FLM_TEXT_TYPE, &field))) fail("insert secret", rc); + if (RC_BAD(rc = rec->setNative(field, kSecret, SECRET_ENCDEF))) fail("set encrypted secret", rc); + if (!rec->isEncryptedField(field)) fail("secret field was not marked encrypted", 0); + if (RC_BAD(rc = rec->insertLast(1, AGE_TAG, FLM_NUMBER_TYPE, &field))) fail("insert age", rc); + if (RC_BAD(rc = rec->setUINT(field, 28))) fail("set age", rc); + + FLMUINT drn = 0; + if (RC_BAD(rc = FlmRecordAdd(hDb, FLM_DATA_CONTAINER, &drn, rec, 0))) fail("FlmRecordAdd", rc); + *drn_out = drn; + rec->Release(); +} + +static void verify_person(HFDB hDb, FLMUINT drn) +{ + RCODE rc; + FlmRecord *rec = NULL; + if (RC_BAD(rc = FlmRecordRetrieve(hDb, FLM_DATA_CONTAINER, drn, FO_EXACT, &rec, NULL))) + { + fail("FlmRecordRetrieve", rc); + } + + char value[128]; + FLMUINT len = sizeof(value); + void *field = rec->find(rec->root(), SECRET_TAG); + if (!field) fail("missing secret field", 0); + if (RC_BAD(rc = rec->getNative(field, value, &len))) fail("decrypt secret", rc); + if (strcmp(value, kSecret) != 0) fail("decrypted secret value mismatch", 0); + if (!rec->isEncryptedField(field)) fail("retrieved secret field was not encrypted", 0); + + field = rec->find(rec->root(), AGE_TAG); + if (!field) fail("missing age field", 0); + FLMUINT age = 0; + if (RC_BAD(rc = rec->getUINT(field, &age))) fail("get age", rc); + if (age != 28) fail("age value mismatch", 0); + + rec->Release(); +} + +static void verify_cursor(HFDB hDb) +{ + HFCURSOR cursor = HFCURSOR_NULL; + RCODE rc; + FLMBYTE value[64]; + FlmRecord *rec = NULL; + + if (RC_BAD(rc = FlmCursorInit(hDb, FLM_DATA_CONTAINER, &cursor))) fail("FlmCursorInit", rc); + if (RC_BAD(rc = FlmCursorAddField(cursor, LAST_NAME_TAG, 0))) fail("cursor last field", rc); + if (RC_BAD(rc = FlmCursorAddOp(cursor, FLM_EQ_OP))) fail("cursor last eq", rc); + f_sprintf((char *)value, "NWE"); + if (RC_BAD(rc = FlmCursorAddValue(cursor, FLM_STRING_VAL, value, 0))) fail("cursor last value", rc); + if (RC_BAD(rc = FlmCursorAddOp(cursor, FLM_AND_OP))) fail("cursor and", rc); + if (RC_BAD(rc = FlmCursorAddField(cursor, FIRST_NAME_TAG, 0))) fail("cursor first field", rc); + if (RC_BAD(rc = FlmCursorAddOp(cursor, FLM_EQ_OP))) fail("cursor first eq", rc); + f_sprintf((char *)value, "Mars"); + if (RC_BAD(rc = FlmCursorAddValue(cursor, FLM_STRING_VAL, value, 0))) fail("cursor first value", rc); + if (RC_BAD(rc = FlmCursorFirst(cursor, &rec))) fail("FlmCursorFirst", rc); + if (rec) rec->Release(); + FlmCursorFree(&cursor); +} + +int main(int argc, char **argv) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s WORKDIR\n", argv[0]); + return 2; + } + + rm_rf(argv[1]); + ensure_dir(argv[1]); + + char db[4096], data[4096], rfl[4096]; + snprintf(db, sizeof(db), "%s/classic.db", argv[1]); + snprintf(data, sizeof(data), "%s/data", argv[1]); + snprintf(rfl, sizeof(rfl), "%s/rfl", argv[1]); + ensure_dir(data); + ensure_dir(rfl); + + RCODE rc; + HFDB hDb = HFDB_NULL; + FLMUINT drn = 0; + + if (RC_BAD(rc = FlmStartup())) fail("FlmStartup", rc); + if (RC_BAD(rc = FlmDbCreate(db, data, rfl, NULL, kDictionary, NULL, &hDb))) fail("FlmDbCreate", rc); + if (RC_BAD(rc = FlmDbTransBegin(hDb, FLM_UPDATE_TRANS, 15))) fail("FlmDbTransBegin", rc); + add_person(hDb, &drn); + if (RC_BAD(rc = FlmDbTransCommit(hDb))) fail("FlmDbTransCommit", rc); + verify_person(hDb, drn); + verify_cursor(hDb); + FlmDbClose(&hDb); + + if (RC_BAD(rc = FlmDbOpen(db, data, rfl, 0, NULL, &hDb))) fail("FlmDbOpen", rc); + verify_person(hDb, drn); + FlmDbClose(&hDb); + FlmShutdown(); + + assert_secret_not_plaintext(argv[1]); + printf("mars-nwe FLAIM classic API/encryption smoke passed: %s\n", db); + return 0; +} diff --git a/tests/flaim/flaimsql_header_link_smoke.cpp b/tests/flaim/flaimsql_header_link_smoke.cpp new file mode 100644 index 0000000..06ae232 --- /dev/null +++ b/tests/flaim/flaimsql_header_link_smoke.cpp @@ -0,0 +1,18 @@ +#include "flaimsql.h" + +#include +#include + +int main(void) +{ + SQL_STATS stats; + memset(&stats, 0, sizeof(stats)); + stats.eErrorType = SQL_NO_ERROR; + if (stats.eErrorType != SQL_NO_ERROR) + { + fprintf(stderr, "unexpected SQL_STATS value\n"); + return 1; + } + printf("mars-nwe FlaimSQL header/link smoke passed\n"); + return 0; +} diff --git a/tests/flaim/xflaim_api_smoke.cpp b/tests/flaim/xflaim_api_smoke.cpp new file mode 100644 index 0000000..9d6b64f --- /dev/null +++ b/tests/flaim/xflaim_api_smoke.cpp @@ -0,0 +1,17 @@ +#include "xflaim.h" + +#include + +int main(void) +{ + IF_DbSystem *dbSystem = NULL; + RCODE rc = FlmAllocDbSystem(&dbSystem); + if (RC_BAD(rc) || dbSystem == NULL) + { + fprintf(stderr, "FlmAllocDbSystem failed: 0x%04x\n", (unsigned)rc); + return 1; + } + dbSystem->Release(); + printf("mars-nwe XFLAIM allocation smoke passed\n"); + return 0; +}