#include "tests.h" #include "bip39.h" #include "http.h" #include "log.h" #include "mem.h" #include "ssb.tests.h" #include "util.js.h" #include #include #include #include #include #include #include #if defined(_WIN32) #define WIFEXITED(x) 1 #define WEXITSTATUS(x) (x) #endif #if defined(__OpenBSD__) #include #endif #if defined(__APPLE__) #include #endif #define TEST_ARGS " --ssb-port=0 --http-port=0 --https-port=0" #if !TARGET_OS_IPHONE static void _write_file(const char* path, const char* contents) { FILE* file = fopen("out/test.js", "w"); if (!file) { printf("Unable to write %s: %s.\n", path, strerror(errno)); fflush(stdout); abort(); } fputs(contents, file); fclose(file); } static void _test_nop(const tf_test_options_t* options) { _write_file("out/test.js", "print('hi');"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); (void)result; assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); } static void _test_child(const tf_test_options_t* options) { _write_file( "out/test.js", "var task = new Task();\n" "task.onExit = function() {\n" " print('child exited');\n" "};\n" "task.activate();\n" "File.readFile('out/child.js').then(function(data) {\n" " task.execute({name: 'child.js', source: utf8Decode(data)}).then(function() {\n" " print('child started');\n" " });\n" "});"); _write_file( "out/child.js", "print('I am the child process.');\n" "exit(0);\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); (void)result; assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/child.js"); } static void _test_promise(const tf_test_options_t* options) { _write_file("out/test.js", "var task = new Task();\n" "task.activate();\n" "File.readFile('out/child.js').then(function(data) {\n" " task.execute({name: 'child.js', source: utf8Decode(data)}).then(function() {\n" " task.getExports().then(function(exports) {\n" " return exports.add(1, 1);\n" " }).then(function(sum) {\n" " if (sum == 2) {\n" " exit(0);\n" " } else {\n" " exit(1);\n" " }\n" " });\n" " });\n" "});\n"); _write_file( "out/child.js", "exports = {\n" " add: function(left, right) {\n" " return left + right;\n" " }\n" "}\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); (void)result; assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/child.js"); } static void _test_promise_remote_throw(const tf_test_options_t* options) { _write_file( "out/test.js", "var task = new Task();\n" "task.activate();\n" "File.readFile('out/child.js').then(function(data) {\n" " task.execute({name: 'child.js', source: utf8Decode(data)}).then(function() {\n" " task.getExports().then(function(exp) {\n" " return exp.add(1, 1);\n" " }).then(function(sum) {\n" " exit(1);\n" " }).catch(function(error) {\n" " print('Caught: ' + error.message);\n" " if (error.stack) {\n" " print('stack: ' + error.stack);\n" " }\n" " exit(0);\n" " });\n" " }).catch(function(e) {\n" " print('caught', e.message);\n" " });\n" "});\n"); _write_file( "out/child.js", "exports = {\n" " add: function(left, right) {\n" " throw new Error('fail');\n" " }\n" "}\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); (void)result; assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/child.js"); } static void _test_promise_remote_reject(const tf_test_options_t* options) { _write_file( "out/test.js", "var task = new Task();\n" "task.activate();\n" "File.readFile('out/child.js').then(function(data) {\n" " task.execute({name: 'child.js', source: utf8Decode(data)}).then(function() {\n" " task.getExports().then(function(exp) {\n" " return exp.add(1, 1);\n" " }).then(function(sum) {\n" " exit(1);\n" " }).catch(function(error) {\n" " print('Caught: ' + error.message);\n" " if (error.stack) {\n" " print('stack: ' + error.stack);\n" " }\n" " exit(0);\n" " });\n" " }).catch(function(e) {\n" " print('caught', e.message);\n" " });\n" "});\n"); _write_file( "out/child.js", "exports = {\n" " add: function(left, right) {\n" " return new Promise(function(resolve, reject) {\n" " reject(new Error('oops'));\n" " });\n" " }\n" "}\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); (void)result; assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/child.js"); } static void _test_database(const tf_test_options_t* options) { _write_file( "out/test.js", "var db = new Database('testdb');\n" "if (db.get('a')) {\n" " exit(1);\n" "}\n" "db.set('a', 1);\n" "if (db.get('a') != 1) {\n" " exit(2);\n" "}\n" "db.set('b', 2);\n" "db.set('c', 3);\n" "\n" "var expected = ['a', 'b', 'c'];\n" "var have = db.getAll();\n" "for (var i = 0; i < have.length; i++) {\n" " var item = have[i];\n" " if (expected.indexOf(item) == -1) {\n" " print('Did not find ' + item + ' in db.');\n" " exit(3);\n" " } else {\n" " expected.splice(expected.indexOf(item), 1);\n" " }\n" "}\n" "if (expected.length) {\n" " print('Expected but did not find: ' + JSON.stringify(expected));\n" " exit(4);\n" "}\n"); char command[256]; unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/test_db0.sqlite"); } static void _test_this(const tf_test_options_t* options) { _write_file( "out/test.js", "var task = new Task();\n" "task.activate.bind(null).apply();\n" "exit(0);\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static void _test_await(const tf_test_options_t* options) { _write_file( "out/test.js", "print('hi');\n" "function foobar() {\n" " return new Promise(function(resolve, reject) {\n" " resolve(10);\n" " });\n" "}\n" "\n" "async function huh() {\n" " let v = await foobar();\n" " print('v => ' + v);\n" " if (v != 10) {\n" " throw new Error('nope');\n" " }\n" "}\n" "\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static void _test_import(const tf_test_options_t* options) { _write_file( "out/test.js", "import * as req from './required.js';\n" "if (req.foo() != 12345) {\n" " exit(1);\n" "}\n"); _write_file( "out/required.js", "export function foo() {\n" " return 12345;\n" "}\n"); _write_file( "out/bad.js", "import * as req from './missing.js';\n" "if (req.foo() != 12345) {\n" " exit(1);\n" "}\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/bad.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/required.js"); unlink("out/missing.js"); } static void _test_exit(const tf_test_options_t* options) { _write_file("out/test.js", "import * as blah from './blah.js';\n"); _write_file("out/blah.js", "\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/blah.js"); } static void _test_icu(const tf_test_options_t* options) { _write_file( "out/test.js", "print('Hi');\n" "print(parseInt('3').toLocaleString());\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static void _test_uint8array(const tf_test_options_t* options) { _write_file( "out/test.js", "var task = new Task();\n" "task.onExit = function() {\n" " print('child exited');\n" "};\n" "task.activate();\n" "File.readFile('out/child.js').then(function(data) {\n" " task.execute({name: 'child.js', source: utf8Decode(data)}).then(async function() {\n" " print('child started');\n" " var input = new Uint8Array(10);\n" " for (var i = 0; i < 10; i++) {\n" " input[i] = i;\n" " }\n" " var test = (await task.getExports()).test;\n" " var output = new Uint8Array(await test(input));\n" " print('input', input, input.length, input.byteLength);\n" " print('output', output, output.length, output.byteLength);\n" " for (var i = 0; i < 10; i++) {\n" " print(output[i]);\n" " if (output[i] != i) {\n" " print('output[' + i + '] == ' + output[i]);\n" " exit(1);\n" " }\n" " }\n" " exit(0);\n" " })\n" "})\n"); _write_file( "out/child.js", "exports = {\n" " test: function(data) {\n" " return data;\n" " }\n" "}\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/child.js"); } static void _test_float(const tf_test_options_t* options) { _write_file( "out/test.js", "var task = new Task();\n" "task.onExit = function() {\n" " print('child exited');\n" "};\n" "task.activate();\n" "File.readFile('out/child.js').then(function(data) {\n" " task.execute({name: 'child.js', source: utf8Decode(data)}).then(async function() {\n" " print('get exports');\n" " let test = (await task.getExports()).test;\n" " print('calling export');\n" " let result = await test(1.2);\n" " print(result);\n" " exit(result == 1.2 ? 0 : 1);\n" " });\n" "});"); _write_file( "out/child.js", "print(\"child\");\n" "exports = {\n" " test: function(value) {\n" " print(value);\n" " return value;\n" " }\n" "};\n" "print(\"child ready\");\n" ); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); (void)result; assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); unlink("out/child.js"); } static void _test_socket(const tf_test_options_t* options) { _write_file( "out/test.js", "'use strict';\n" "\n" "var s = new Socket();\n" "print('connecting');\n" "print('before connect', s.isConnected);\n" "s.onError(function(e) {\n" " print(e);\n" "});\n" "print('noDelay', s.noDelay);\n" "s.noDelay = true;\n" "s.connect('www.unprompted.com', 80).then(function() {\n" " print('connected', 'www.unprompted.com', 80, s.isConnected);\n" " print(s.peerName);\n" " s.read(function(data) {\n" " print('read', data ? data.length : null);\n" " });\n" " s.write('GET / HTTP/1.0\\r\\n\\r\\n');\n" "}).then(function(e) {\n" " print('closed 1');\n" "});\n" "\n" "var s2 = new Socket();\n" "print('connecting');\n" "print('before connect', s2.isConnected);\n" "s2.onError(function(e) {\n" " print('error');\n" " print(e);\n" "});\n" "print('noDelay', s2.noDelay);\n" "s2.noDelay = true;\n" "s2.connect('www.unprompted.com', 443).then(function() {\n" " print('connected', 'www.unprompted.com', 443);\n" " s2.read(function(data) {\n" " print('read', data ? data.length : null);\n" " });\n" " return s2.startTls();\n" "}).then(function() {\n" " print('ready');\n" " print(s2.peerName);\n" " s2.write('GET / HTTP/1.0\\r\\nConnection: close\\r\\n\\r\\n').then(function() {\n" " s2.shutdown();\n" " });\n" "}).catch(function(e) {\n" " print('caught');\n" " print(e);\n" "});\n" "var s3 = new Socket();\n" "print('connecting s3');\n" "print('before connect', s3.isConnected);\n" "s3.onError(function(e) {\n" " print('error');\n" " print(e);\n" "});\n" "print('noDelay', s3.noDelay);\n" "s3.noDelay = true;\n" "s3.connect('0.0.0.0', 443).then(function() {\n" " print('connected', '0.0.0.0', 443);\n" " s3.read(function(data) {\n" " print('read', data ? data.length : null);\n" " });\n" " return s3.startTls();\n" "}).then(function() {\n" " print('ready');\n" " print(s3.peerName);\n" " s3.write('GET / HTTP/1.0\\r\\nConnection: close\\r\\n\\r\\n').then(function() {\n" " s3.shutdown();\n" " });\n" "}).catch(function(e) {\n" " print('caught');\n" " print(e);\n" "});\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static void _test_file(const tf_test_options_t* options) { _write_file( "out/test.js", "'use strict';\n" "File.readFile('out/test.js').then(function(data) {\n" "}).catch(function(error) {\n" " print('ERROR', error);\n" " exit(1);\n" "});\n" "File.readFile('out/missing.txt').then(function(data) {\n" " print('READ', utf8Decode(data));\n" " exit(1);\n" "}).catch(function(error) {\n" " print('expected error', error);\n" "});\n"); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static void _test_sign(const tf_test_options_t* options) { _write_file( "out/test.js", "'use strict';\n" "let id = ssb.createIdentity('test');\n" "print(id);\n" "let sig = ssb.hmacsha256sign('hello', 'test', id);\n" "print(sig);\n" "if (!ssb.hmacsha256verify(id, 'hello', sig)) {\n" " exit(1);\n" "}\n" "if (ssb.hmacsha256verify(id, 'world', sig)) {\n" " exit(1);\n" "}\n" "if (ssb.hmacsha256verify(id, 'hello1', sig)) {\n" " exit(1);\n" "}\n" ); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static void _test_b64(const tf_test_options_t* options) { _write_file( "out/test.js", "'use strict';\n" "print(base64Encode('hello'));\n" "let x = utf8Decode(base64Decode(base64Encode('hello')));\n" "if (x !== 'hello') {\n" " print(x);\n" " exit(1);\n" "}\n" ); char command[256]; snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); tf_printf("%s\n", command); int result = system(command); tf_printf("returned %d\n", WEXITSTATUS(result)); assert(WIFEXITED(result)); assert(WEXITSTATUS(result) == 0); unlink("out/test.js"); } static const char* k_bip39_test_key_base64 = "GO0Lv5BvcuuJJdHrokHoo0PmCDC/XjO/SZ6H+ddq4UvWd/VPW1RJrjd1aCUIfPIojFXrWMb8R54vVerU2TwjdQ==.ed25519"; static const char* k_bip32_test_key_words = "body hair useful camp warm into cause riot two bamboo kick educate dinosaur advice seed type crisp where guilt avocado output rely lunch goddess"; static void _test_bip39(const tf_test_options_t* options) { uint8_t bytes[64] = { 0 }; tf_base64_decode(k_bip39_test_key_base64, strlen(k_bip39_test_key_base64) - strlen(".ed25519"), bytes, sizeof(bytes)); char words[4096] = ""; bool result = tf_bip39_bytes_to_words(bytes, sizeof(bytes) / 2, words, sizeof(words)); tf_printf("%d: %s\n", result, words); assert(result); assert(strcmp(words, k_bip32_test_key_words) == 0); uint8_t test_bytes[32] = { 0 }; result = tf_bip39_words_to_bytes(k_bip32_test_key_words, test_bytes, sizeof(test_bytes)); assert(result); assert(memcmp(bytes, test_bytes, sizeof(test_bytes)) == 0); } typedef struct _test_http_t { uv_loop_t* loop; uv_async_t async; bool done; } test_http_t; static void _test_http_async(uv_async_t* async) { } static void _test_http_thread(void* data) { test_http_t* test = data; int r = system("curl -v http://localhost:23456/404"); assert(WEXITSTATUS(r) == 0); tf_printf("curl returned %d\n", WEXITSTATUS(r)); r = system("curl -v http://localhost:23456/hello"); assert(WEXITSTATUS(r) == 0); tf_printf("curl returned %d\n", WEXITSTATUS(r)); r = system("curl -v --data 'hello world' http://localhost:23456/post"); assert(WEXITSTATUS(r) == 0); tf_printf("curl returned %d\n", WEXITSTATUS(r)); r = system("curl -v http://localhost:23456/hello http://localhost:23456/hello http://localhost:23456/hello"); assert(WEXITSTATUS(r) == 0); tf_printf("curl returned %d\n", WEXITSTATUS(r)); test->done = true; /* All to wake up the loop. */ uv_async_send(&test->async); } static void _test_http_handler(tf_http_request_t* request) { const char* headers[] = { "User-Agent", "TildeFriends/1.0", }; const char* k_payload = "Hello, world!\n"; tf_http_respond(request, 200, headers, 1, k_payload, strlen(k_payload)); } static void _test_http_handler_post(tf_http_request_t* request) { const void* body = NULL; size_t size = tf_http_get_body(request, &body); tf_printf("size = %zd body=%.*s\n", size, (int)size, (const char*)body); const char* headers[] = { "Connection", "close", }; const char* k_payload = "Hello, world!\n"; tf_http_respond(request, 200, headers, 1, k_payload, strlen(k_payload)); } static void _test_http(const tf_test_options_t* options) { tf_printf("Starting http.\n"); uv_loop_t loop = { 0 }; uv_loop_init(&loop); tf_http_t* http = tf_http_create(&loop); tf_http_add_handler(http, "/hello", _test_http_handler, NULL, NULL); tf_http_add_handler(http, "/post", _test_http_handler_post, NULL, NULL); tf_http_listen(http, 23456, NULL, NULL, NULL); test_http_t test = { .loop = &loop }; uv_async_init(&loop, &test.async, _test_http_async); uv_thread_t thread = { 0 }; uv_thread_create(&thread, _test_http_thread, &test); while (!test.done) { uv_run(&loop, UV_RUN_ONCE); } uv_close((uv_handle_t*)&test.async, NULL); tf_printf("Done running.\n"); tf_http_destroy(http); uv_run(&loop, UV_RUN_DEFAULT); uv_loop_close(&loop); uv_thread_join(&thread); } static void _tf_test_run(const tf_test_options_t* options, const char* name, void (*test)(const tf_test_options_t* options), bool opt_in) { bool specified = false; if (options->tests) { char* dup = tf_strdup(options->tests); char* state = NULL; const char* t = NULL; while ((t = strtok_r(t ? NULL : dup, ",", &state)) != NULL) { if (strcmp(t, name) == 0) { specified = true; break; } } tf_free(dup); } if ((!opt_in && !options->tests) || specified) { #define GREEN "\e[1;32m" #define MAGENTA "\e[1;35m" #define CYAN "\e[1;36m" #define RESET "\e[0m" tf_printf(CYAN "== running test " MAGENTA "%s" CYAN " ==\n" RESET, name); test(options); tf_printf("[" GREEN "pass" RESET "] %s\n", name); #undef GREEN #undef MAGENTA #undef CYAN #undef RESET } } #endif void tf_tests(const tf_test_options_t* options) { #if !TARGET_OS_IPHONE _tf_test_run(options, "bip39", _test_bip39, false); _tf_test_run(options, "http", _test_http, false); _tf_test_run(options, "ssb", tf_ssb_test_ssb, false); _tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false); _tf_test_run(options, "ssb_following", tf_ssb_test_following, false); _tf_test_run(options, "nop", _test_nop, false); _tf_test_run(options, "child", _test_child, false); _tf_test_run(options, "promise", _test_promise, false); _tf_test_run(options, "promise_remote_throw", _test_promise_remote_throw, false); _tf_test_run(options, "promise_remote_reject", _test_promise_remote_reject, false); _tf_test_run(options, "database", _test_database, false); _tf_test_run(options, "this", _test_this, false); _tf_test_run(options, "await", _test_await, false); _tf_test_run(options, "import", _test_import, false); _tf_test_run(options, "exit", _test_exit, false); _tf_test_run(options, "icu", _test_icu, false); _tf_test_run(options, "uint8array", _test_uint8array, false); _tf_test_run(options, "float", _test_float, false); _tf_test_run(options, "socket", _test_socket, false); _tf_test_run(options, "file", _test_file, false); _tf_test_run(options, "sign", _test_sign, false); _tf_test_run(options, "b64", _test_b64, false); _tf_test_run(options, "rooms", tf_ssb_test_rooms, false); _tf_test_run(options, "bench", tf_ssb_test_bench, false); _tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true); tf_printf("Tests completed.\n"); #endif }