#include "test_harness.h" #include "lua_sandbox.h" #include "permission_gate.h" #include #include #include "audit_log.h" #include "rate_limiter.h" #include "path_sandbox.h" #include "timer_manager.h" #include "json_api.h" #include "crypto_api.h" #include "virtual_fs.h" #include "database_manager.h" #include "http_validator.h" #include "network_manager.h" #include "websocket_manager.h" #include "camera_interface.h" #include "microphone_interface.h" #include "audio_output.h" #include #include #include #include #include // Get path to scripts directory std::string GetScriptsDir() { // Scripts are copied to build directory by CMake return "scripts"; } // Helper to create test context SandboxContext TestContext() { return SandboxContext{ .app_id = "test.app", .app_path = ".", .permissions = {}, .is_system_app = false }; } // Helper to read file contents std::string ReadFile(const std::string& path) { std::ifstream f(path); if (!f) return ""; std::stringstream ss; ss << f.rdbuf(); return ss.str(); } // Helper to setup a _test table in real _G for timer tests // This allows test scripts to store state without triggering the proxy's __newindex void SetupTestTable(lua_State* L) { lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS); if (lua_getmetatable(L, -1)) { lua_getfield(L, -1, "__index"); if (lua_istable(L, -1)) { // Found real _G through proxy's __index lua_newtable(L); // Create _test table lua_setfield(L, -2, "_test"); lua_pop(L, 3); // pop real _G, metatable, proxy return; } lua_pop(L, 2); // pop __index, metatable } // No proxy, use directly lua_newtable(L); lua_setfield(L, -2, "_test"); lua_pop(L, 1); // pop _G } //============================================================================= // TEST DEFINITIONS //============================================================================= bool Test_DangerousGlobalsRemoved(std::string& error_msg) { LuaSandbox sandbox(TestContext()); std::string script = ReadFile(GetScriptsDir() + "/test_globals_removed.lua"); EXPECT_FALSE(script.empty()); EXPECT_TRUE(sandbox.LoadString(script, "test_globals_removed.lua")); return true; } bool Test_BytecodeRejected(std::string& error_msg) { LuaSandbox sandbox(TestContext()); // Lua 5.4 bytecode signature std::string bytecode = "\x1bLua\x54\x00\x19\x93\r\n\x1a\n"; EXPECT_FALSE(sandbox.LoadString(bytecode, "bytecode_test")); // Error should mention binary/bytecode std::string err = sandbox.GetLastError(); bool mentions_binary = (err.find("binary") != std::string::npos || err.find("attempt to load") != std::string::npos || err.find("text") != std::string::npos); EXPECT_TRUE(mentions_binary); return true; } bool Test_MemoryLimitEnforced(std::string& error_msg) { SandboxLimits limits; limits.memory_bytes = 512 * 1024; // 512 KB - very small LuaSandbox sandbox(TestContext(), limits); std::string script = ReadFile(GetScriptsDir() + "/test_memory_limit.lua"); EXPECT_FALSE(script.empty()); // Should fail due to memory exhaustion EXPECT_FALSE(sandbox.LoadString(script, "test_memory_limit.lua")); return true; } bool Test_CPULimitEnforced(std::string& error_msg) { SandboxLimits limits; limits.instructions_per_call = 10000; // Very low LuaSandbox sandbox(TestContext(), limits); std::string script = ReadFile(GetScriptsDir() + "/test_cpu_limit.lua"); EXPECT_FALSE(script.empty()); // Should fail due to instruction limit EXPECT_FALSE(sandbox.LoadString(script, "test_cpu_limit.lua")); // Error should mention instructions std::string err = sandbox.GetLastError(); EXPECT_CONTAINS(err, "instruction"); return true; } bool Test_MetatableProtected(std::string& error_msg) { LuaSandbox sandbox(TestContext()); std::string script = ReadFile(GetScriptsDir() + "/test_metatable_protected.lua"); EXPECT_FALSE(script.empty()); if (!sandbox.LoadString(script, "test_metatable_protected.lua")) { error_msg = "Script failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_SafeOperationsWork(std::string& error_msg) { LuaSandbox sandbox(TestContext()); std::string script = ReadFile(GetScriptsDir() + "/test_safe_operations.lua"); EXPECT_FALSE(script.empty()); EXPECT_TRUE(sandbox.LoadString(script, "test_safe_operations.lua")); return true; } bool Test_StringDumpRemoved(std::string& error_msg) { LuaSandbox sandbox(TestContext()); std::string script = ReadFile(GetScriptsDir() + "/test_string_dump_removed.lua"); EXPECT_FALSE(script.empty()); EXPECT_TRUE(sandbox.LoadString(script, "test_string_dump_removed.lua")); return true; } bool Test_MemoryTracking(std::string& error_msg) { LuaSandbox sandbox(TestContext()); // Initially should have some baseline memory size_t initial = sandbox.GetMemoryUsed(); EXPECT_TRUE(initial > 0); // Allocate some data sandbox.LoadString("local t = {}; for i=1,1000 do t[i] = string.rep('x', 100) end", "alloc"); // Memory should have increased size_t after = sandbox.GetMemoryUsed(); EXPECT_TRUE(after > initial); return true; } bool Test_InstructionCounting(std::string& error_msg) { SandboxLimits limits; limits.instructions_per_call = 1000000; // 1M instructions LuaSandbox sandbox(TestContext(), limits); // Run some code sandbox.LoadString("for i=1,10000 do local x = i * 2 end", "counting"); // Should have used some instructions EXPECT_TRUE(sandbox.GetInstructionsUsed() > 0); return true; } bool Test_MultipleLoads(std::string& error_msg) { LuaSandbox sandbox(TestContext()); // Should be able to load multiple scripts EXPECT_TRUE(sandbox.LoadString("local a = 1", "script1")); EXPECT_TRUE(sandbox.LoadString("local b = 2", "script2")); EXPECT_TRUE(sandbox.LoadString("local c = 3", "script3")); return true; } bool Test_ErrorRecovery(std::string& error_msg) { LuaSandbox sandbox(TestContext()); // Script with error EXPECT_FALSE(sandbox.LoadString("error('test error')", "error_script")); // Should still be able to run more code after error EXPECT_TRUE(sandbox.LoadString("local x = 1", "after_error")); return true; } //============================================================================= // PERMISSION SYSTEM TESTS (Milestone 2) //============================================================================= bool Test_NormalPermissionAutoGranted(std::string& error_msg) { SandboxContext ctx = TestContext(); ctx.permissions = {"internet", "vibrate"}; // Declare normal permissions mosis::PermissionGate gate(ctx); // Normal permissions should be auto-granted when declared EXPECT_TRUE(gate.HasPermission("internet")); EXPECT_TRUE(gate.HasPermission("vibrate")); return true; } bool Test_DangerousPermissionRequiresGrant(std::string& error_msg) { SandboxContext ctx = TestContext(); ctx.permissions = {"camera"}; // Declare dangerous permission mosis::PermissionGate gate(ctx); // Not granted yet (regular app) EXPECT_FALSE(gate.HasPermission("camera")); // Grant at runtime gate.GrantPermission("camera"); // Now should have it EXPECT_TRUE(gate.HasPermission("camera")); // Revoke gate.RevokePermission("camera"); EXPECT_FALSE(gate.HasPermission("camera")); return true; } bool Test_SignaturePermissionSystemOnly(std::string& error_msg) { // Non-system app SandboxContext ctx = TestContext(); ctx.permissions = {"system.settings"}; ctx.is_system_app = false; mosis::PermissionGate gate(ctx); EXPECT_FALSE(gate.HasPermission("system.settings")); // System app SandboxContext sys_ctx = TestContext(); sys_ctx.permissions = {"system.settings"}; sys_ctx.is_system_app = true; mosis::PermissionGate sys_gate(sys_ctx); EXPECT_TRUE(sys_gate.HasPermission("system.settings")); return true; } bool Test_UserGestureTracking(std::string& error_msg) { SandboxContext ctx = TestContext(); mosis::PermissionGate gate(ctx); // No recent gesture EXPECT_FALSE(gate.HasRecentUserGesture(5000)); // Record gesture gate.RecordUserGesture(); // Should have recent gesture EXPECT_TRUE(gate.HasRecentUserGesture(5000)); // Wait for gesture to expire (use short window) std::this_thread::sleep_for(std::chrono::milliseconds(100)); EXPECT_FALSE(gate.HasRecentUserGesture(50)); // 50ms window, we waited 100ms return true; } bool Test_UndeclaredPermissionDenied(std::string& error_msg) { SandboxContext ctx = TestContext(); ctx.permissions = {}; // No permissions declared mosis::PermissionGate gate(ctx); // Even normal permissions need to be declared EXPECT_FALSE(gate.HasPermission("internet")); // Dangerous permissions also denied EXPECT_FALSE(gate.HasPermission("camera")); return true; } bool Test_SystemAppGetsDangerousAuto(std::string& error_msg) { // System apps get dangerous permissions automatically (no runtime grant needed) SandboxContext ctx = TestContext(); ctx.permissions = {"camera", "microphone"}; ctx.is_system_app = true; mosis::PermissionGate gate(ctx); // System app should have dangerous perms without explicit grant EXPECT_TRUE(gate.HasPermission("camera")); EXPECT_TRUE(gate.HasPermission("microphone")); return true; } bool Test_PermissionCategoryCheck(std::string& error_msg) { // Check that permission categories are correct EXPECT_TRUE(mosis::PermissionGate::GetCategory("internet") == mosis::PermissionCategory::Normal); EXPECT_TRUE(mosis::PermissionGate::GetCategory("camera") == mosis::PermissionCategory::Dangerous); EXPECT_TRUE(mosis::PermissionGate::GetCategory("system.settings") == mosis::PermissionCategory::Signature); // Unknown permissions default to Dangerous EXPECT_TRUE(mosis::PermissionGate::GetCategory("unknown.perm") == mosis::PermissionCategory::Dangerous); return true; } //============================================================================= // AUDIT LOG TESTS (Milestone 3) //============================================================================= bool Test_AuditLogBasic(std::string& error_msg) { mosis::AuditLog log(1000); log.Log(mosis::AuditEvent::AppStart, "test.app", "App started"); log.Log(mosis::AuditEvent::PermissionCheck, "test.app", "camera", true); log.Log(mosis::AuditEvent::PermissionDenied, "test.app", "microphone", false); auto entries = log.GetEntries(10); EXPECT_TRUE(entries.size() == 3); auto app_entries = log.GetEntriesForApp("test.app", 10); EXPECT_TRUE(app_entries.size() == 3); // Check event filtering auto denied_entries = log.GetEntriesByEvent(mosis::AuditEvent::PermissionDenied, 10); EXPECT_TRUE(denied_entries.size() == 1); return true; } bool Test_AuditLogRingBuffer(std::string& error_msg) { mosis::AuditLog log(100); // Small buffer // Log more than capacity for (int i = 0; i < 200; i++) { log.Log(mosis::AuditEvent::Custom, "test.app", std::to_string(i)); } // Should only have latest 100 stored auto entries = log.GetEntries(200); EXPECT_TRUE(entries.size() == 100); // Total logged should be 200 EXPECT_TRUE(log.GetTotalEntries() == 200); // Most recent should be "199" EXPECT_TRUE(entries[0].details == "199"); return true; } bool Test_AuditLogThreadSafe(std::string& error_msg) { mosis::AuditLog log(10000); // Spawn multiple threads logging concurrently std::vector threads; for (int t = 0; t < 4; t++) { threads.emplace_back([&log, t]() { for (int i = 0; i < 1000; i++) { log.Log(mosis::AuditEvent::Custom, "app" + std::to_string(t), std::to_string(i)); } }); } for (auto& thread : threads) { thread.join(); } // Should have logged 4000 entries EXPECT_TRUE(log.GetTotalEntries() == 4000); return true; } //============================================================================= // RATE LIMITER TESTS (Milestone 3) //============================================================================= bool Test_RateLimiterBasic(std::string& error_msg) { mosis::RateLimiter limiter; // Should succeed initially (has tokens) EXPECT_TRUE(limiter.Check("test.app", "network.request")); return true; } bool Test_RateLimiterExhaustion(std::string& error_msg) { mosis::RateLimiter limiter; limiter.SetLimit("test.op", {0.0, 5.0}); // 5 tokens, no refill // Use all tokens for (int i = 0; i < 5; i++) { EXPECT_TRUE(limiter.Check("test.app", "test.op")); } // Should be denied now EXPECT_FALSE(limiter.Check("test.app", "test.op")); return true; } bool Test_RateLimiterRefill(std::string& error_msg) { mosis::RateLimiter limiter; limiter.SetLimit("test.op", {1000.0, 1.0}); // 1000/sec, max 1 token // Use the token EXPECT_TRUE(limiter.Check("test.app", "test.op")); EXPECT_FALSE(limiter.Check("test.app", "test.op")); // Wait a bit for refill (2ms = ~2 tokens at 1000/sec, but max is 1) std::this_thread::sleep_for(std::chrono::milliseconds(5)); // Should have token again EXPECT_TRUE(limiter.Check("test.app", "test.op")); return true; } bool Test_RateLimiterAppIsolation(std::string& error_msg) { mosis::RateLimiter limiter; limiter.SetLimit("test.op", {0.0, 1.0}); // 1 token, no refill // App 1 uses its token EXPECT_TRUE(limiter.Check("app1", "test.op")); EXPECT_FALSE(limiter.Check("app1", "test.op")); // App 2 should still have its token EXPECT_TRUE(limiter.Check("app2", "test.op")); return true; } bool Test_RateLimiterReset(std::string& error_msg) { mosis::RateLimiter limiter; limiter.SetLimit("test.op", {0.0, 2.0}); // 2 tokens, no refill // Use all tokens EXPECT_TRUE(limiter.Check("test.app", "test.op")); EXPECT_TRUE(limiter.Check("test.app", "test.op")); EXPECT_FALSE(limiter.Check("test.app", "test.op")); // Reset the app limiter.ResetApp("test.app"); // Should have tokens again EXPECT_TRUE(limiter.Check("test.app", "test.op")); return true; } bool Test_RateLimiterNoConfig(std::string& error_msg) { mosis::RateLimiter limiter; // Operation with no config should always succeed for (int i = 0; i < 100; i++) { EXPECT_TRUE(limiter.Check("test.app", "unconfigured.operation")); } return true; } //============================================================================= // PATH SANDBOX TESTS (Milestone 4) //============================================================================= bool Test_PathRejectsTraversal(std::string& error_msg) { mosis::PathSandbox sandbox("D:/test/app"); EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("../etc/passwd")); EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("foo/../../../bar")); EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..\\windows\\system32")); EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("data/..")); EXPECT_TRUE(mosis::PathSandbox::ContainsTraversal("..")); // Should not match ".." in filenames EXPECT_FALSE(mosis::PathSandbox::ContainsTraversal("file..txt")); EXPECT_FALSE(mosis::PathSandbox::ContainsTraversal("test...name")); std::string canonical; EXPECT_FALSE(sandbox.ValidatePath("../etc/passwd", canonical)); EXPECT_FALSE(sandbox.ValidatePath("data/../../../etc/passwd", canonical)); return true; } bool Test_PathRejectsAbsolute(std::string& error_msg) { EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("/etc/passwd")); EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("C:\\Windows\\System32")); EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("D:/test/file.txt")); EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("\\\\server\\share")); EXPECT_TRUE(mosis::PathSandbox::IsAbsolutePath("//server/share")); EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("scripts/utils.lua")); EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("./data/file.txt")); EXPECT_FALSE(mosis::PathSandbox::IsAbsolutePath("data/config.json")); mosis::PathSandbox sandbox("D:/test/app"); std::string canonical; EXPECT_FALSE(sandbox.ValidatePath("/etc/passwd", canonical)); EXPECT_FALSE(sandbox.ValidatePath("C:\\Windows\\System32\\file.dll", canonical)); return true; } bool Test_PathAcceptsValid(std::string& error_msg) { mosis::PathSandbox sandbox(GetScriptsDir()); std::string canonical; EXPECT_TRUE(sandbox.ValidatePath("test_globals_removed.lua", canonical)); EXPECT_TRUE(sandbox.ValidatePath("./test_memory_limit.lua", canonical)); return true; } bool Test_ModuleNameValidation(std::string& error_msg) { // Valid names EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("utils")); EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("my_module")); EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("ui.button")); EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("a.b.c")); EXPECT_TRUE(mosis::PathSandbox::IsValidModuleName("Module123")); // Invalid names EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName(".utils")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("utils.")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("ui..button")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("../evil")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("/etc/passwd")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo;bar")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo/bar")); EXPECT_FALSE(mosis::PathSandbox::IsValidModuleName("foo\\bar")); return true; } bool Test_ModuleToPath(std::string& error_msg) { EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("utils") == "scripts/utils.lua"); EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("ui.button") == "scripts/ui/button.lua"); EXPECT_TRUE(mosis::PathSandbox::ModuleToPath("a.b.c") == "scripts/a/b/c.lua"); return true; } bool Test_SafeRequireLoads(std::string& error_msg) { // Create sandbox with scripts directory as app path // The test_module.lua is in scripts/scripts/ so after ModuleToPath // it becomes scripts/scripts/test_module.lua // Safe require is auto-registered by LuaSandbox when app_path is set SandboxContext ctx = TestContext(); ctx.app_path = GetScriptsDir(); // "scripts" LuaSandbox sandbox(ctx); // Should be able to require a test module std::string script = "local m = require('test_module')\n" "if m.value ~= 42 then\n" " error('module value mismatch')\n" "end\n" "return true\n"; if (!sandbox.LoadString(script, "require_test")) { error_msg = "Failed to load module: " + sandbox.GetLastError(); return false; } return true; } bool Test_SafeRequireCaches(std::string& error_msg) { // Safe require is auto-registered by LuaSandbox when app_path is set SandboxContext ctx = TestContext(); ctx.app_path = GetScriptsDir(); LuaSandbox sandbox(ctx); std::string script = "local m1 = require('test_module')\n" "local m2 = require('test_module')\n" "if m1 ~= m2 then\n" " error('modules should be same (cached)')\n" "end\n" "return true\n"; if (!sandbox.LoadString(script, "cache_test")) { error_msg = "Cache test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_SafeRequireRejectsInvalid(std::string& error_msg) { // Safe require is auto-registered by LuaSandbox when app_path is set SandboxContext ctx = TestContext(); ctx.app_path = GetScriptsDir(); LuaSandbox sandbox(ctx); // Should reject path traversal in module name EXPECT_FALSE(sandbox.LoadString("require('../evil')", "evil_require")); // Should reject absolute paths EXPECT_FALSE(sandbox.LoadString("require('/etc/passwd')", "abs_require")); // Should reject special characters EXPECT_FALSE(sandbox.LoadString("require('foo;bar')", "special_require")); // Should reject empty EXPECT_FALSE(sandbox.LoadString("require('')", "empty_require")); return true; } //============================================================================= // TIMER MANAGER TESTS (Milestone 5) //============================================================================= bool Test_SetTimeoutFires(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; // Setup _test table for storing state (bypasses proxy __newindex) SetupTestTable(sandbox.GetState()); // Register timer API mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); // Set a timeout that modifies _test table std::string script = "_test.fired = false\n" "setTimeout(function() _test.fired = true end, 50)\n"; if (!sandbox.LoadString(script, "timeout_test")) { error_msg = "Failed to set timeout: " + sandbox.GetLastError(); return false; } // Process timers after delay std::this_thread::sleep_for(std::chrono::milliseconds(100)); manager.ProcessTimers(); // Check if callback fired if (!sandbox.LoadString("assert(_test.fired == true, 'callback did not fire')", "check")) { error_msg = "Timeout callback did not fire: " + sandbox.GetLastError(); return false; } return true; } bool Test_SetIntervalFires(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; // Setup _test table for storing state SetupTestTable(sandbox.GetState()); mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); std::string script = "_test.count = 0\n" "setInterval(function() _test.count = _test.count + 1 end, 30)\n"; if (!sandbox.LoadString(script, "interval_test")) { error_msg = "Failed to set interval: " + sandbox.GetLastError(); return false; } // Process multiple times for (int i = 0; i < 5; i++) { std::this_thread::sleep_for(std::chrono::milliseconds(40)); manager.ProcessTimers(); } // Should have fired multiple times if (!sandbox.LoadString("assert(_test.count >= 3, 'interval fired only ' .. _test.count .. ' times')", "check")) { error_msg = "Interval did not fire enough times: " + sandbox.GetLastError(); return false; } return true; } bool Test_ClearTimeoutCancels(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; // Setup _test table for storing state SetupTestTable(sandbox.GetState()); mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); std::string script = "_test.fired = false\n" "local id = setTimeout(function() _test.fired = true end, 100)\n" "clearTimeout(id)\n"; if (!sandbox.LoadString(script, "clear_test")) { error_msg = "Failed to clear timeout: " + sandbox.GetLastError(); return false; } std::this_thread::sleep_for(std::chrono::milliseconds(150)); manager.ProcessTimers(); // Should NOT have fired if (!sandbox.LoadString("assert(_test.fired == false, 'callback should not have fired')", "check")) { error_msg = "Cancelled timeout still fired: " + sandbox.GetLastError(); return false; } return true; } bool Test_ClearIntervalCancels(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; // Setup _test table for storing state SetupTestTable(sandbox.GetState()); mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); // Store both count and interval ID in _test table so they persist across LoadString calls std::string script = "_test.count = 0\n" "_test.id = setInterval(function() _test.count = _test.count + 1 end, 30)\n"; if (!sandbox.LoadString(script, "interval_setup")) { error_msg = "Failed to set interval: " + sandbox.GetLastError(); return false; } // Let it fire once std::this_thread::sleep_for(std::chrono::milliseconds(40)); manager.ProcessTimers(); // Now cancel it sandbox.LoadString("clearInterval(_test.id)", "cancel"); // Wait and process more std::this_thread::sleep_for(std::chrono::milliseconds(100)); manager.ProcessTimers(); // Should have fired only once (or maybe twice due to timing) if (!sandbox.LoadString("assert(_test.count <= 2, 'interval fired too many times: ' .. _test.count)", "check")) { error_msg = "Interval kept firing after cancel: " + sandbox.GetLastError(); return false; } return true; } bool Test_TimerLimitEnforced(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); // Try to create more than MAX_TIMERS_PER_APP (100) timers std::string script = "created = 0\n" "for i = 1, 150 do\n" " local ok, err = pcall(function()\n" " setTimeout(function() end, 1000000)\n" " end)\n" " if ok then created = created + 1 end\n" "end\n"; sandbox.LoadString(script, "limit_test"); // Should be capped at MAX_TIMERS_PER_APP size_t count = manager.GetTimerCount(ctx.app_id); if (count > 100) { error_msg = "Timer limit not enforced: " + std::to_string(count) + " timers created"; return false; } return true; } bool Test_ClearAppTimersCleanup(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); std::string script = "for i = 1, 10 do\n" " setTimeout(function() end, 1000000)\n" "end\n"; sandbox.LoadString(script, "cleanup_test"); size_t before = manager.GetTimerCount(ctx.app_id); EXPECT_TRUE(before == 10); // Clear all timers for app (simulating app stop) manager.ClearAppTimers(ctx.app_id); size_t after = manager.GetTimerCount(ctx.app_id); EXPECT_TRUE(after == 0); return true; } bool Test_MinIntervalEnforced(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); // Manager must be declared AFTER sandbox so it's destroyed BEFORE sandbox mosis::TimerManager manager; // Setup _test table for storing state SetupTestTable(sandbox.GetState()); mosis::RegisterTimerAPI(sandbox.GetState(), &manager, ctx.app_id); // Try to set interval less than minimum (10ms) std::string script = "_test.count = 0\n" "setInterval(function() _test.count = _test.count + 1 end, 1)\n"; // 1ms, should be clamped to 10ms sandbox.LoadString(script, "min_interval_test"); // With 1ms interval, in 50ms we'd get 50 callbacks // With 10ms minimum, we should get ~5 std::this_thread::sleep_for(std::chrono::milliseconds(55)); for (int i = 0; i < 10; i++) { manager.ProcessTimers(); } if (!sandbox.LoadString("assert(_test.count <= 10, 'interval fired too often: ' .. _test.count)", "check")) { error_msg = "Minimum interval not enforced: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MILESTONE 6: JSON & CRYPTO API TESTS //============================================================================= bool Test_JsonDecodeValid(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::RegisterJsonAPI(sandbox.GetState()); std::string script = R"( local obj = json.decode('{"name":"test","value":42,"arr":[1,2,3]}') assert(obj.name == "test", "name should be test") assert(obj.value == 42, "value should be 42") assert(#obj.arr == 3, "arr should have 3 elements") assert(obj.arr[1] == 1, "first element should be 1") )"; if (!sandbox.LoadString(script, "decode_test")) { error_msg = "JSON decode failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_JsonDecodeRejectsDeep(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::JsonLimits limits; limits.max_depth = 5; mosis::RegisterJsonAPI(sandbox.GetState(), limits); // Create deeply nested JSON (10 levels) std::string script = R"( local deep_json = '[[[[[[[[[[1]]]]]]]]]]' local result, err = json.decode(deep_json) assert(result == nil, 'should fail on deep nesting') assert(err and err:find('depth'), 'error should mention depth') )"; if (!sandbox.LoadString(script, "deep_test")) { error_msg = "Test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_JsonEncodeValid(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::RegisterJsonAPI(sandbox.GetState()); std::string script = R"( local str = json.encode({name = "test", value = 42}) assert(type(str) == "string", "should return string") -- Decode back to verify round-trip local obj = json.decode(str) assert(obj.name == "test", "round-trip name") assert(obj.value == 42, "round-trip value") )"; if (!sandbox.LoadString(script, "encode_test")) { error_msg = "JSON encode failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_JsonEncodeDetectsCycles(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::RegisterJsonAPI(sandbox.GetState()); std::string script = R"( local t = {a = 1} t.self = t -- Create cycle local result, err = json.encode(t) assert(result == nil, 'should fail on cycle') assert(err and (err:find('cycle') or err:find('circular')), 'should mention cycle') )"; if (!sandbox.LoadString(script, "cycle_test")) { error_msg = "Test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_JsonRejectsTooLarge(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::JsonLimits limits; limits.max_array_size = 10; mosis::RegisterJsonAPI(sandbox.GetState(), limits); std::string script = R"( -- Try to decode array with 20 elements local result, err = json.decode('[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]') assert(result == nil, 'should fail on large array') assert(err and (err:find('size') or err:find('limit')), 'should mention size limit') )"; if (!sandbox.LoadString(script, "size_test")) { error_msg = "Test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_CryptoRandomBytes(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::RegisterCryptoAPI(sandbox.GetState()); std::string script = R"( local bytes = crypto.randomBytes(16) assert(#bytes == 16, 'should be 16 bytes') -- Should be different each time local bytes2 = crypto.randomBytes(16) assert(bytes ~= bytes2, 'should be random') )"; if (!sandbox.LoadString(script, "random_test")) { error_msg = "Random bytes test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_CryptoHashSHA256(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::RegisterCryptoAPI(sandbox.GetState()); std::string script = R"( local hash = crypto.hash("sha256", "hello") -- Known SHA256 of "hello" local expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" assert(hash == expected, 'SHA256 mismatch: got ' .. hash) )"; if (!sandbox.LoadString(script, "hash_test")) { error_msg = "Hash test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_CryptoHMAC(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::RegisterCryptoAPI(sandbox.GetState()); std::string script = R"( local hmac = crypto.hmac("sha256", "key", "message") -- Known HMAC-SHA256 of "message" with key "key" local expected = "6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4a" assert(hmac == expected, 'HMAC mismatch: got ' .. hmac) )"; if (!sandbox.LoadString(script, "hmac_test")) { error_msg = "HMAC test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_SecureMathRandom(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::SecureRandom rng; mosis::RegisterSecureMathRandom(sandbox.GetState(), &rng); std::string script = R"( -- math.randomseed should be removed assert(math.randomseed == nil, 'randomseed should be removed') -- math.random should work local r1 = math.random() assert(r1 >= 0 and r1 < 1, 'random() should return [0,1)') local r2 = math.random(10) assert(r2 >= 1 and r2 <= 10, 'random(n) should return [1,n]') local r3 = math.random(5, 15) assert(r3 >= 5 and r3 <= 15, 'random(m,n) should return [m,n]') )"; if (!sandbox.LoadString(script, "math_random_test")) { error_msg = "Math.random test failed: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MILESTONE 7: VIRTUAL FILESYSTEM TESTS //============================================================================= bool Test_VirtualFSReadWrite(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFS vfs("test.app", test_root); std::string err; // Write a file EXPECT_TRUE(vfs.Write("/data/test.txt", "Hello World", err)); // Read it back auto content = vfs.Read("/data/test.txt", err); EXPECT_TRUE(content.has_value()); EXPECT_TRUE(*content == "Hello World"); // Cleanup vfs.ClearAll(); return true; } bool Test_VirtualFSBlocksTraversal(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFS vfs("test.app", test_root); std::string err; // Should reject traversal EXPECT_FALSE(vfs.Write("/data/../../../etc/passwd", "hack", err)); EXPECT_TRUE(err.find("traversal") != std::string::npos || err.find("invalid") != std::string::npos); // Should reject paths without valid prefix err.clear(); EXPECT_FALSE(vfs.Write("/etc/passwd", "hack", err)); vfs.ClearAll(); return true; } bool Test_VirtualFSEnforcesQuota(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFSLimits limits; limits.max_quota_bytes = 1024; // 1 KB quota for testing mosis::VirtualFS vfs("test.app", test_root, limits); std::string err; // Write should succeed std::string small_data(500, 'a'); EXPECT_TRUE(vfs.Write("/data/file1.txt", small_data, err)); // Second write should fail (exceeds quota) std::string large_data(600, 'b'); EXPECT_FALSE(vfs.Write("/data/file2.txt", large_data, err)); EXPECT_TRUE(err.find("quota") != std::string::npos); vfs.ClearAll(); return true; } bool Test_VirtualFSCleansUpTemp(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFS vfs("test.app", test_root); std::string err; // Write to temp EXPECT_TRUE(vfs.Write("/temp/session.txt", "temp data", err)); EXPECT_TRUE(vfs.Exists("/temp/session.txt")); // Clear temp vfs.ClearTemp(); // Should be gone EXPECT_FALSE(vfs.Exists("/temp/session.txt")); vfs.ClearAll(); return true; } bool Test_VirtualFSList(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFS vfs("test.app", test_root); std::string err; // Create some files vfs.Write("/data/file1.txt", "content1", err); vfs.Write("/data/file2.txt", "content2", err); vfs.MakeDir("/data/subdir", err); // List directory auto files = vfs.List("/data/", err); EXPECT_TRUE(files.has_value()); EXPECT_TRUE(files->size() == 3); vfs.ClearAll(); return true; } bool Test_VirtualFSStat(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFS vfs("test.app", test_root); std::string err; // Write a file vfs.Write("/data/test.txt", "Hello", err); // Get stat auto stat = vfs.Stat("/data/test.txt", err); EXPECT_TRUE(stat.has_value()); EXPECT_TRUE(stat->size == 5); EXPECT_FALSE(stat->is_dir); // Directory stat vfs.MakeDir("/data/subdir", err); auto dir_stat = vfs.Stat("/data/subdir", err); EXPECT_TRUE(dir_stat.has_value()); EXPECT_TRUE(dir_stat->is_dir); vfs.ClearAll(); return true; } bool Test_VirtualFSLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); std::string test_root = "test_vfs_lua"; mosis::VirtualFS vfs("test.app", test_root); mosis::RegisterVirtualFS(sandbox.GetState(), &vfs); std::string script = R"( -- Write and read local ok, err = fs.write("/data/test.txt", "Hello from Lua") assert(ok, "write failed: " .. (err or "")) local content, err = fs.read("/data/test.txt") assert(content == "Hello from Lua", "content mismatch: " .. (content or "nil")) -- Check exists assert(fs.exists("/data/test.txt"), "file should exist") assert(not fs.exists("/data/nonexistent.txt"), "file should not exist") -- Stat local stat = fs.stat("/data/test.txt") assert(stat.size == 14, "size should be 14, got " .. tostring(stat.size)) assert(not stat.isDir, "should not be dir") )"; bool ok = sandbox.LoadString(script, "vfs_test"); vfs.ClearAll(); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_VirtualFSMaxFileSize(std::string& error_msg) { std::string test_root = "test_vfs_app"; mosis::VirtualFSLimits limits; limits.max_file_size = 100; // 100 bytes max limits.max_quota_bytes = 10000; // Large quota mosis::VirtualFS vfs("test.app", test_root, limits); std::string err; // Small file should succeed EXPECT_TRUE(vfs.Write("/data/small.txt", std::string(50, 'a'), err)); // Large file should fail EXPECT_FALSE(vfs.Write("/data/large.txt", std::string(200, 'b'), err)); EXPECT_TRUE(err.find("size") != std::string::npos); vfs.ClearAll(); return true; } //============================================================================= // MILESTONE 8: SQLITE DATABASE TESTS //============================================================================= bool Test_DatabaseCreatesTables(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; auto db = manager.Open("test", err); EXPECT_TRUE(db != nullptr); // Create table EXPECT_TRUE(db->Execute( "CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)", {}, err)); // Insert EXPECT_TRUE(db->Execute( "INSERT INTO items (name) VALUES (?)", {std::string("Test Item")}, err)); // Query auto rows = db->Query("SELECT * FROM items", {}, err); EXPECT_TRUE(rows.has_value()); EXPECT_TRUE(rows->size() == 1); db->Close(); manager.CloseAll(); // Cleanup std::filesystem::remove_all(test_root); return true; } bool Test_DatabasePreparedStatements(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; auto db = manager.Open("test", err); EXPECT_TRUE(db != nullptr); db->Execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", {}, err); db->Execute("INSERT INTO users (name) VALUES (?)", {std::string("Alice")}, err); // Attempt SQL injection via parameter - should be safely escaped std::string malicious = "'; DROP TABLE users; --"; auto rows = db->Query("SELECT * FROM users WHERE name = ?", {malicious}, err); // Query should succeed (finding nothing) and table should still exist EXPECT_TRUE(rows.has_value()); EXPECT_TRUE(rows->size() == 0); // Table should still exist auto check = db->Query("SELECT * FROM users", {}, err); EXPECT_TRUE(check.has_value()); EXPECT_TRUE(check->size() == 1); db->Close(); std::filesystem::remove_all(test_root); return true; } bool Test_DatabaseBlocksAttach(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; auto db = manager.Open("test", err); EXPECT_TRUE(db != nullptr); // Try to attach another database - should fail EXPECT_FALSE(db->Execute("ATTACH DATABASE '/etc/passwd' AS evil", {}, err)); EXPECT_TRUE(err.find("authorized") != std::string::npos || err.find("denied") != std::string::npos || err.find("not authorized") != std::string::npos); db->Close(); std::filesystem::remove_all(test_root); return true; } bool Test_DatabaseBlocksDangerousPragma(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; auto db = manager.Open("test", err); EXPECT_TRUE(db != nullptr); // Try dangerous PRAGMAs - should fail EXPECT_FALSE(db->Execute("PRAGMA journal_mode = OFF", {}, err)); err.clear(); EXPECT_FALSE(db->Execute("PRAGMA synchronous = OFF", {}, err)); // Safe PRAGMAs should work err.clear(); auto rows = db->Query("PRAGMA table_info(sqlite_master)", {}, err); EXPECT_TRUE(rows.has_value()); db->Close(); std::filesystem::remove_all(test_root); return true; } bool Test_DatabaseMultiple(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; auto db1 = manager.Open("db1", err); auto db2 = manager.Open("db2", err); EXPECT_TRUE(db1 != nullptr); EXPECT_TRUE(db2 != nullptr); EXPECT_TRUE(manager.GetOpenDatabaseCount() == 2); // They should be independent db1->Execute("CREATE TABLE t1 (x INTEGER)", {}, err); db2->Execute("CREATE TABLE t2 (y INTEGER)", {}, err); // t1 shouldn't exist in db2 auto rows = db2->Query("SELECT * FROM t1", {}, err); EXPECT_FALSE(rows.has_value()); // Should fail - table doesn't exist manager.CloseAll(); std::filesystem::remove_all(test_root); return true; } bool Test_DatabaseLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); std::string test_root = "test_db_lua"; mosis::DatabaseManager manager("test.app", test_root); mosis::RegisterDatabaseAPI(sandbox.GetState(), &manager); std::string script = R"lua( -- Just test if database global exists if not database then error("database global not found") end if not database.open then error("database.open not found") end )lua"; bool ok = sandbox.LoadString(script, "db_test"); manager.CloseAll(); std::filesystem::remove_all(test_root); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } bool Test_DatabaseInvalidNames(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; // Path traversal auto db1 = manager.Open("../evil", err); EXPECT_TRUE(db1 == nullptr); // Absolute path component err.clear(); auto db2 = manager.Open("/etc/passwd", err); EXPECT_TRUE(db2 == nullptr); // Special characters err.clear(); auto db3 = manager.Open("test;drop", err); EXPECT_TRUE(db3 == nullptr); std::filesystem::remove_all(test_root); return true; } bool Test_DatabaseLastInsertAndChanges(std::string& error_msg) { std::string test_root = "test_db_app"; mosis::DatabaseManager manager("test.app", test_root); std::string err; auto db = manager.Open("test", err); EXPECT_TRUE(db != nullptr); db->Execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)", {}, err); db->Execute("INSERT INTO items (name) VALUES (?)", {std::string("Item 1")}, err); EXPECT_TRUE(db->GetLastInsertRowId() == 1); EXPECT_TRUE(db->GetChanges() == 1); db->Execute("INSERT INTO items (name) VALUES (?)", {std::string("Item 2")}, err); EXPECT_TRUE(db->GetLastInsertRowId() == 2); db->Execute("UPDATE items SET name = ?", {std::string("Updated")}, err); EXPECT_TRUE(db->GetChanges() == 2); // Updated 2 rows db->Close(); std::filesystem::remove_all(test_root); return true; } //============================================================================= // MILESTONE 9: NETWORK HTTP TESTS //============================================================================= bool Test_NetworkBlocksPrivateIP(std::string& error_msg) { mosis::NetworkManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; // All these should be blocked std::vector private_urls = { "https://127.0.0.1/api", "https://10.0.0.1/api", "https://192.168.1.1/api", "https://172.16.0.1/api", "https://169.254.169.254/latest/meta-data/", "https://localhost/api", "https://0.0.0.0/api" }; for (const auto& url : private_urls) { mosis::HttpRequest req; req.url = url; auto response = manager.Request(req, err); if (response.status_code != 0 || err.empty()) { error_msg = "Expected " + url + " to be blocked, but it wasn't"; return false; } err.clear(); } return true; } bool Test_NetworkBlocksPlainHttp(std::string& error_msg) { mosis::NetworkManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; mosis::HttpRequest req; req.url = "http://example.com/api"; // No HTTPS auto response = manager.Request(req, err); EXPECT_TRUE(response.status_code == 0); EXPECT_TRUE(err.find("HTTPS") != std::string::npos); return true; } bool Test_NetworkRequiresHttps(std::string& error_msg) { mosis::HttpValidator validator; std::string err; // HTTPS should validate auto parsed = validator.Validate("https://example.com/api", err); EXPECT_TRUE(parsed.has_value()); EXPECT_TRUE(parsed->scheme == "https"); // HTTP should fail validation err.clear(); parsed = validator.Validate("http://example.com/api", err); EXPECT_FALSE(parsed.has_value()); EXPECT_TRUE(err.find("HTTPS") != std::string::npos); return true; } bool Test_NetworkEnforcesDomainWhitelist(std::string& error_msg) { mosis::NetworkManager manager("test.app"); // Set allowed domains manager.SetAllowedDomains({"api.example.com", "cdn.example.com"}); std::string err; // Allowed domain should validate auto parsed = manager.GetValidator().Validate("https://api.example.com/data", err); EXPECT_TRUE(parsed.has_value()); // Disallowed domain should fail err.clear(); parsed = manager.GetValidator().Validate("https://evil.com/steal", err); EXPECT_FALSE(parsed.has_value()); EXPECT_TRUE(err.find("allowed") != std::string::npos || err.find("whitelist") != std::string::npos || err.find("not in allowed") != std::string::npos); return true; } bool Test_NetworkUrlParsing(std::string& error_msg) { mosis::HttpValidator validator; std::string err; // Full URL with port auto parsed = validator.Validate("https://api.example.com:8443/path/to/resource?key=value", err); EXPECT_TRUE(parsed.has_value()); EXPECT_TRUE(parsed->scheme == "https"); EXPECT_TRUE(parsed->host == "api.example.com"); EXPECT_TRUE(parsed->port == 8443); EXPECT_TRUE(parsed->path == "/path/to/resource"); EXPECT_TRUE(parsed->query == "?key=value"); // Default port err.clear(); parsed = validator.Validate("https://example.com/api", err); EXPECT_TRUE(parsed.has_value()); EXPECT_TRUE(parsed->port == 443); // IP address err.clear(); parsed = validator.Validate("https://192.0.2.1/api", err); // TEST-NET-1, documentation IP EXPECT_TRUE(parsed.has_value()); EXPECT_TRUE(parsed->is_ip_address); return true; } bool Test_NetworkBlocksMetadata(std::string& error_msg) { mosis::NetworkManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; // AWS metadata mosis::HttpRequest req; req.url = "https://169.254.169.254/latest/meta-data/"; auto response = manager.Request(req, err); EXPECT_TRUE(response.status_code == 0); // GCP metadata hostname err.clear(); req.url = "https://metadata.google.internal/computeMetadata/v1/"; response = manager.Request(req, err); EXPECT_TRUE(response.status_code == 0); return true; } bool Test_NetworkRequestLimits(std::string& error_msg) { mosis::NetworkLimits limits; limits.max_request_body = 1024; // 1 KB for testing mosis::NetworkManager manager("test.app", limits); manager.ClearDomainRestrictions(); std::string err; mosis::HttpRequest req; req.url = "https://example.com/api"; req.method = "POST"; req.body = std::string(2048, 'X'); // 2 KB - exceeds limit auto response = manager.Request(req, err); EXPECT_TRUE(response.status_code == 0); EXPECT_TRUE(err.find("size") != std::string::npos || err.find("limit") != std::string::npos || err.find("large") != std::string::npos); return true; } bool Test_NetworkLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::NetworkManager manager("test.app"); manager.ClearDomainRestrictions(); mosis::RegisterNetworkAPI(sandbox.GetState(), &manager); std::string script = R"lua( -- Test that network global exists if not network then error("network global not found") end if not network.request then error("network.request not found") end -- Test validation rejection (private IP) local response, err = network.request({ url = "https://127.0.0.1/api" }) if response then error("expected private IP to be blocked") end )lua"; bool ok = sandbox.LoadString(script, "network_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MILESTONE 10: WebSocket //============================================================================= bool Test_WebSocketUrlValidation(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; // WSS should be allowed (will fail in mock mode but validation passes) auto ws = manager.Connect("wss://example.com/socket", err); EXPECT_TRUE(err.find("mock") != std::string::npos || err.find("disabled") != std::string::npos); // WS (plain) should be blocked at validation ws = manager.Connect("ws://example.com/socket", err); EXPECT_TRUE(ws == nullptr); EXPECT_TRUE(err.find("WSS") != std::string::npos || err.find("HTTPS") != std::string::npos || err.find("required") != std::string::npos); return true; } bool Test_WebSocketConnectionLimits(std::string& error_msg) { mosis::WebSocketLimits limits; limits.max_connections_per_app = 2; mosis::WebSocketManager manager("test.app", limits); manager.ClearDomainRestrictions(); manager.SetMockMode(false); // Disable mock to test connection tracking std::string err; // Create max connections auto ws1 = manager.Connect("wss://example.com/socket1", err); auto ws2 = manager.Connect("wss://example.com/socket2", err); // Third should fail auto ws3 = manager.Connect("wss://example.com/socket3", err); EXPECT_TRUE(ws3 == nullptr); EXPECT_TRUE(err.find("limit") != std::string::npos || err.find("Connection") != std::string::npos); return true; } bool Test_WebSocketBlocksPrivateIP(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; std::vector private_urls = { "wss://127.0.0.1/socket", "wss://localhost/socket", "wss://10.0.0.1/socket", "wss://192.168.1.1/socket", "wss://169.254.169.254/socket" }; for (const auto& url : private_urls) { auto ws = manager.Connect(url, err); EXPECT_TRUE(ws == nullptr); } return true; } bool Test_WebSocketDomainWhitelist(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.SetAllowedDomains({"api.example.com"}); std::string err; // Allowed domain - should pass validation (may fail in mock mode for other reasons) auto ws1 = manager.Connect("wss://api.example.com/socket", err); bool allowed_passed = (ws1 != nullptr) || (err.find("mock") != std::string::npos) || (err.find("disabled") != std::string::npos); EXPECT_TRUE(allowed_passed); // Disallowed domain - should fail validation auto ws2 = manager.Connect("wss://evil.com/socket", err); EXPECT_TRUE(ws2 == nullptr); EXPECT_TRUE(err.find("allowed") != std::string::npos || err.find("whitelist") != std::string::npos || err.find("Domain") != std::string::npos); return true; } bool Test_WebSocketMessageLimits(std::string& error_msg) { // Create a WebSocket directly to test send limits mosis::WebSocket ws(1, "wss://example.com/socket", 1024); // 1 KB limit // WebSocket is in Connecting state, so send should fail std::string small_message(512, 'X'); bool send_result = ws.Send(small_message); EXPECT_FALSE(send_result); // Not connected // Simulate open ws.SimulateOpen(); // Now small message should work send_result = ws.Send(small_message); EXPECT_TRUE(send_result); // Large message should fail std::string large_message(2048, 'X'); // 2 KB send_result = ws.Send(large_message); EXPECT_FALSE(send_result); return true; } bool Test_WebSocketCloseAll(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); manager.SetMockMode(false); // Disable mock to track connections std::string err; // Create some connections manager.Connect("wss://example.com/socket1", err); manager.Connect("wss://example.com/socket2", err); EXPECT_TRUE(manager.GetActiveConnectionCount() == 2); // Close all manager.CloseAll(); // Should have no active connections EXPECT_TRUE(manager.GetActiveConnectionCount() == 0); return true; } bool Test_WebSocketLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); mosis::RegisterWebSocketAPI(sandbox.GetState(), &manager); std::string script = R"lua( -- Test that network.websocket exists if not network then error("network global not found") end if not network.websocket then error("network.websocket not found") end -- Test validation rejection (private IP) local ws, err = network.websocket("wss://127.0.0.1/socket") if ws then error("expected private IP to be blocked") end )lua"; bool ok = sandbox.LoadString(script, "websocket_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MILESTONE 11: Camera //============================================================================= bool Test_CameraRequiresPermission(std::string& error_msg) { // Create sandbox context WITHOUT camera permission SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {}; // No camera permission declared ctx.is_system_app = false; PermissionGate permissions(ctx); mosis::CameraInterface camera("test.app", &permissions); camera.SimulateUserGesture(); std::string err; mosis::CameraConfig config; auto session = camera.StartSession(config, err); EXPECT_TRUE(session == nullptr); EXPECT_TRUE(err.find("permission") != std::string::npos); return true; } bool Test_CameraRequiresUserGesture(std::string& error_msg) { // Create sandbox context WITH camera permission SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"camera"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("camera"); mosis::CameraInterface camera("test.app", &permissions); // Note: NOT calling SimulateUserGesture() std::string err; mosis::CameraConfig config; auto session = camera.StartSession(config, err); EXPECT_TRUE(session == nullptr); EXPECT_TRUE(err.find("gesture") != std::string::npos); return true; } bool Test_CameraShowsIndicator(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"camera"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("camera"); mosis::CameraInterface camera("test.app", &permissions); camera.SimulateUserGesture(); EXPECT_FALSE(camera.IsIndicatorVisible()); std::string err; mosis::CameraConfig config; auto session = camera.StartSession(config, err); EXPECT_TRUE(session != nullptr); EXPECT_TRUE(camera.IsIndicatorVisible()); camera.StopSession(); EXPECT_FALSE(camera.IsIndicatorVisible()); return true; } bool Test_CameraSingleSession(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"camera"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("camera"); mosis::CameraInterface camera("test.app", &permissions); camera.SimulateUserGesture(); std::string err; mosis::CameraConfig config; // First session should succeed auto session1 = camera.StartSession(config, err); EXPECT_TRUE(session1 != nullptr); // Second session should fail camera.SimulateUserGesture(); auto session2 = camera.StartSession(config, err); EXPECT_TRUE(session2 == nullptr); EXPECT_TRUE(err.find("active") != std::string::npos || err.find("already") != std::string::npos); return true; } bool Test_CameraStopsOnShutdown(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"camera"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("camera"); mosis::CameraInterface camera("test.app", &permissions); camera.SimulateUserGesture(); std::string err; mosis::CameraConfig config; auto session = camera.StartSession(config, err); EXPECT_TRUE(session != nullptr); EXPECT_TRUE(camera.HasActiveSession()); // Simulate app stop camera.Shutdown(); EXPECT_FALSE(camera.HasActiveSession()); EXPECT_FALSE(camera.IsIndicatorVisible()); return true; } bool Test_CameraLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); ctx.permissions = {"camera"}; LuaSandbox sandbox(ctx); PermissionGate permissions(ctx); permissions.GrantPermission("camera"); mosis::CameraInterface camera("test.app", &permissions); camera.SimulateUserGesture(); mosis::RegisterCameraAPI(sandbox.GetState(), &camera); std::string script = R"lua( -- Test that camera global exists if not camera then error("camera global not found") end if not camera.start then error("camera.start not found") end if not camera.isActive then error("camera.isActive not found") end -- isActive should be false initially if camera.isActive() then error("camera should not be active initially") end )lua"; bool ok = sandbox.LoadString(script, "camera_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MILESTONE 12: Microphone //============================================================================= bool Test_MicrophoneRequiresPermission(std::string& error_msg) { // Create sandbox context WITHOUT microphone permission SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {}; // No microphone permission declared ctx.is_system_app = false; PermissionGate permissions(ctx); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session == nullptr); EXPECT_TRUE(err.find("permission") != std::string::npos); return true; } bool Test_MicrophoneRequiresUserGesture(std::string& error_msg) { // Create sandbox context WITH microphone permission SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); // Note: NOT calling SimulateUserGesture() std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session == nullptr); EXPECT_TRUE(err.find("gesture") != std::string::npos); return true; } bool Test_MicrophoneShowsIndicator(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); EXPECT_FALSE(mic.IsIndicatorVisible()); std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session != nullptr); EXPECT_TRUE(mic.IsIndicatorVisible()); mic.StopSession(); EXPECT_FALSE(mic.IsIndicatorVisible()); return true; } bool Test_MicrophoneSingleSession(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); std::string err; mosis::RecordingConfig config; // First session should succeed auto session1 = mic.StartSession(config, err); EXPECT_TRUE(session1 != nullptr); // Second session should fail mic.SimulateUserGesture(); auto session2 = mic.StartSession(config, err); EXPECT_TRUE(session2 == nullptr); EXPECT_TRUE(err.find("active") != std::string::npos || err.find("already") != std::string::npos); return true; } bool Test_MicrophoneStopsOnShutdown(std::string& error_msg) { SandboxContext ctx; ctx.app_id = "test.app"; ctx.permissions = {"microphone"}; ctx.is_system_app = false; PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); std::string err; mosis::RecordingConfig config; auto session = mic.StartSession(config, err); EXPECT_TRUE(session != nullptr); EXPECT_TRUE(mic.HasActiveSession()); // Simulate app stop mic.Shutdown(); EXPECT_FALSE(mic.HasActiveSession()); EXPECT_FALSE(mic.IsIndicatorVisible()); return true; } bool Test_MicrophoneLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); ctx.permissions = {"microphone"}; LuaSandbox sandbox(ctx); PermissionGate permissions(ctx); permissions.GrantPermission("microphone"); mosis::MicrophoneInterface mic("test.app", &permissions); mic.SimulateUserGesture(); mosis::RegisterMicrophoneAPI(sandbox.GetState(), &mic); std::string script = R"lua( -- Test that microphone global exists if not microphone then error("microphone global not found") end if not microphone.start then error("microphone.start not found") end if not microphone.isActive then error("microphone.isActive not found") end -- isActive should be false initially if microphone.isActive() then error("microphone should not be active initially") end )lua"; bool ok = sandbox.LoadString(script, "microphone_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MILESTONE 13: Audio Output //============================================================================= bool Test_AudioPlaysSound(std::string& error_msg) { mosis::AudioOutputInterface audio("test.app"); mosis::AudioData data; data.sample_rate = 44100; data.channels = 1; data.bits_per_sample = 16; data.data = std::vector(1000); // Dummy data mosis::PlaybackConfig config; std::string err; auto player = audio.Play(data, config, err); EXPECT_TRUE(player != nullptr); EXPECT_TRUE(player->GetState() == mosis::AudioState::Playing); return true; } bool Test_AudioRespectsSystemVolume(std::string& error_msg) { mosis::AudioOutputInterface audio("test.app"); audio.SetSystemVolume(0.5f); mosis::AudioData data; data.data = std::vector(1000); mosis::PlaybackConfig config; config.volume = 1.0f; // App requests full volume std::string err; auto player = audio.Play(data, config, err); // Player is created with requested volume EXPECT_TRUE(player != nullptr); EXPECT_TRUE(player->GetVolume() == 1.0f); // But effective volume is capped by system // (In real implementation, audio subsystem applies this) return true; } bool Test_AudioLimitsConcurrent(std::string& error_msg) { mosis::AudioOutputInterface audio("test.app"); mosis::AudioData data; data.data = std::vector(1000); mosis::PlaybackConfig config; // Create MAX_CONCURRENT_SOUNDS players std::vector> players; for (int i = 0; i < 10; i++) { std::string err; auto player = audio.Play(data, config, err); EXPECT_TRUE(player != nullptr); players.push_back(player); } // 11th should fail std::string err; auto extra = audio.Play(data, config, err); EXPECT_TRUE(extra == nullptr); EXPECT_TRUE(err.find("limit") != std::string::npos || err.find("concurrent") != std::string::npos); return true; } bool Test_AudioStopAll(std::string& error_msg) { mosis::AudioOutputInterface audio("test.app"); mosis::AudioData data; data.data = std::vector(1000); mosis::PlaybackConfig config; std::string err; auto player1 = audio.Play(data, config, err); auto player2 = audio.Play(data, config, err); EXPECT_TRUE(audio.GetActivePlayerCount() == 2); audio.StopAll(); EXPECT_TRUE(audio.GetActivePlayerCount() == 0); return true; } bool Test_AudioStopsOnShutdown(std::string& error_msg) { mosis::AudioOutputInterface audio("test.app"); mosis::AudioData data; data.data = std::vector(1000); mosis::PlaybackConfig config; std::string err; auto player = audio.Play(data, config, err); EXPECT_TRUE(player != nullptr); // Simulate app stop audio.Shutdown(); EXPECT_TRUE(audio.GetActivePlayerCount() == 0); return true; } bool Test_AudioLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::AudioOutputInterface audio("test.app"); mosis::RegisterAudioAPI(sandbox.GetState(), &audio); std::string script = R"lua( -- Test that audio global exists if not audio then error("audio global not found") end if not audio.play then error("audio.play not found") end if not audio.stopAll then error("audio.stopAll not found") end if not audio.getActiveCount then error("audio.getActiveCount not found") end -- Active count should be 0 initially if audio.getActiveCount() ~= 0 then error("should have no active sounds initially") end )lua"; bool ok = sandbox.LoadString(script, "audio_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } //============================================================================= // MAIN //============================================================================= int main(int argc, char* argv[]) { std::string filter; std::string output_file = "test_results.json"; // Parse args for (int i = 1; i < argc; i++) { std::string arg = argv[i]; if (arg == "--test" && i + 1 < argc) { filter = argv[++i]; } else if (arg == "--output" && i + 1 < argc) { output_file = argv[++i]; } else if (arg == "--help") { std::cout << "Usage: sandbox-test [options]\n"; std::cout << "Options:\n"; std::cout << " --test Run only tests containing \n"; std::cout << " --output Write JSON report to \n"; std::cout << " --help Show this help\n"; return 0; } } std::cout << "========================================\n"; std::cout << " LUA SANDBOX SECURITY TESTS\n"; std::cout << "========================================\n\n"; // Check scripts directory exists if (!std::filesystem::exists(GetScriptsDir())) { std::cerr << "ERROR: Scripts directory not found: " << GetScriptsDir() << "\n"; std::cerr << "Make sure to run from the build directory.\n"; return 1; } // Register tests TestHarness harness; // Milestone 1: Core Sandbox harness.AddTest("DangerousGlobalsRemoved", Test_DangerousGlobalsRemoved); harness.AddTest("BytecodeRejected", Test_BytecodeRejected); harness.AddTest("MemoryLimitEnforced", Test_MemoryLimitEnforced); harness.AddTest("CPULimitEnforced", Test_CPULimitEnforced); harness.AddTest("MetatableProtected", Test_MetatableProtected); harness.AddTest("SafeOperationsWork", Test_SafeOperationsWork); harness.AddTest("StringDumpRemoved", Test_StringDumpRemoved); harness.AddTest("MemoryTracking", Test_MemoryTracking); harness.AddTest("InstructionCounting", Test_InstructionCounting); harness.AddTest("MultipleLoads", Test_MultipleLoads); harness.AddTest("ErrorRecovery", Test_ErrorRecovery); // Milestone 2: Permission System harness.AddTest("NormalPermissionAutoGranted", Test_NormalPermissionAutoGranted); harness.AddTest("DangerousPermissionRequiresGrant", Test_DangerousPermissionRequiresGrant); harness.AddTest("SignaturePermissionSystemOnly", Test_SignaturePermissionSystemOnly); harness.AddTest("UserGestureTracking", Test_UserGestureTracking); harness.AddTest("UndeclaredPermissionDenied", Test_UndeclaredPermissionDenied); harness.AddTest("SystemAppGetsDangerousAuto", Test_SystemAppGetsDangerousAuto); harness.AddTest("PermissionCategoryCheck", Test_PermissionCategoryCheck); // Milestone 3: Audit Logging & Rate Limiting harness.AddTest("AuditLogBasic", Test_AuditLogBasic); harness.AddTest("AuditLogRingBuffer", Test_AuditLogRingBuffer); harness.AddTest("AuditLogThreadSafe", Test_AuditLogThreadSafe); harness.AddTest("RateLimiterBasic", Test_RateLimiterBasic); harness.AddTest("RateLimiterExhaustion", Test_RateLimiterExhaustion); harness.AddTest("RateLimiterRefill", Test_RateLimiterRefill); harness.AddTest("RateLimiterAppIsolation", Test_RateLimiterAppIsolation); harness.AddTest("RateLimiterReset", Test_RateLimiterReset); harness.AddTest("RateLimiterNoConfig", Test_RateLimiterNoConfig); // Milestone 4: Safe Path & Require harness.AddTest("PathRejectsTraversal", Test_PathRejectsTraversal); harness.AddTest("PathRejectsAbsolute", Test_PathRejectsAbsolute); harness.AddTest("PathAcceptsValid", Test_PathAcceptsValid); harness.AddTest("ModuleNameValidation", Test_ModuleNameValidation); harness.AddTest("ModuleToPath", Test_ModuleToPath); harness.AddTest("SafeRequireLoads", Test_SafeRequireLoads); harness.AddTest("SafeRequireCaches", Test_SafeRequireCaches); harness.AddTest("SafeRequireRejectsInvalid", Test_SafeRequireRejectsInvalid); // Milestone 5: Timer & Callback System harness.AddTest("SetTimeoutFires", Test_SetTimeoutFires); harness.AddTest("SetIntervalFires", Test_SetIntervalFires); harness.AddTest("ClearTimeoutCancels", Test_ClearTimeoutCancels); harness.AddTest("ClearIntervalCancels", Test_ClearIntervalCancels); harness.AddTest("TimerLimitEnforced", Test_TimerLimitEnforced); harness.AddTest("ClearAppTimersCleanup", Test_ClearAppTimersCleanup); harness.AddTest("MinIntervalEnforced", Test_MinIntervalEnforced); // Milestone 6: JSON & Crypto APIs harness.AddTest("JsonDecodeValid", Test_JsonDecodeValid); harness.AddTest("JsonDecodeRejectsDeep", Test_JsonDecodeRejectsDeep); harness.AddTest("JsonEncodeValid", Test_JsonEncodeValid); harness.AddTest("JsonEncodeDetectsCycles", Test_JsonEncodeDetectsCycles); harness.AddTest("JsonRejectsTooLarge", Test_JsonRejectsTooLarge); harness.AddTest("CryptoRandomBytes", Test_CryptoRandomBytes); harness.AddTest("CryptoHashSHA256", Test_CryptoHashSHA256); harness.AddTest("CryptoHMAC", Test_CryptoHMAC); harness.AddTest("SecureMathRandom", Test_SecureMathRandom); // Milestone 7: Virtual Filesystem harness.AddTest("VirtualFSReadWrite", Test_VirtualFSReadWrite); harness.AddTest("VirtualFSBlocksTraversal", Test_VirtualFSBlocksTraversal); harness.AddTest("VirtualFSEnforcesQuota", Test_VirtualFSEnforcesQuota); harness.AddTest("VirtualFSCleansUpTemp", Test_VirtualFSCleansUpTemp); harness.AddTest("VirtualFSList", Test_VirtualFSList); harness.AddTest("VirtualFSStat", Test_VirtualFSStat); harness.AddTest("VirtualFSLuaIntegration", Test_VirtualFSLuaIntegration); harness.AddTest("VirtualFSMaxFileSize", Test_VirtualFSMaxFileSize); // Milestone 8: SQLite Database harness.AddTest("DatabaseCreatesTables", Test_DatabaseCreatesTables); harness.AddTest("DatabasePreparedStatements", Test_DatabasePreparedStatements); harness.AddTest("DatabaseBlocksAttach", Test_DatabaseBlocksAttach); harness.AddTest("DatabaseBlocksDangerousPragma", Test_DatabaseBlocksDangerousPragma); harness.AddTest("DatabaseMultiple", Test_DatabaseMultiple); harness.AddTest("DatabaseLuaIntegration", Test_DatabaseLuaIntegration); harness.AddTest("DatabaseInvalidNames", Test_DatabaseInvalidNames); harness.AddTest("DatabaseLastInsertAndChanges", Test_DatabaseLastInsertAndChanges); // Milestone 9: Network HTTP harness.AddTest("NetworkBlocksPrivateIP", Test_NetworkBlocksPrivateIP); harness.AddTest("NetworkBlocksPlainHttp", Test_NetworkBlocksPlainHttp); harness.AddTest("NetworkRequiresHttps", Test_NetworkRequiresHttps); harness.AddTest("NetworkEnforcesDomainWhitelist", Test_NetworkEnforcesDomainWhitelist); harness.AddTest("NetworkUrlParsing", Test_NetworkUrlParsing); harness.AddTest("NetworkBlocksMetadata", Test_NetworkBlocksMetadata); harness.AddTest("NetworkRequestLimits", Test_NetworkRequestLimits); harness.AddTest("NetworkLuaIntegration", Test_NetworkLuaIntegration); // Milestone 10: WebSocket harness.AddTest("WebSocketUrlValidation", Test_WebSocketUrlValidation); harness.AddTest("WebSocketConnectionLimits", Test_WebSocketConnectionLimits); harness.AddTest("WebSocketBlocksPrivateIP", Test_WebSocketBlocksPrivateIP); harness.AddTest("WebSocketDomainWhitelist", Test_WebSocketDomainWhitelist); harness.AddTest("WebSocketMessageLimits", Test_WebSocketMessageLimits); harness.AddTest("WebSocketCloseAll", Test_WebSocketCloseAll); harness.AddTest("WebSocketLuaIntegration", Test_WebSocketLuaIntegration); // Milestone 11: Camera harness.AddTest("CameraRequiresPermission", Test_CameraRequiresPermission); harness.AddTest("CameraRequiresUserGesture", Test_CameraRequiresUserGesture); harness.AddTest("CameraShowsIndicator", Test_CameraShowsIndicator); harness.AddTest("CameraSingleSession", Test_CameraSingleSession); harness.AddTest("CameraStopsOnShutdown", Test_CameraStopsOnShutdown); harness.AddTest("CameraLuaIntegration", Test_CameraLuaIntegration); // Milestone 12: Microphone harness.AddTest("MicrophoneRequiresPermission", Test_MicrophoneRequiresPermission); harness.AddTest("MicrophoneRequiresUserGesture", Test_MicrophoneRequiresUserGesture); harness.AddTest("MicrophoneShowsIndicator", Test_MicrophoneShowsIndicator); harness.AddTest("MicrophoneSingleSession", Test_MicrophoneSingleSession); harness.AddTest("MicrophoneStopsOnShutdown", Test_MicrophoneStopsOnShutdown); harness.AddTest("MicrophoneLuaIntegration", Test_MicrophoneLuaIntegration); // Milestone 13: Audio Output harness.AddTest("AudioPlaysSound", Test_AudioPlaysSound); harness.AddTest("AudioRespectsSystemVolume", Test_AudioRespectsSystemVolume); harness.AddTest("AudioLimitsConcurrent", Test_AudioLimitsConcurrent); harness.AddTest("AudioStopAll", Test_AudioStopAll); harness.AddTest("AudioStopsOnShutdown", Test_AudioStopsOnShutdown); harness.AddTest("AudioLuaIntegration", Test_AudioLuaIntegration); // Run tests auto results = harness.Run(filter); // Output harness.PrintResults(results); harness.WriteJsonReport(results, output_file); std::cout << "\nJSON report written to: " << output_file << "\n"; // Return non-zero if any tests failed int failed = 0; for (const auto& r : results) { if (!r.passed) failed++; } return failed > 0 ? 1 : 0; }