implement Milestone 11: Camera interface with permission and user gesture requirements
This commit is contained in:
@@ -24,6 +24,7 @@ add_library(mosis-sandbox STATIC
|
||||
../src/main/cpp/sandbox/http_validator.cpp
|
||||
../src/main/cpp/sandbox/network_manager.cpp
|
||||
../src/main/cpp/sandbox/websocket_manager.cpp
|
||||
../src/main/cpp/sandbox/camera_interface.cpp
|
||||
)
|
||||
target_include_directories(mosis-sandbox PUBLIC
|
||||
../src/main/cpp/sandbox
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "http_validator.h"
|
||||
#include "network_manager.h"
|
||||
#include "websocket_manager.h"
|
||||
#include "camera_interface.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -1839,6 +1840,173 @@ bool Test_WebSocketLuaIntegration(std::string& error_msg) {
|
||||
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;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// MAIN
|
||||
//=============================================================================
|
||||
@@ -1980,6 +2148,14 @@ int main(int argc, char* argv[]) {
|
||||
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);
|
||||
|
||||
// Run tests
|
||||
auto results = harness.Run(filter);
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"name": "Lua Sandbox Security Tests",
|
||||
"summary": {
|
||||
"failed": 0,
|
||||
"passed": 42,
|
||||
"total": 42
|
||||
"passed": 82,
|
||||
"total": 82
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
@@ -77,7 +77,7 @@
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 111,
|
||||
"duration_ms": 106,
|
||||
"name": "UserGestureTracking",
|
||||
"status": "passed"
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 14,
|
||||
"duration_ms": 13,
|
||||
"name": "AuditLogThreadSafe",
|
||||
"status": "passed"
|
||||
},
|
||||
@@ -122,7 +122,7 @@
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 16,
|
||||
"duration_ms": 17,
|
||||
"name": "RateLimiterRefill",
|
||||
"status": "passed"
|
||||
},
|
||||
@@ -182,17 +182,17 @@
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 107,
|
||||
"duration_ms": 108,
|
||||
"name": "SetTimeoutFires",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 237,
|
||||
"duration_ms": 234,
|
||||
"name": "SetIntervalFires",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 155,
|
||||
"duration_ms": 158,
|
||||
"name": "ClearTimeoutCancels",
|
||||
"status": "passed"
|
||||
},
|
||||
@@ -212,10 +212,210 @@
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 62,
|
||||
"duration_ms": 63,
|
||||
"name": "MinIntervalEnforced",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "JsonDecodeValid",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "JsonDecodeRejectsDeep",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "JsonEncodeValid",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "JsonEncodeDetectsCycles",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "JsonRejectsTooLarge",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "CryptoRandomBytes",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "CryptoHashSHA256",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "CryptoHMAC",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "SecureMathRandom",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 1,
|
||||
"name": "VirtualFSReadWrite",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "VirtualFSBlocksTraversal",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "VirtualFSEnforcesQuota",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "VirtualFSCleansUpTemp",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 1,
|
||||
"name": "VirtualFSList",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 4,
|
||||
"name": "VirtualFSStat",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 1,
|
||||
"name": "VirtualFSLuaIntegration",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 1,
|
||||
"name": "VirtualFSMaxFileSize",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 16,
|
||||
"name": "DatabaseCreatesTables",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 13,
|
||||
"name": "DatabasePreparedStatements",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 1,
|
||||
"name": "DatabaseBlocksAttach",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 1,
|
||||
"name": "DatabaseBlocksDangerousPragma",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 16,
|
||||
"name": "DatabaseMultiple",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "DatabaseLuaIntegration",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "DatabaseInvalidNames",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 25,
|
||||
"name": "DatabaseLastInsertAndChanges",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkBlocksPrivateIP",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkBlocksPlainHttp",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkRequiresHttps",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkEnforcesDomainWhitelist",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkUrlParsing",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkBlocksMetadata",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkRequestLimits",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "NetworkLuaIntegration",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketUrlValidation",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketConnectionLimits",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketBlocksPrivateIP",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketDomainWhitelist",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketMessageLimits",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketCloseAll",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"duration_ms": 0,
|
||||
"name": "WebSocketLuaIntegration",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-01-18T13:19:38Z"
|
||||
"timestamp": "2026-01-18T14:29:44Z"
|
||||
}
|
||||
Reference in New Issue
Block a user