2508 lines
80 KiB
C++
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;
|
|
}
|