diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2025-07-18 09:32:16 +0200 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2025-07-18 09:32:33 +0200 |
| commit | 69528489cddd27b4d4b9411d00b60cccc175b0e8 (patch) | |
| tree | eeed9d13f99858b3fd5660bc8d57e86bf81088a3 /test | |
| parent | a46e9a5cbb8fc233a0ccb47708e7686c2b561ac0 (diff) | |
Add a simple API for lookups
Diffstat (limited to 'test')
| -rw-r--r-- | test/api.c | 246 | ||||
| -rw-r--r-- | test/proto.c | 71 |
2 files changed, 300 insertions, 17 deletions
diff --git a/test/api.c b/test/api.c new file mode 100644 index 0000000..c88ed46 --- /dev/null +++ b/test/api.c @@ -0,0 +1,246 @@ +#include "log.h" +#include "unity.h" + +#include "api.h" +#include "base64.h" + +#include <assert.h> +#include <curl/curl.h> +#include <stdlib.h> +#include <string.h> + +// @PASTE Stolen from libcurl documentation +struct memory { + char *body; + size_t size; +}; + +static size_t write_to_memory(char *data, size_t size, size_t nmemb, void *clientp) { + size_t realsize = size * nmemb; + struct memory *mem = (struct memory *)clientp; + + char *ptr = realloc(mem->body, mem->size + realsize + 1); + if(!ptr) return 0; /* out of memory */ + + mem->body = ptr; + memcpy(&(mem->body[mem->size]), data, realsize); + mem->size += realsize; + mem->body[mem->size] = 0; + + return realsize; +} + +static size_t read_from_memory(char *data, size_t size, size_t nmemb, void *clientp) { + size_t realsize = size * nmemb; + struct memory *mem = (struct memory *)clientp; + + realsize = realsize > mem->size ? mem->size : realsize; + + memcpy(data, mem->body, realsize); + mem->size -= realsize; + mem->body += realsize; + + return realsize; +} + +void test_base64_decode() { + size_t res = 0; + uint8_t buf[256]; + + res = base64_decode((unsigned char*)"AAAA", 4, buf, 256); + TEST_ASSERT_EQUAL(3, res); + TEST_ASSERT_EQUAL_MEMORY("\0\0\0", buf, 3); + + res = base64_decode((unsigned char*)"AA==", 4, buf, 256); + TEST_ASSERT_EQUAL(1, res); + TEST_ASSERT_EQUAL_MEMORY("\0", buf, 1); + + res = base64_decode((unsigned char*)"MQ==", 4, buf, 256); + TEST_ASSERT_EQUAL(1, res); + TEST_ASSERT_EQUAL_MEMORY("1", buf, 1); + + res = base64_decode((unsigned char*)"AAA=", 4, buf, 256); + TEST_ASSERT_EQUAL(2, res); + TEST_ASSERT_EQUAL_MEMORY("\0\0", buf, 2); + + // Too short of an input string + res = base64_decode((unsigned char*)"AA=", 3, buf, 256); + TEST_ASSERT_EQUAL(-1, res); + + // Too little space in the output buffer + res = base64_decode((unsigned char*)"AAA=", 4, buf, 1); + TEST_ASSERT_EQUAL(-1, res); + + // Too little space in the output buffer + res = base64_decode((unsigned char*)"dGVzdA==", 8, buf, 256); + TEST_ASSERT_EQUAL(4, res); + TEST_ASSERT_EQUAL_MEMORY("test", buf, 4); + + // A null byte in the middle of the input + res = base64_decode((unsigned char*)"AA\0=", 4, buf, 256); + TEST_ASSERT_EQUAL(-1, res); + + // It should stop at the first equals + res = base64_decode_incr((unsigned char*)"AA==W", 5, buf, 256); + TEST_ASSERT_EQUAL(1, res); + TEST_ASSERT_EQUAL_MEMORY("\0", buf, 1); + + res = base64_decode_incr((unsigned char*)"AAAA\"", 5, buf, 256); + TEST_ASSERT_EQUAL(3, res); + TEST_ASSERT_EQUAL_MEMORY("\0\0\0", buf, 3); + + res = base64_decode_incr((unsigned char*)"AAAA}", 5, buf, 256); + TEST_ASSERT_EQUAL(3, res); + TEST_ASSERT_EQUAL_MEMORY("\0\0\0", buf, 3); + + // We are a little overpermissive when it comes to the padding equals. This + // isn't technically valid, but due to some implementation details we still + // accept it. I think that's fine + res = base64_decode_incr((unsigned char*)"AA=W", 4, buf, 256); + TEST_ASSERT_EQUAL(1, res); + TEST_ASSERT_EQUAL_MEMORY("\0", buf, 1); +} + +void test_root_get() { + CURLcode curlRes; + CURL *curl = curl_easy_init(); + TEST_ASSERT_NOT_NULL(curl); + + struct dht dht = {0}; + dht.self = (struct nodeid){.inner={0x42424242, 0x42424242, 0x42424242, 0x42424242, 0x42424242}}; + api_init(&dht); + + curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:6982/"); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + struct memory body = {0}; + curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_perform(curl); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + char *ct; + curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + TEST_ASSERT_EQUAL_STRING("application/json", ct); + + TEST_ASSERT_EQUAL_STRING("{ \"id\": \"QkJCQkJCQkJCQkJCQkJCQkJCQkI=\" }", body.body); + body = (struct memory){0}; + + free(body.body); + curl_easy_cleanup(curl); + api_end(); +} + +void test_lookup_get() { + curl_global_init(CURL_GLOBAL_ALL); + CURLcode curlRes; + CURL *curl = curl_easy_init(); + TEST_ASSERT_NOT_NULL(curl); + + struct dht dht = {0}; + dht.self = (struct nodeid){.inner={0x42424242, 0x42424242, 0x42424242, 0x42424242, 0x42424242}}; + api_init(&dht); + + curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:6982/lookup"); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + struct memory body = {0}; + curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_perform(curl); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + char *ct; + curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + TEST_ASSERT_EQUAL_STRING("application/json", ct); + + TEST_ASSERT_EQUAL_STRING( + "{ " + "\"state\": \"empty\" " + "}", + body.body + ); + body = (struct memory){0}; + + { + curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + struct memory req_body = { + .body = "{\"target\": \"BAAAAAAAAAAAAAAAAAAAAAAAAAA=\", \"state\": \"pending\"}", + .size = strlen(req_body.body), + }; + curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_perform(curl); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + TEST_ASSERT_EQUAL_STRING("application/json", ct); + + TEST_ASSERT_EQUAL_STRING( + "{ " + "\"state\": \"pending\", " + "\"outstanding\": 0, " + "\"target\": \"BAAAAAAAAAAAAAAAAAAAAAAAAAA=\" " + "}", + body.body + ); + body = (struct memory){0}; + } + + // The protocol does whatever and complete the lookup + dht.lookup.state = OP_COMPLETED; + + { + curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + struct memory req_body = { + .body = "{\"state\": \"empty\"}", + .size = strlen(req_body.body), + }; + curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_perform(curl); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + + curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + TEST_ASSERT_EQUAL(CURLE_OK, curlRes); + TEST_ASSERT_EQUAL_STRING("application/json", ct); + + TEST_ASSERT_EQUAL_STRING( + "{ " + "\"state\": \"empty\" " + "}", + body.body + ); + body = (struct memory){0}; + } + + free(body.body); + curl_easy_cleanup(curl); + curl_global_cleanup(); + api_end(); +} diff --git a/test/proto.c b/test/proto.c index 29563a3..a566971 100644 --- a/test/proto.c +++ b/test/proto.c @@ -745,19 +745,21 @@ void test_lookup_response() { // The node responds. This should add it to our frontier since it's // empty. It should also fan out the search into what it returns since // we still have empty spots after adding this one. - char buff[] = "d1:y1:r1:t1:11:rd2:id20:CBBBBBBBBBBBBBBBBBBB5:nodes52:aBBBBBBBBBBBBBBBBBBB\xFF\xFF\xFF\xFF\x00\x01""aaaaBBBBBBBBBBBBBBBB\xFF\xFF\xFF\xFF\x00\x01""ee"; + char buff[] = "d1:y1:r1:t1:11:rd2:id20:CBBBBBBBBBBBBBBBBBBB5:nodes78:aBBBBBBBBBBBBBBBBBBB\xFF\xFF\xFF\xFF\x00\x01""aaaaBBBBBBBBBBBBBBBB\xFF\xFF\xFF\xFF\x00\x01""aaaBBBBBBBBBBBBBBBBB\xFF\xFF\xFF\xFF\x00\x01""ee"; struct message* message_cursor = outbuff; - int rc = proto_run(&dht, buff, sizeof(buff), (struct sockaddr_in*)&remote, sizeof(remote), now, &message_cursor, outbuff+2); + int rc = proto_run(&dht, buff, sizeof(buff), (struct sockaddr_in*)&remote, sizeof(remote), now, &message_cursor, outbuff+10); TEST_ASSERT_EQUAL(rc, 0); TEST_ASSERT_EQUAL_CHAR_ARRAY("CBBBBBBBBBBBBBBBBBBB", &dht.lookup.closest[0], 20); - TEST_ASSERT_EQUAL_PTR(message_cursor, outbuff+2); + TEST_ASSERT_EQUAL_PTR(message_cursor, outbuff+3); TEST_ASSERT_EQUAL(91, outbuff[0].payload_len); TEST_ASSERT_EQUAL_CHAR_ARRAY("d1:ad2:id20:BBBBBBBBBBBBBBBBBBBB6:target20:aaaaaaaaaaaaaaaaaaaae1:q9:find_node1:t1:21:y1:qe", outbuff[0].payload, 91); TEST_ASSERT_EQUAL(91, outbuff[1].payload_len); TEST_ASSERT_EQUAL_CHAR_ARRAY("d1:ad2:id20:BBBBBBBBBBBBBBBBBBBB6:target20:aaaaaaaaaaaaaaaaaaaae1:q9:find_node1:t1:31:y1:qe", outbuff[1].payload, 91); - TEST_ASSERT_EQUAL(2, dht.lookup.outstanding); // Resolve 1, add 2 + TEST_ASSERT_EQUAL(91, outbuff[2].payload_len); + TEST_ASSERT_EQUAL_CHAR_ARRAY("d1:ad2:id20:BBBBBBBBBBBBBBBBBBBB6:target20:aaaaaaaaaaaaaaaaaaaae1:q9:find_node1:t1:41:y1:qe", outbuff[2].payload, 91); + TEST_ASSERT_EQUAL(3, dht.lookup.outstanding); // Resolve 1, add 3 memcpy(&remote, &outbuff[0].dest, sizeof(remote)); } @@ -770,20 +772,51 @@ void test_lookup_response() { // we just asserted above. Those nodes happened to be closer to the final // target than the outstanding request we have going on. { - for(size_t i = 0; i < 8; i++) { - dht.lookup.closest[i] = (struct nodeid){.inner={0x42424242, 0x42424242, 0x42424242, 0x42424242, 0x42424242}}; - // The first two bytes match - dht.lookup.closest[i].inner_b[0] = 'a'; - dht.lookup.closest[i].inner_b[1] = 'a'; - dht.lookup.closest[i].inner_b[2] = 'a' + i; - - dht.lookup.closest_addr[i].ip = 0; - dht.lookup.closest_addr[i].port = 1; - } + dht.lookup.closest[0] = (struct nodeid){.inner_b={"aaaBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[0] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[1] = (struct nodeid){.inner_b={"aabBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[1] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[2] = (struct nodeid){.inner_b={"aacBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[2] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[3] = (struct nodeid){.inner_b={"aadBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[3] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[4] = (struct nodeid){.inner_b={"aaeBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[4] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[5] = (struct nodeid){.inner_b={"aafBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[5] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[6] = (struct nodeid){.inner_b={"aagBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[6] = (struct addr){.ip = 0, .port = 1}; + dht.lookup.closest[7] = (struct nodeid){.inner_b={"aahBBBBBBBBBBBBBBBBB"}}; + dht.lookup.closest_addr[7] = (struct addr){.ip = 0, .port = 1}; } now += 1; { + // The whole omitted interactive above included a response to the 3rd + // message from the last send, but before that, we fanned out from + // another node that knew about it too. This means we still have + // a leftover pending request to that node waiting for us. When that + // comes in, we should notice that, although it's better than some of + // our other candidates, it's also already there. It should therefore + // not be included + char buff[] = "d1:y1:r1:t1:41:rd2:id20:aaaBBBBBBBBBBBBBBBBB5:nodes0:ee"; + struct message* message_cursor = outbuff; + int rc = proto_run(&dht, buff, sizeof(buff), (struct sockaddr_in*)&remote, sizeof(remote), now, &message_cursor, outbuff+2); + + TEST_ASSERT_EQUAL(rc, 0); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[0], "aaaBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[1], "aabBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[2], "aacBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[3], "aadBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[4], "aaeBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[5], "aafBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[6], "aagBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[7], "aahBBBBBBBBBBBBBBBBB", 20); + + TEST_ASSERT_EQUAL(2, dht.lookup.outstanding); // Resolve 1 + } + + { // The node now finally responds, but woops only the first byte of its // ID matches. That's worse than the frontier and shouldn't cause any // addtional adjustment to the frontier. @@ -794,10 +827,14 @@ void test_lookup_response() { int rc = proto_run(&dht, buff, sizeof(buff), (struct sockaddr_in*)&remote, sizeof(remote), now, &message_cursor, outbuff+2); TEST_ASSERT_EQUAL(rc, 0); - // We only need to check this once since we always pick the first slot - // with a given score. It's a little implementation dependant, but it - // beats having 8 asserts. TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[0], "aaaBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[1], "aabBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[2], "aacBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[3], "aadBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[4], "aaeBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[5], "aafBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[6], "aagBBBBBBBBBBBBBBBBB", 20); + TEST_ASSERT_EQUAL_CHAR_ARRAY(&dht.lookup.closest[7], "aahBBBBBBBBBBBBBBBBB", 20); // And we didn't fan out to the new node since it's already part of the current frontier TEST_ASSERT_EQUAL_PTR(message_cursor, outbuff); |
