453 lines
18 KiB
Plaintext
453 lines
18 KiB
Plaintext
struct CliHttpResponse
|
|
{
|
|
u64 status = 0;
|
|
String raw;
|
|
String headers;
|
|
String body;
|
|
};
|
|
|
|
u64 cli_tests_passed = 0;
|
|
u64 cli_tests_failed = 0;
|
|
u64 cli_tests_skipped = 0;
|
|
StringList cli_tests_failures;
|
|
|
|
bool cli_contains(String haystack, String needle)
|
|
{
|
|
return(haystack.find(needle) != String::npos);
|
|
}
|
|
|
|
String cli_truncate(String value, u64 max_len = 240)
|
|
{
|
|
if(value.length() <= max_len)
|
|
return(value);
|
|
return(value.substr(0, max_len) + "...");
|
|
}
|
|
|
|
CliHttpResponse cli_http_request(String host, u16 port, String path, String extra_headers = "")
|
|
{
|
|
CliHttpResponse res;
|
|
u64 fd = socket_connect(host, port);
|
|
if(fd == 0)
|
|
{
|
|
res.raw = "socket_connect failed";
|
|
return(res);
|
|
}
|
|
String host_header = host;
|
|
if(host == "127.0.0.1" && port == 80)
|
|
host_header = "uce.openfu.com";
|
|
else if(host == "127.0.0.1" && port == 8080)
|
|
host_header = "localhost";
|
|
String request = "GET " + path + " HTTP/1.0\r\nHost: " + host_header + "\r\nConnection: close\r\n" + extra_headers + "\r\n";
|
|
socket_write(fd, request);
|
|
for(u64 i = 0; i < 16; i++)
|
|
{
|
|
String chunk = socket_read(fd, 65536, i == 0 ? 30 : 5);
|
|
if(chunk == "")
|
|
break;
|
|
res.raw += chunk;
|
|
}
|
|
socket_close(fd);
|
|
|
|
u64 line_end = res.raw.find("\r\n");
|
|
if(line_end != String::npos)
|
|
{
|
|
StringList parts = split(res.raw.substr(0, line_end), " ");
|
|
if(parts.size() >= 2)
|
|
res.status = int_val(parts[1]);
|
|
}
|
|
u64 split_at = res.raw.find("\r\n\r\n");
|
|
if(split_at != String::npos)
|
|
{
|
|
res.headers = res.raw.substr(0, split_at + 4);
|
|
res.body = res.raw.substr(split_at + 4);
|
|
}
|
|
else
|
|
{
|
|
res.headers = res.raw;
|
|
res.body = "";
|
|
}
|
|
return(res);
|
|
}
|
|
|
|
CliHttpResponse cli_frontend(String path, String extra_headers = "")
|
|
{
|
|
return(cli_http_request("127.0.0.1", 80, path, extra_headers));
|
|
}
|
|
|
|
CliHttpResponse cli_direct_http(String path, String extra_headers = "")
|
|
{
|
|
return(cli_http_request("127.0.0.1", 8080, path, extra_headers));
|
|
}
|
|
|
|
void cli_test_case(String name, bool ok, String summary)
|
|
{
|
|
if(ok)
|
|
{
|
|
cli_tests_passed++;
|
|
print("[PASS] ", name, " - ", summary, "\n");
|
|
}
|
|
else
|
|
{
|
|
cli_tests_failed++;
|
|
cli_tests_failures.push_back(name + ": " + summary);
|
|
print("[FAIL] ", name, " - ", summary, "\n");
|
|
}
|
|
}
|
|
|
|
void cli_test_skip(String name, String summary)
|
|
{
|
|
cli_tests_skipped++;
|
|
print("[SKIP] ", name, " - ", summary, "\n");
|
|
}
|
|
|
|
bool cli_expect_http_once(CliHttpResponse& res, String path, u64 expected_status, String marker, StringList error_markers, String& summary)
|
|
{
|
|
bool ok = res.status == expected_status;
|
|
summary = "HTTP " + std::to_string(res.status) + " for " + path;
|
|
if(marker != "" && !cli_contains(res.body, marker))
|
|
{
|
|
ok = false;
|
|
summary += "; missing marker " + marker;
|
|
}
|
|
String lower = to_lower(res.body);
|
|
for(String marker_text : error_markers)
|
|
{
|
|
if(cli_contains(lower, marker_text))
|
|
{
|
|
ok = false;
|
|
summary += "; body contained " + marker_text;
|
|
}
|
|
}
|
|
return(ok);
|
|
}
|
|
|
|
bool cli_expect_http(String name, String path, u64 expected_status, String marker, StringList error_markers = {})
|
|
{
|
|
CliHttpResponse res = cli_frontend(path);
|
|
String summary;
|
|
bool ok = cli_expect_http_once(res, path, expected_status, marker, error_markers, summary);
|
|
if(!ok && expected_status != 500)
|
|
{
|
|
usleep(500000);
|
|
res = cli_frontend(path);
|
|
ok = cli_expect_http_once(res, path, expected_status, marker, error_markers, summary);
|
|
}
|
|
cli_test_case(name, ok, ok ? summary : summary + "; body=" + cli_truncate(res.body));
|
|
return(ok);
|
|
}
|
|
|
|
StringList cli_demo_error_markers()
|
|
{
|
|
StringList markers;
|
|
markers.push_back(String("uce compile ") + "error");
|
|
markers.push_back(String("fatal signal during ") + "request");
|
|
markers.push_back(String("uncaught exception during ") + "request");
|
|
return(markers);
|
|
}
|
|
|
|
void cli_run_demo_smoke()
|
|
{
|
|
StringList files = split("call_file.uce\ncall_file_funcs.uce\ncollections.uce\ncomponents.uce\ncookie.uce\ndvalue.uce\nempty.uce\nerror-reporting.uce\nfile_append.uce\nfileio.uce\nheader.uce\nhello.uce\nindex.uce\njson.uce\nmarkdown.uce\nmemcached.uce\nmysql.uce\nonce-init.uce\nparse_time.uce\npost-multipart.uce\npost.uce\npreprocessor-comments.uce\nrandom.uce\nregex.uce\nscript.uce\nsession.uce\nsharedunit.uce\nshell.uce\nsqlite.uce\nstr_replace.uce\nstring.uce\ntask-status.uce\ntask.uce\ntask_repeat.uce\nunit-browser.uce\nunit-info.uce\nuri.uce\nutf8.uce\nwebsockets.ws.uce\nworking-dir.uce\nxml.uce\nyaml.uce\nzip.uce", "\n");
|
|
for(String file : files)
|
|
cli_expect_http("uce_demo_smoke:demo page " + file, "/demo/" + file, 200, "", file == "unit-browser.uce" ? StringList() : cli_demo_error_markers());
|
|
}
|
|
|
|
bool cli_doc_source_has_example(String source)
|
|
{
|
|
for(String line : split(source, "\n"))
|
|
if(trim(line) == ":example")
|
|
return(true);
|
|
return(false);
|
|
}
|
|
|
|
// A real example must exercise the documented API and print a derived result.
|
|
// These placeholder shapes (auto-generated stubs) are forbidden by the gate.
|
|
bool cli_doc_example_is_placeholder(String source)
|
|
{
|
|
bool in_example = false;
|
|
StringList body;
|
|
for(String line : split(source, "\n"))
|
|
{
|
|
String t = trim(line);
|
|
if(!in_example)
|
|
{
|
|
if(t == ":example")
|
|
in_example = true;
|
|
continue;
|
|
}
|
|
if(t.length() > 0 && t.substr(0, 1) == ":")
|
|
break;
|
|
body.push_back(line);
|
|
}
|
|
String joined = join(body, "\n");
|
|
if(cli_contains(joined, " example\\n\");"))
|
|
return(true);
|
|
if(cli_contains(joined, "is resource-bound; configure"))
|
|
return(true);
|
|
if(cli_contains(joined, "documents a UCE concept"))
|
|
return(true);
|
|
return(false);
|
|
}
|
|
|
|
void cli_run_doc_pages_gate()
|
|
{
|
|
StringList error_markers;
|
|
error_markers.push_back("doc example error");
|
|
error_markers.push_back(String("uce compile ") + "error");
|
|
error_markers.push_back(String("wasm runtime error during ") + "request");
|
|
error_markers.push_back(String("fatal signal during ") + "request");
|
|
error_markers.push_back(String("uncaught exception during ") + "request");
|
|
error_markers.push_back("timed out acquiring compile lock");
|
|
|
|
for(String file_name : ls("../doc/pages/"))
|
|
{
|
|
String source = file_get_contents("../doc/pages/" + file_name);
|
|
String page = nibble(file_name, ".");
|
|
if(page == "")
|
|
continue;
|
|
bool has_example = cli_doc_source_has_example(source);
|
|
String path = "/doc/index.uce?p=" + uri_encode(page);
|
|
CliHttpResponse res = cli_frontend(path);
|
|
String summary;
|
|
bool ok = cli_expect_http_once(res, path, 200, "doc-detail", error_markers, summary);
|
|
String lower = to_lower(res.body);
|
|
if(has_example)
|
|
{
|
|
if(!cli_contains(res.body, "class=\"example-output\"") || !cli_contains(res.body, "example-output-label\">Output"))
|
|
{
|
|
ok = false;
|
|
summary += "; missing example Output block";
|
|
}
|
|
if(cli_contains(lower, "doc example error"))
|
|
{
|
|
ok = false;
|
|
summary += "; example reported an error";
|
|
}
|
|
if(cli_doc_example_is_placeholder(source))
|
|
{
|
|
ok = false;
|
|
summary += "; placeholder example (must exercise the API)";
|
|
}
|
|
}
|
|
cli_test_case("uce_doc_gate:doc page " + page, ok, ok ? summary : summary + "; body=" + cli_truncate(res.body));
|
|
}
|
|
}
|
|
|
|
void cli_run_http_smoke()
|
|
{
|
|
cli_expect_http("uce_http_smoke:doc index", "/doc/index.uce", 200, "<html>");
|
|
// Regression guard: the index enumerates pages via ls("pages/"). When that
|
|
// returned empty (wasm ls() was a stub), the page still rendered <html> but
|
|
// listed nothing — so assert the function grid is actually populated.
|
|
cli_expect_http("uce_http_smoke:doc index lists functions", "/doc/index.uce", 200, "func-item");
|
|
cli_expect_http("uce_http_smoke:doc singlepage", "/doc/singlepage.uce", 200, "<html>");
|
|
cli_expect_http("uce_http_smoke:doc component page", "/doc/index.uce?p=component", 200, "component()");
|
|
cli_expect_http("uce_http_smoke:doc regex page", "/doc/index.uce?p=regex_search", 200, "regex_search");
|
|
cli_expect_http("uce_http_smoke:doc xml page", "/doc/index.uce?p=xml_encode", 200, "xml_encode");
|
|
cli_expect_http("uce_http_smoke:doc yaml page", "/doc/index.uce?p=yaml_encode", 200, "yaml_encode");
|
|
cli_expect_http("uce_http_smoke:doc relative time page", "/doc/index.uce?p=time_format_relative", 200, "time_format_relative");
|
|
cli_expect_http("uce_http_smoke:starter home", "/examples/uce-starter/", 200, "Stunning Apps");
|
|
cli_expect_http("uce_http_smoke:starter dashboard", "/examples/uce-starter/?dashboard", 200, "Dashboard");
|
|
cli_expect_http("uce_http_smoke:starter dashboard ONCE assets reach head", "/examples/uce-starter/?dashboard", 200, "views/dashboard.css");
|
|
cli_expect_http("uce_http_smoke:starter workspace nested route", "/examples/uce-starter/?workspace/projects", 200, "Workspace");
|
|
cli_expect_http("uce_http_smoke:starter workspace ONCE assets reach head", "/examples/uce-starter/?workspace/projects", 200, "css/workspace.css");
|
|
cli_expect_http("uce_http_smoke:starter ajax section", "/examples/uce-starter/?page2-section1", 200, "UCE starter AJAX fragment response");
|
|
cli_expect_http("uce_http_smoke:starter route traversal blocked", "/examples/uce-starter/?../../../demo/index", 404, "The requested page does not exist.");
|
|
// Configurable error page (page_runtime_error): a runtime fault must render the
|
|
// developer-defined error-page unit with the real error info marshalled across
|
|
// the membrane, at the error status. Asserts HTTP 500 AND that error["request_uri"]
|
|
// reached the page (the "mode=trap" query is only shown if error_info crossed).
|
|
cli_expect_http("uce_http_smoke:configurable error page 500 + error info", "/demo/error-reporting.uce?mode=trap", 500, "mode=trap");
|
|
}
|
|
|
|
void cli_run_site_suite()
|
|
{
|
|
DValue manifest;
|
|
for(String line : split(file_get_contents("manifest.txt"), "\n"))
|
|
{
|
|
line = trim(line);
|
|
if(line == "" || line[0] == '#')
|
|
continue;
|
|
StringList parts = split(line, "|");
|
|
if(parts.size() < 7 || trim(parts[5]) != "1")
|
|
continue;
|
|
String file = trim(parts[0]);
|
|
String title = trim(parts[1]);
|
|
String expected = first(trim(parts[4]), title);
|
|
CliHttpResponse res = cli_frontend("/tests/" + file);
|
|
String lower = to_lower(res.body);
|
|
bool ok = res.status == 200 && cli_contains(res.body, expected);
|
|
for(String marker : { "compile error", "runtime error", "timed out acquiring compile lock", "near line" })
|
|
if(cli_contains(lower, marker))
|
|
ok = false;
|
|
if(cli_contains(lower, "<div class=\"tests-summary\">") && !cli_contains(lower, ">failed 0</span>"))
|
|
ok = false;
|
|
if(!ok && file == "services.uce")
|
|
{
|
|
usleep(1000000);
|
|
res = cli_frontend("/tests/" + file);
|
|
lower = to_lower(res.body);
|
|
ok = res.status == 200 && cli_contains(res.body, expected);
|
|
for(String marker : { "compile error", "runtime error", "timed out acquiring compile lock", "near line" })
|
|
if(cli_contains(lower, marker))
|
|
ok = false;
|
|
if(cli_contains(lower, "<div class=\"tests-summary\">") && !cli_contains(lower, ">failed 0</span>"))
|
|
ok = false;
|
|
}
|
|
String name = "uce_site_suite:site tests " + (file == "index.uce" ? "index" : file == "io.uce" ? "filesystem" : file == "websockets.ws.uce" ? "websockets page" : replace(replace(replace(file, ".uce", ""), ".ws", ""), "_", " "));
|
|
cli_test_case(name, ok, (ok ? "HTTP 200 with suite page marker and no failed cases for /tests/" : "site suite failed for /tests/") + file + (ok ? "" : "; body=" + cli_truncate(res.body)));
|
|
}
|
|
}
|
|
|
|
void cli_run_security_smoke()
|
|
{
|
|
CliHttpResponse dotdot = cli_direct_http("/../site/demo/hello.uce");
|
|
cli_test_case("uce_security_smoke:direct HTTP rejects dot-dot script traversal", dotdot.status != 200 && !cli_contains(dotdot.body, "hello world"), "direct HTTP dot-dot traversal returned HTTP " + std::to_string(dotdot.status));
|
|
|
|
CliHttpResponse spoof = cli_direct_http("/no-such-script.uce", "Script-Filename: /Code/uce.openfu.com/uce/site/demo/hello.uce\r\n");
|
|
cli_test_case("uce_security_smoke:direct HTTP ignores Script-Filename header", spoof.status != 200 && !cli_contains(spoof.body, "hello world"), "direct HTTP Script-Filename override returned HTTP " + std::to_string(spoof.status));
|
|
|
|
CliHttpResponse sanitizer = cli_frontend("/tests/security_headers.uce");
|
|
String headers_lower = to_lower(sanitizer.headers);
|
|
bool injected = false;
|
|
for(String header : { "x-uce-injected", "x-uce-injected-name", "x-uce-cookie-injected", "x-uce-redirect-injected", "x-uce-status-injected" })
|
|
if(cli_contains(headers_lower, "\r\n" + to_lower(header) + ":"))
|
|
injected = true;
|
|
CliHttpResponse direct_sanitizer = cli_direct_http("/site/tests/security_headers.uce");
|
|
bool direct_ok = direct_sanitizer.status < 500 || cli_contains(direct_sanitizer.body, "security header sanitizer test");
|
|
cli_test_case("uce_security_smoke:response headers sanitize CRLF", !injected && direct_ok, "CRLF response header injection sanitized");
|
|
|
|
String attacker = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
CliHttpResponse session = cli_frontend("/tests/http.uce", "Cookie: uce-site-tests=" + attacker + "\r\n");
|
|
String session_headers = session.headers;
|
|
bool adopted = cli_contains(session_headers, "Set-Cookie: uce-site-tests=" + attacker);
|
|
bool hardened = cli_contains(session_headers, "Set-Cookie: uce-site-tests=") && cli_contains(session_headers, "HttpOnly") && cli_contains(session_headers, "SameSite=Lax");
|
|
cli_test_case("uce_security_smoke:unknown session ids are not adopted", !adopted && hardened, "unknown caller-supplied session id was replaced");
|
|
}
|
|
|
|
void cli_run_starter_parity()
|
|
{
|
|
StringList markers = {
|
|
"landing\t\tHome | UCE Starter",
|
|
"dashboard\t?dashboard\tDashboard | UCE Starter",
|
|
"gauges\t?gauges\tGauges | UCE Starter",
|
|
"features\t?features\tFeatures | UCE Starter",
|
|
"components\t?page1\tComponents | UCE Starter",
|
|
"workspace\t?workspace\tWorkspace | UCE Starter"
|
|
};
|
|
for(String row : markers)
|
|
{
|
|
StringList parts = split(row, "\t");
|
|
String name = parts[0];
|
|
String query = parts[1];
|
|
String title = parts[2];
|
|
cli_expect_http("uce_starter_parity:starter view " + name, "/examples/uce-starter/index.uce" + query, 200, "<title>" + title + "</title>", cli_demo_error_markers());
|
|
}
|
|
cli_expect_http("uce_starter_parity:starter unknown route 404", "/examples/uce-starter/index.uce?does-not-exist", 404, "<title>404 Not Found | UCE Starter</title>", cli_demo_error_markers());
|
|
}
|
|
|
|
void cli_run_task_lifetime_smoke()
|
|
{
|
|
String late_file = "/tmp/uce-site-tests-late-task.txt";
|
|
file_unlink(late_file);
|
|
CliHttpResponse res = cli_frontend("/tests/tasks.uce?mode=late");
|
|
usleep(3000000);
|
|
String observed = file_exists(late_file) ? file_get_contents(late_file) : "";
|
|
cli_test_case("uce_task_lifetime:delayed callback after request return", res.status == 200 && cli_contains(res.body, "late task scheduled") && cli_contains(observed, "late callback ran"), "status=" + std::to_string(res.status) + " observed=" + cli_truncate(observed));
|
|
}
|
|
|
|
void cli_run_tcp_smoke()
|
|
{
|
|
u64 fd80 = socket_connect("127.0.0.1", 80);
|
|
cli_test_case("uce_tcp_smoke:frontend port 80", fd80 != 0, "TCP connect " + String(fd80 != 0 ? "succeeded" : "failed") + " on port 80");
|
|
if(fd80 != 0)
|
|
socket_close(fd80);
|
|
u64 fd8080 = socket_connect("127.0.0.1", 8080);
|
|
cli_test_case("uce_tcp_smoke:http websocket port 8080", fd8080 != 0, "TCP connect " + String(fd8080 != 0 ? "succeeded" : "failed") + " on port 8080");
|
|
if(fd8080 != 0)
|
|
socket_close(fd8080);
|
|
}
|
|
|
|
void cli_run_wasm_kill(bool include_kill)
|
|
{
|
|
if(!include_kill)
|
|
return;
|
|
for(String row : { "wasm kill trap|/tests/wasm-kill/oob.uce|unreachable", "wasm kill loop|/tests/wasm-kill/loop.uce|interrupt", "wasm kill recurse|/tests/wasm-kill/recurse.uce|wasm_kill_recurse" })
|
|
{
|
|
StringList parts = split(row, "|");
|
|
CliHttpResponse res = cli_frontend(parts[1]);
|
|
CliHttpResponse health = cli_frontend("/demo/hello.uce");
|
|
bool ok = res.status == 500 && cli_contains(res.body, "wasm runtime error during request") && cli_contains(res.body, parts[2]) && health.status == 200 && cli_contains(health.body, "hello world");
|
|
cli_test_case("uce_wasm_kill:" + parts[0], ok, ok ? "clean wasm trap page and post-trap health check for " + parts[1] : "kill check failed; status=" + std::to_string(res.status) + " health=" + std::to_string(health.status));
|
|
}
|
|
}
|
|
|
|
void cli_print_list(bool include_kill)
|
|
{
|
|
print("UCE CLI test groups:\n");
|
|
print(" demo, http, site, security, starter, tcp");
|
|
if(include_kill)
|
|
print(", wasm-kill");
|
|
print("\n");
|
|
}
|
|
|
|
CLI(Request& context)
|
|
{
|
|
context.header["Content-Type"] = "text/plain; charset=utf-8";
|
|
DValue input = cli_input(context);
|
|
String action = first(input["action"].to_string(), "run");
|
|
bool include_kill = input["include_kill"].to_bool() || input["include_wasm_kill"].to_bool();
|
|
if(action == "list")
|
|
{
|
|
cli_print_list(include_kill);
|
|
return;
|
|
}
|
|
|
|
print("Running UCE CLI tests\n");
|
|
f64 group_start = time_precise();
|
|
cli_run_demo_smoke();
|
|
print("[GROUP] demo ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_http_smoke();
|
|
print("[GROUP] http ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_site_suite();
|
|
print("[GROUP] site ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_doc_pages_gate();
|
|
print("[GROUP] doc-gate ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_security_smoke();
|
|
print("[GROUP] security ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_task_lifetime_smoke();
|
|
print("[GROUP] task-lifetime ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_starter_parity();
|
|
print("[GROUP] starter ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_tcp_smoke();
|
|
String repo_root = process_start_directory();
|
|
String cli_arg_out = shell_exec("cd " + shell_escape(repo_root) + " && bash scripts/uce-cli /tests/cli.uce action=arg message=hello-cli 2>&1");
|
|
cli_test_case("uce_cli_fixture:cli_arg", contains(cli_arg_out, "hello-cli") && contains(cli_arg_out, "fallback"), cli_truncate(cli_arg_out));
|
|
print("[GROUP] tcp ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
group_start = time_precise();
|
|
cli_run_wasm_kill(include_kill);
|
|
print("[GROUP] wasm-kill ", std::to_string((u64)((time_precise() - group_start) * 1000)), " ms\n");
|
|
print("\nSummary: ", std::to_string(cli_tests_passed), " passed, ", std::to_string(cli_tests_failed), " failed, ", std::to_string(cli_tests_skipped), " skipped\n");
|
|
if(cli_tests_failed != 0)
|
|
{
|
|
context.set_status(500, "CLI Tests Failed");
|
|
print("Failures:\n");
|
|
for(String failure : cli_tests_failures)
|
|
print("- ", failure, "\n");
|
|
}
|
|
}
|
|
|
|
RENDER(Request& context)
|
|
{
|
|
context.set_status(404, "Not Found");
|
|
context.header["Content-Type"] = "text/plain; charset=utf-8";
|
|
print("Invoke this test runner through the UCE CLI socket.\n");
|
|
}
|