Files
MosisService/sandbox-test/src/main.cpp

2508 lines
80 KiB
C++

#include "test_harness.h"
#include "lua_sandbox.h"
#include "permission_gate.h"
#include <lua.hpp>
#include <iostream>
#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 <filesystem>
#include <fstream>
#include <sstream>
#include <thread>
#include <chrono>
// 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<std::thread> 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<std::string> 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<std::string> 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<uint8_t>(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<uint8_t>(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<uint8_t>(1000);
mosis::PlaybackConfig config;
// Create MAX_CONCURRENT_SOUNDS players
std::vector<std::shared_ptr<mosis::SoundPlayer>> 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<uint8_t>(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<uint8_t>(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 <name> Run only tests containing <name>\n";
std::cout << " --output <file> Write JSON report to <file>\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;
}