Chrysostomos Nanakos 067fbd99f4
fix(discovery): togglePrivateQueries refuses enable when mix not set up (#1462)
Signed-off-by: Chrysostomos Nanakos <chris@include.gr>
2026-06-19 17:58:04 +00:00

988 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>
#include "../../library/libstorage.h"
/* Provide realpath on Windows (not available on some MSVC/MinGW setups) */
#if defined(_WIN32) || defined(_WIN64)
#include <limits.h>
#if defined(_MSC_VER)
#include <direct.h>
#define realpath(N,R) _fullpath((R),(N),_MAX_PATH)
#else
/* MinGW / other Windows gcc: map to _fullpath using PATH_MAX */
#include <stdlib.h>
#define realpath(N,R) _fullpath((R),(N),PATH_MAX)
#endif
#endif
#define GRN "\033[0;32m"
#define RED "\033[0;31m"
#define YEL "\033[0;33m"
#define NC "\033[0m" // No Color
#define BEGIN_SUITE int passed = 0;
// RUN_TEST runs a test expression, printing the test name as it executes.
#define RUN_TEST(expr) \
do \
{ \
printf(YEL "[RUN] %s" NC "... ", #expr); \
fflush(stdout); \
if ((expr) != RET_OK) \
{ \
fprintf(stderr, RED "[FAIL]\n" NC); \
fprintf(stderr, RED "FAIL. Tests run: %d\n" NC, passed + 1); \
return RET_ERR; \
} \
printf(GRN "[PASS]\n" NC); \
passed += 1; \
fflush(stdout); \
} while (0)
#define END_SUITE printf(GRN "SUCCESS. Tests passed: %d\n" NC, passed + 1); \
fflush(stdout);
// We need 250 as max retries mainly for the start function in CI.
// Other functions should be not need that many retries.
#define MAX_RETRIES 250
typedef struct
{
pthread_mutex_t mutex;
pthread_cond_t cond;
bool done;
int ret;
char *msg;
char *chunk;
size_t len;
} Resp;
static Resp *alloc_resp(void)
{
Resp *r = (Resp *)calloc(1, sizeof(Resp));
pthread_mutex_init(&r->mutex, NULL);
pthread_cond_init(&r->cond, NULL);
r->done = false;
r->msg = NULL;
r->chunk = NULL;
r->ret = -1;
return r;
}
static void free_resp(Resp *r)
{
if (!r)
{
return;
}
if (r->msg)
{
free(r->msg);
}
if (r->chunk)
{
free(r->chunk);
}
pthread_cond_destroy(&r->cond);
pthread_mutex_destroy(&r->mutex);
free(r);
}
static int get_ret(Resp *r)
{
if (!r)
{
return RET_ERR;
}
pthread_mutex_lock(&r->mutex);
int ret = r->ret;
pthread_mutex_unlock(&r->mutex);
return ret;
}
// wait_resp waits until the async response is ready or max retries is reached.
// The resp is initially set to -1, to any code (RET_OK, RET_ERR, RET_PROGRESS) will
// indicate that the response is ready to be consumed.
static void wait_resp(Resp *r)
{
if (!r)
{
return;
}
const long timeout_ms = MAX_RETRIES * 100;
struct timespec deadline;
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec += timeout_ms / 1000;
deadline.tv_nsec += (timeout_ms % 1000) * 1000000;
if (deadline.tv_nsec >= 1000000000)
{
deadline.tv_sec += 1;
deadline.tv_nsec -= 1000000000;
}
pthread_mutex_lock(&r->mutex);
while (!r->done)
{
int rc = pthread_cond_timedwait(&r->cond, &r->mutex, &deadline);
if (rc == ETIMEDOUT)
{
break;
}
}
pthread_mutex_unlock(&r->mutex);
}
// is_resp_ok checks if the async response indicates success.
// It will wait first for the response to be ready.
// Then it will copy the message or chunk to res if provided.
static int is_resp_ok(Resp *r, char **res)
{
if (!r)
{
return RET_ERR;
}
wait_resp(r);
pthread_mutex_lock(&r->mutex);
int ret = (r->ret == RET_OK) ? RET_OK : RET_ERR;
// If a response pointer is provided, its safe to initialize it to NULL.
if (res)
{
*res = NULL;
}
// If the response contains a chunk (for a download or an upload with RET_PROGRESS),
// the response will be in chunk.
// Otherwise, the response will be in msg.
if (res && r->chunk)
{
*res = strdup(r->chunk);
}
else if (res && r->msg)
{
*res = strdup(r->msg);
}
pthread_mutex_unlock(&r->mutex);
free_resp(r);
return ret;
}
// callback is the function that will be called by the storage library
// when an async operation is completed or has progress to report.
// - ret is the return code of the callback
// - msg is the data returned by the callback: it can be a string or a chunk
// - len is the size of that data
// - userData is the bridge between the caller and the lib.
// The caller passes this userData to the library.
// When the library invokes the callback, it passes the same userData back. The callback
// then fills it with the received information (return code, message). Once the callback
// has completed, the caller can read the populated userData.
static void callback(int ret, const char *msg, size_t len, void *userData)
{
Resp *r = (Resp *)userData;
// This means that the caller did not provide a valid userData pointer.
// In that case, we have nothing to do but return.
if (!r)
{
return;
}
pthread_mutex_lock(&r->mutex);
// If the reponse already has a message, just free it first.
if (r->msg)
{
free(r->msg);
r->msg = NULL;
r->len = 0;
}
// For a RET_PROGRESS with chunk, copy the chunk data directly.
// This is used for upload/download chunk progress.
if (ret == RET_PROGRESS && msg && len > 0 && r->chunk)
{
memcpy(r->chunk, msg, len);
r->len = len;
}
// For terminal responses, copy the message data.
if (ret != RET_PROGRESS && msg && len > 0)
{
// Allocate memory for the message plus null terminator.
r->msg = (char *)malloc(len + 1);
// Just in case malloc fails.
if (!r->msg)
{
r->len = 0;
r->ret = RET_ERR;
r->done = true;
pthread_cond_signal(&r->cond);
pthread_mutex_unlock(&r->mutex);
return;
}
memcpy(r->msg, msg, len);
// Null terminate is needed here otherwise
// the msg will contains non valid string like "0<> :g"
r->msg[len] = '\0';
r->len = len;
}
else if (ret != RET_PROGRESS)
{
r->msg = NULL;
r->len = 0;
}
// Progress updates are intermediate callbacks. Keep any copied chunk data,
// but wait for the final RET_OK/RET_ERR before completing the response.
if (ret == RET_PROGRESS)
{
pthread_mutex_unlock(&r->mutex);
return;
}
// Publish completion last so wait_resp can only observe a fully written Resp.
r->ret = ret;
r->done = true;
pthread_cond_signal(&r->cond);
pthread_mutex_unlock(&r->mutex);
}
static int read_file(const char *filepath, char **res)
{
FILE *file;
char c;
// Just read first 100 bytes for the test
char content[100];
file = fopen(filepath, "r");
if (file == NULL)
{
return RET_ERR;
}
fgets(content, 100, file);
*res = strdup(content);
fclose(file);
return RET_OK;
}
int setup(void **storage_ctx)
{
// Initialize Nim runtime
extern void libstorageNimMain(void);
libstorageNimMain();
Resp *r = alloc_resp();
const char *cfg = "{\"log-level\":\"WARN\",\"data-dir\":\"./data-dir\"}";
void *ctx = storage_new(cfg, (StorageCallback)callback, r);
if (!ctx)
{
free_resp(r);
return RET_ERR;
}
wait_resp(r);
if (get_ret(r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
(*storage_ctx) = ctx;
free_resp(r);
return RET_OK;
}
int start(void *storage_ctx)
{
Resp *r = alloc_resp();
if (storage_start(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
return is_resp_ok(r, NULL);
}
int cleanup(void *storage_ctx)
{
Resp *r = alloc_resp();
// Stop node
if (storage_stop(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, NULL) != RET_OK)
{
return RET_ERR;
}
r = alloc_resp();
// Close node
if (storage_close(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, NULL) != RET_OK)
{
return RET_ERR;
}
// Destroy node
// No need to wait here as storage_destroy is synchronous
if (storage_destroy(storage_ctx) != RET_OK)
{
return RET_ERR;
}
return RET_OK;
}
int check_version(void *storage_ctx)
{
char *version = storage_version(storage_ctx);
printf("version: %s\n", version);
free(version);
return RET_OK;
}
int check_repo(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_repo(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (res == NULL || strcmp(res, "./data-dir") != 0)
{
printf("repo mismatch: %s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_debug(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_debug(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
// Simple check to ensure the response contains spr
if (res == NULL || strstr(res, "spr") == NULL)
{
fprintf(stderr, "debug content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_spr(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_spr(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (res == NULL || strstr(res, "spr") == NULL)
{
fprintf(stderr, "spr content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_peer_id(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_peer_id(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
return is_resp_ok(r, &res);
}
int update_log_level(void *storage_ctx, const char *log_level)
{
char *res = NULL;
Resp *r = alloc_resp();
if (storage_log_level(storage_ctx, log_level, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
return is_resp_ok(r, NULL);
}
int check_upload_chunk(void *storage_ctx, const char *filepath)
{
Resp *r = alloc_resp();
char *res = NULL;
char *session_id = NULL;
const char *payload = "hello world";
size_t chunk_size = strlen(payload);
if (storage_upload_init(storage_ctx, filepath, chunk_size, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, &session_id) != RET_OK)
{
return RET_ERR;
}
uint8_t *chunk = malloc(chunk_size);
if (!chunk)
{
free(session_id);
return RET_ERR;
}
memcpy(chunk, payload, chunk_size);
r = alloc_resp();
if (storage_upload_chunk(storage_ctx, session_id, chunk, chunk_size, (StorageCallback)callback, r) != RET_OK)
{
free(session_id);
free_resp(r);
free(chunk);
return RET_ERR;
}
if (is_resp_ok(r, NULL) != RET_OK)
{
free(session_id);
free(chunk);
return RET_ERR;
}
free(chunk);
r = alloc_resp();
if (storage_upload_finalize(storage_ctx, session_id, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
free(session_id);
return RET_ERR;
}
free(session_id);
int ret = is_resp_ok(r, &res);
if (res == NULL || strlen(res) == 0)
{
fprintf(stderr, "CID is missing\n");
ret = RET_ERR;
}
free(res);
return ret;
}
int upload_cancel(void *storage_ctx)
{
Resp *r = alloc_resp();
char *session_id = NULL;
size_t chunk_size = 64 * 1024;
if (storage_upload_init(storage_ctx, "hello.txt", chunk_size, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, &session_id) != RET_OK)
{
return RET_ERR;
}
r = alloc_resp();
if (storage_upload_cancel(storage_ctx, session_id, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
free(session_id);
return RET_ERR;
}
free(session_id);
return is_resp_ok(r, NULL);
}
int check_upload_file(void *storage_ctx, const char *filepath, char **res)
{
Resp *r = alloc_resp();
char *session_id = NULL;
size_t chunk_size = 64 * 1024;
if (storage_upload_init(storage_ctx, filepath, chunk_size, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, &session_id) != RET_OK)
{
return RET_ERR;
}
r = alloc_resp();
if (storage_upload_file(storage_ctx, session_id, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
free(session_id);
return RET_ERR;
}
free(session_id);
int ret = is_resp_ok(r, res);
if (res == NULL || *res == NULL || strlen(*res) == 0)
{
fprintf(stderr, "CID is missing\n");
return RET_ERR;
}
return ret;
}
int check_download_stream(void *storage_ctx, const char *cid, const char *filepath)
{
Resp *r = alloc_resp();
char *res = NULL;
size_t chunk_size = 64 * 1024;
bool local = true;
if (storage_download_init(storage_ctx, cid, chunk_size, local, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, NULL) != RET_OK)
{
return RET_ERR;
}
r = alloc_resp();
r->chunk = malloc(chunk_size + 1);
if (storage_download_stream(storage_ctx, cid, chunk_size, local, filepath, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (res == NULL || strncmp(res, "Hello World!", strlen("Hello World!")) != 0)
{
fprintf(stderr, "downloaded content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
if (read_file("downloaded_hello.txt", &res) != RET_OK)
{
fprintf(stderr, "read downloaded file failed\n");
ret = RET_ERR;
}
if (res == NULL || strncmp(res, "Hello World!", strlen("Hello World!")) != 0)
{
fprintf(stderr, "downloaded content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_download_chunk(void *storage_ctx, const char *cid)
{
Resp *r = alloc_resp();
char *res = NULL;
size_t chunk_size = 64 * 1024;
bool local = true;
if (storage_download_init(storage_ctx, cid, chunk_size, local, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
if (is_resp_ok(r, NULL) != RET_OK)
{
return RET_ERR;
}
r = alloc_resp();
r->chunk = malloc(chunk_size + 1);
if (storage_download_chunk(storage_ctx, cid, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (res == NULL || strncmp(res, "Hello World!", strlen("Hello World!")) != 0)
{
fprintf(stderr, "downloaded chunk content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_download_cancel(void *storage_ctx, const char *cid)
{
Resp *r = alloc_resp();
if (storage_download_cancel(storage_ctx, cid, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
return is_resp_ok(r, NULL);
}
int check_download_manifest(void *storage_ctx, const char *cid)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_download_manifest(storage_ctx, cid, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
const char *expected_manifest = "{\"manifestVersion\":0,\"treeCid\":\"zDzSvJTf8JYwvysKPmG7BtzpbiAHfuwFMRphxm4hdvnMJ4XPJjKX\",\"datasetSize\":12,\"blockSize\":65536,\"filename\":\"hello_world.txt\",\"mimetype\":\"text/plain\"}";
if (res == NULL || strncmp(res, expected_manifest, strlen(expected_manifest)) != 0)
{
fprintf(stderr, "downloaded manifest content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_list(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_list(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
const char *expected_manifest = "{\"manifestVersion\":0,\"treeCid\":\"zDzSvJTf8JYwvysKPmG7BtzpbiAHfuwFMRphxm4hdvnMJ4XPJjKX\",\"datasetSize\":12,\"blockSize\":65536,\"filename\":\"hello_world.txt\",\"mimetype\":\"text/plain\"}";
if (res == NULL || strstr(res, expected_manifest) == NULL)
{
fprintf(stderr, "downloaded manifest content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_space(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_space(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
// Simple check to ensure the response contains totalBlocks
if (res == NULL || strstr(res, "totalBlocks") == NULL)
{
fprintf(stderr, "space content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
free(res);
return ret;
}
int check_exists(void *storage_ctx, const char *cid, bool expected)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_exists(storage_ctx, cid, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (expected)
{
if (res == NULL || strcmp(res, "true") != 0)
{
fprintf(stderr, "exists content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
}
else
{
if (res == NULL || strcmp(res, "false") != 0)
{
fprintf(stderr, "exists content mismatch, res:%s\n", res ? res : "(null)");
ret = RET_ERR;
}
}
free(res);
return ret;
}
int check_delete(void *storage_ctx, const char *cid)
{
Resp *r = alloc_resp();
if (storage_delete(storage_ctx, cid, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
return is_resp_ok(r, NULL);
}
int check_toggle_private_queries(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
// First toggle is false -> true
if (storage_toggle_private_queries(storage_ctx, true, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (ret == RET_OK)
{
fprintf(stderr, "expected toggle(true) to fail when mix is not configured, got ok\n");
free(res);
return RET_ERR;
}
free(res);
// Second toggle is true -> false
r = alloc_resp();
if (storage_toggle_private_queries(storage_ctx, false, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
ret = is_resp_ok(r, &res);
if (res == NULL || strcmp(res, "false") != 0)
{
fprintf(stderr, "toggle private queries content mismatch, res:%s\n", res ? res : "(null)");
free(res);
return RET_ERR;
}
free(res);
return RET_OK;
}
int check_get_metrics(void *storage_ctx)
{
Resp *r = alloc_resp();
char *res = NULL;
if (storage_get_metrics(storage_ctx, (StorageCallback)callback, r) != RET_OK)
{
free_resp(r);
return RET_ERR;
}
int ret = is_resp_ok(r, &res);
if (ret != RET_OK)
{
free(res);
return ret;
}
// Checks that response contains a metric we are SURE must exist
if (res == NULL || strstr(res, "libp2p_successful_dials_total") == NULL)
{
fprintf(stderr, "get_metrics missing expected metric\n");
free(res);
return RET_ERR;
}
free(res);
return RET_OK;
}
// TODO: implement check_fetch
// It is a bit complicated because it requires two nodes
// connected together to fetch from peers.
// A good idea would be to use connect function using addresses.
// This test will be quite important when the block engine is re-implemented.
int check_fetch(void *storage_ctx, const char *cid)
{
return RET_OK;
}
int main(void)
{
void *storage_ctx = NULL;
char *res = NULL;
char *cid = NULL;
BEGIN_SUITE
RUN_TEST(setup(&storage_ctx));
RUN_TEST(check_version(storage_ctx));
RUN_TEST(start(storage_ctx));
RUN_TEST(check_repo(storage_ctx));
RUN_TEST(check_debug(storage_ctx));
RUN_TEST(check_spr(storage_ctx));
RUN_TEST(check_peer_id(storage_ctx));
RUN_TEST(check_upload_chunk(storage_ctx, "hello_world.txt"));
RUN_TEST(upload_cancel(storage_ctx));
char *path = realpath("hello_world.txt", NULL);
if (!path)
{
fprintf(stderr, "realpath failed\n");
return RET_ERR;
}
RUN_TEST(check_upload_file(storage_ctx, path, &cid));
free(path);
RUN_TEST(check_download_stream(storage_ctx, cid, "downloaded_hello.txt"));
RUN_TEST(check_download_chunk(storage_ctx, cid));
RUN_TEST(check_download_cancel(storage_ctx, cid));
RUN_TEST(check_download_manifest(storage_ctx, cid));
RUN_TEST(check_list(storage_ctx));
RUN_TEST(check_space(storage_ctx));
RUN_TEST(check_exists(storage_ctx, cid, true));
RUN_TEST(check_delete(storage_ctx, cid));
RUN_TEST(check_exists(storage_ctx, cid, false));
free(cid);
RUN_TEST(check_toggle_private_queries(storage_ctx));
RUN_TEST(update_log_level(storage_ctx, "TRACE"));
RUN_TEST(check_get_metrics(storage_ctx));
RUN_TEST(cleanup(storage_ctx));
END_SUITE
}