catalyst-core 0.3.0-beta.5 → 0.3.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +185 -71
- package/dist/errors/generateDocs.js +100 -0
- package/dist/errors/index.js +243 -0
- package/dist/errors/registry.js +483 -0
- package/dist/errors-index.json +506 -0
- package/dist/globals.d.ts +62 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.jsx +5 -8
- package/dist/logger.js +1 -1
- package/dist/native/androidProject/app/build.gradle.kts +142 -7
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +4 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +2 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +3 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +3 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +43 -8
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +181 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +156 -236
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileSizeRouterUtilsTest.kt +471 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +222 -226
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FrameworkServerUtilsTest.kt +300 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/IntentUtilsTest.kt +244 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/MetricsMonitorTest.kt +359 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +108 -63
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +329 -385
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/OfflineCacheServiceTest.kt +302 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/PerfEventBufferTest.kt +133 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/UtilsKtTest.kt +152 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +145 -324
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/BarcodeDetectorTest.kt +132 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/HoldControllerTest.kt +155 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/TorchControllerTest.kt +150 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/VideoStreamStateMachineTest.kt +120 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/VideoStreamStateTest.kt +62 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/ViewfinderMapperTest.kt +143 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/ZoomControllerTest.kt +421 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/design/DesignTokensTest.kt +207 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +368 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/FridaDetectorTest.kt +193 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/SecurityCheckManagerTest.kt +260 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/SecurityCheckSchedulerTest.kt +166 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/DeviceInfoUtilsTest.kt +167 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/DownloadUtilsTest.kt +197 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/NetworkMonitorTest.kt +271 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/PushNotificationUtilsTest.kt +119 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/SafeAreaUtilsTest.kt +108 -0
- package/dist/native/androidProject/build.gradle.kts +1 -0
- package/dist/native/androidProject/gradle/libs.versions.toml +26 -0
- package/dist/native/androidProject/settings.gradle.kts +11 -8
- package/dist/native/androidSetup.js +2 -2
- package/dist/native/bridge/WebBridge.d.ts +98 -0
- package/dist/native/bridge/WebBridge.js +40 -6
- package/dist/native/bridge/constants/NativeInterfaces.d.ts +391 -0
- package/dist/native/bridge/errors.d.ts +67 -0
- package/dist/native/bridge/errors.js +66 -151
- package/dist/native/bridge/hooks/index.d.ts +13 -0
- package/dist/native/bridge/hooks/index.js +0 -6
- package/dist/native/bridge/hooks/useAI.d.ts +1 -0
- package/dist/native/bridge/hooks/useAI.js +3 -1
- package/dist/native/bridge/hooks/useCamera.d.ts +86 -0
- package/dist/native/bridge/hooks/useCamera.js +35 -6
- package/dist/native/bridge/hooks/useDataProtection.d.ts +17 -0
- package/dist/native/bridge/hooks/useDeviceInfo.d.ts +34 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +5 -0
- package/dist/native/bridge/hooks/useFilePicker.d.ts +53 -0
- package/dist/native/bridge/hooks/useFilePicker.js +13 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.d.ts +12 -0
- package/dist/native/bridge/hooks/useHapticFeedback.d.ts +76 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +6 -1
- package/dist/native/bridge/hooks/useIntent.d.ts +33 -0
- package/dist/native/bridge/hooks/useIntent.js +2 -0
- package/dist/native/bridge/hooks/useNativeTransition.d.ts +53 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +12 -5
- package/dist/native/bridge/hooks/useNetworkStatus.d.ts +22 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +7 -0
- package/dist/native/bridge/hooks/useNotification.d.ts +83 -0
- package/dist/native/bridge/hooks/useNotification.js +81 -34
- package/dist/native/bridge/hooks/useSafeArea.d.ts +12 -0
- package/dist/native/bridge/hooks/useSafeArea.js +8 -0
- package/dist/native/bridge/hooks/useVideoStream.d.ts +67 -0
- package/dist/native/bridge/hooks/useVideoStream.js +38 -52
- package/dist/native/bridge/hooks/utils.d.ts +4 -0
- package/dist/native/bridge/hooks.d.ts +1 -0
- package/dist/native/bridge/perf/collectors/ApiCollector.d.ts +30 -0
- package/dist/native/bridge/perf/collectors/ApiCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.d.ts +43 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/HookCollector.d.ts +37 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.d.ts +103 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.d.ts +53 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.d.ts +30 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.d.ts +37 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.d.ts +30 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.d.ts +47 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +8 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.d.ts +29 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +4 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.d.ts +31 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +4 -0
- package/dist/native/bridge/perf/core/constants.d.ts +76 -0
- package/dist/native/bridge/perf/core/measure.d.ts +39 -0
- package/dist/native/bridge/perf/core/measure.js +4 -0
- package/dist/native/bridge/perf/core/session.d.ts +49 -0
- package/dist/native/bridge/perf/core/session.js +4 -0
- package/dist/native/bridge/perf/core/store.d.ts +37 -0
- package/dist/native/bridge/perf/core/store.js +4 -0
- package/dist/native/bridge/perf/core/summary.d.ts +60 -0
- package/dist/native/bridge/perf/core/traceExport.d.ts +10 -0
- package/dist/native/bridge/perf/core/waterfalls.d.ts +2 -0
- package/dist/native/bridge/perf/index.d.ts +63 -0
- package/dist/native/bridge/perf/index.js +4 -0
- package/dist/native/bridge/safeArea.d.ts +11 -0
- package/dist/native/bridge/safeArea.js +2 -0
- package/dist/native/bridge/useBaseHook.d.ts +137 -0
- package/dist/native/bridge/useBaseHook.js +34 -4
- package/dist/native/bridge/utils/CameraUtils.d.ts +94 -0
- package/dist/native/bridge/utils/CameraUtils.js +4 -0
- package/dist/native/bridge/utils/FileObjectConverter.d.ts +36 -0
- package/dist/native/bridge/utils/NativeBridge.d.ts +270 -0
- package/dist/native/bridge/utils/NativeBridge.js +10 -0
- package/dist/native/buildAndroid/index.js +1 -1
- package/dist/native/buildAppAndroid.js +9 -0
- package/dist/native/buildAppIos.js +16 -1
- package/dist/native/buildErrorFormat.js +57 -0
- package/dist/native/buildIos/config.js +15 -1
- package/dist/native/buildIos/index.js +1 -1
- package/dist/native/buildIos/plugins.js +10 -1
- package/dist/native/iosnativeWebView/CoreLogic/Package.swift +36 -0
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Camera/VideoStreamState.swift +1 -1
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Camera/VideoStreamStateMachine.swift +6 -4
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/BootTimingUtility.swift +3 -3
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/FrameworkServerUtils.swift +14 -9
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NetworkMonitor.swift +11 -9
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NotificationHandlerProvider.swift +1 -1
- package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/URLWhitelistManager.swift +7 -7
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/AnyCodableTests.swift +154 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/BootTimingUtilityTests.swift +23 -5
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/CatalystConstantsTests.swift +192 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/ConfigMappingTests.swift +1 -1
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/FrameworkServerUtilsErrorPathTests.swift +106 -0
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/FrameworkServerUtilsLoopbackTests.swift +243 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/FrameworkServerUtilsTests.swift +1 -1
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/NetworkMonitorTests.swift +119 -0
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/NotificationHandlerProviderTests.swift +59 -0
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/NotificationHandlerTests.swift +1 -1
- package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/URLWhitelistManagerTests.swift +1 -1
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/VideoStreamStateMachineTests.swift +171 -0
- package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/VideoStreamStateTests.swift +91 -0
- package/dist/native/iosnativeWebView/Package.swift +10 -9
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +7 -2
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -2
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +5 -4
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +6 -5
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +31 -30
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +22 -19
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ImageHandler.swift +40 -39
- package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +15 -15
- package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +4 -3
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +6 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +3 -5
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +20 -13
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +6 -5
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +14 -13
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +3 -3
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +9 -8
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +7 -6
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +24 -25
- package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +6 -1
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +4 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +2 -22
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +1 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +2 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +15 -9
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +1 -0
- package/dist/native/setupEmulatorIos.js +2 -2
- package/dist/native/terminalProgress.js +17 -17
- package/dist/router/ClientRouter.d.ts +8 -0
- package/dist/router/ClientRouter.js +13 -11
- package/dist/router/ServerRouter.d.ts +13 -0
- package/dist/router/ServerRouter.js +19 -7
- package/dist/scripts/build.js +14 -2
- package/dist/scripts/loadEnvironmentVariables.js +3 -2
- package/dist/scripts/plugins.js +1 -1
- package/dist/scripts/preServerInit.js +49 -3
- package/dist/scripts/preflight.js +111 -0
- package/dist/scripts/scriptUtils.js +46 -1
- package/dist/scripts/serve.js +7 -3
- package/dist/scripts/start.js +8 -3
- package/dist/server/expressServer.d.ts +1 -0
- package/dist/server/expressServer.js +247 -271
- package/dist/server/manifestCache.d.ts +2 -0
- package/dist/server/manifestCache.js +25 -33
- package/dist/server/renderer/ChunkExtractor.d.ts +44 -0
- package/dist/server/renderer/ChunkExtractor.js +113 -125
- package/dist/server/renderer/document/Body.d.ts +21 -0
- package/dist/server/renderer/document/Body.jsx +45 -45
- package/dist/server/renderer/document/FastRefresh.d.ts +3 -0
- package/dist/server/renderer/document/FastRefresh.jsx +7 -13
- package/dist/server/renderer/document/Head.d.ts +20 -0
- package/dist/server/renderer/document/Head.jsx +32 -32
- package/dist/server/renderer/document/index.d.ts +2 -0
- package/dist/server/renderer/document/index.jsx +2 -2
- package/dist/server/renderer/extract.d.ts +67 -0
- package/dist/server/renderer/extract.js +92 -89
- package/dist/server/renderer/handler.d.ts +2 -0
- package/dist/server/renderer/handler.jsx +401 -404
- package/dist/server/renderer/index.d.ts +2 -0
- package/dist/server/renderer/index.js +5 -8
- package/dist/server/renderer/render.d.ts +35 -0
- package/dist/server/renderer/render.js +25 -32
- package/dist/server/utils/botDetectionMiddleware.d.ts +1 -0
- package/dist/server/utils/botDetectionMiddleware.js +33 -23
- package/dist/server/utils/cjsRequire.d.ts +8 -0
- package/dist/server/utils/cjsRequire.js +2 -2
- package/dist/server/utils/userAgentUtil.d.ts +7 -0
- package/dist/server/utils/userAgentUtil.js +68 -71
- package/dist/server/utils/validator.d.ts +31 -0
- package/dist/server/utils/validator.js +133 -158
- package/dist/vite/inject-cache-key-plugin.js +3 -27
- package/dist/vite/moduleAliases.js +36 -0
- package/dist/vite/node-loader.mjs +2 -29
- package/dist/vite/resolveDevServerConfig.js +4 -1
- package/dist/vite/vite.config.js +2 -29
- package/dist/web-router/components/MetaTag.d.ts +4 -0
- package/dist/web-router/components/MetaTag.jsx +26 -28
- package/dist/web-router/components/MetaTag.test.jsx +100 -0
- package/dist/web-router/components/RouterDataProvider.d.ts +96 -0
- package/dist/web-router/components/RouterDataProvider.jsx +260 -259
- package/dist/web-router/components/RouterDataProvider.server.test.jsx +98 -0
- package/dist/web-router/components/RouterDataProvider.test.jsx +310 -0
- package/dist/web-router/components/Split.d.ts +74 -0
- package/dist/web-router/components/Split.jsx +147 -156
- package/dist/web-router/components/Split.server.test.jsx +52 -0
- package/dist/web-router/components/Split.test.jsx +167 -0
- package/dist/web-router/components/SplitInview.d.ts +22 -0
- package/dist/web-router/components/SplitInview.jsx +78 -89
- package/dist/web-router/components/SplitInview.test.jsx +112 -0
- package/dist/web-router/components/SsrRequestContext.d.ts +20 -0
- package/dist/web-router/components/SsrRequestContext.jsx +17 -7
- package/dist/web-router/components/SsrRequestContext.test.jsx +53 -0
- package/dist/web-router/context.d.ts +15 -0
- package/dist/web-router/context.jsx +5 -5
- package/dist/web-router/context.test.jsx +10 -0
- package/dist/web-router/utils/metaDataUtils.d.ts +24 -0
- package/dist/web-router/utils/metaDataUtils.jsx +86 -86
- package/dist/web-router/utils/metaDataUtils.test.jsx +188 -0
- package/dist/web-router/vitest.setup.js +12 -0
- package/mcp_v2/knowledge-base.json +115 -17
- package/mcp_v2/mcp.js +141 -3
- package/mcp_v2/package-lock.json +2031 -0
- package/mcp_v2/package.json +13 -2
- package/mcp_v2/scripts/copyErrorsIndex.cjs +34 -0
- package/mcp_v2/test/errors.test.ts +123 -0
- package/mcp_v2/test/github.test.ts +177 -0
- package/mcp_v2/tools/errors.d.ts +20 -0
- package/mcp_v2/tools/errors.js +63 -0
- package/mcp_v2/tools/github.d.ts +41 -0
- package/mcp_v2/tools/github.js +231 -0
- package/mcp_v2/vitest.config.mjs +22 -0
- package/package.json +60 -11
- package/test/errors/docsDrift.test.ts +113 -0
- package/test/errors/index.test.ts +200 -0
- package/test/errors/registry.test.ts +108 -0
- package/test/scripts/preflight.test.ts +118 -0
- package/test/server/ChunkExtractor.test.ts +97 -0
- package/test/server/extract.test.ts +128 -0
- package/test/server/fixtures/template/server/document.jsx +19 -0
- package/test/server/fixtures/template/server/index.js +10 -0
- package/test/server/fixtures/template/src/js/containers/App/index.jsx +33 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +72 -0
- package/test/server/fixtures/template/src/js/routes/utils.jsx +39 -0
- package/test/server/fixtures/template/src/js/store/index.js +19 -0
- package/test/server/handler.assets.test.ts +330 -0
- package/test/server/handler.test.ts +208 -0
- package/test/server/manifestCache.test.ts +71 -0
- package/test/server/render.test.ts +71 -0
- package/test/server/scriptUtils.test.ts +103 -0
- package/test/server/userAgentUtil.test.ts +48 -0
- package/test/server/validator.test.ts +225 -0
- package/test/vite/resolveDevServerConfig.test.js +4 -1
- package/tsconfig.test.json +23 -0
- package/vitest.config.ts +93 -0
- package/dist/caching.js +0 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +0 -187
- package/dist/native/bridge/standardizedHooks.js +0 -611
- package/dist/router.js +0 -1
- package/dist/scripts/validator.js +0 -149
- package/dist/web-router/hooks.jsx +0 -72
- package/dist/web-router/types/components/MetaTag.d.ts +0 -2
- package/dist/web-router/types/components/RouterDataProvider.d.ts +0 -95
- package/dist/web-router/types/context.d.ts +0 -14
- package/dist/web-router/types/hooks.d.ts +0 -1
- package/dist/web-router/types/index.d.ts +0 -3
- package/dist/web-router/types/utils/metaDataUtils.d.ts +0 -40
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Constants/CatalystConstants.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Constants/ConfigConstants.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Notifications/NotificationModels.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Protocols/NotificationHandlerProtocol.swift +0 -0
- /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NullNotificationHandler.swift +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
package io.yourname.androidproject.security
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
import java.io.File
|
|
7
|
+
import java.net.InetSocketAddress
|
|
8
|
+
import java.net.ServerSocket
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unit tests for FridaDetector (Android coverage batch 3), previously
|
|
12
|
+
* entirely untested (0/~96 lines).
|
|
13
|
+
*
|
|
14
|
+
* FridaDetector is a pure-JVM `object` for its core isFridaDetected() path
|
|
15
|
+
* -- java.io.File existence checks, a real SocketChannel connect against
|
|
16
|
+
* Frida's default ports, /proc/self/maps line scanning, and
|
|
17
|
+
* Class.forName() lookups. None of that needs Context or a View, so this
|
|
18
|
+
* suite exercises it directly with no Mockito mocking of Android types at
|
|
19
|
+
* all:
|
|
20
|
+
*
|
|
21
|
+
* - checkFridaPorts: verified empirically (see task history) that binding
|
|
22
|
+
* a real java.net.ServerSocket on 127.0.0.1:27042 makes
|
|
23
|
+
* isFridaDetected() return true via the real non-blocking
|
|
24
|
+
* SocketChannel.connect()/finishConnect() loop; no listener there means
|
|
25
|
+
* the connect fails/times out and the check contributes false. This is
|
|
26
|
+
* more reliable than trying to mock java.nio.channels.SocketChannel
|
|
27
|
+
* (a concrete JDK class, not a mockable Android stub).
|
|
28
|
+
* - checkFridaFiles / checkHookingFrameworks' XposedBridge.jar file check:
|
|
29
|
+
* these check hardcoded absolute paths (e.g. /data/local/tmp/
|
|
30
|
+
* frida-server, /system/framework/XposedBridge.jar) that do not exist on
|
|
31
|
+
* a dev/CI JVM test runner, so they deterministically contribute false
|
|
32
|
+
* here. This suite does not attempt to fake those paths into existing
|
|
33
|
+
* (would require filesystem root manipulation out of scope for a unit
|
|
34
|
+
* test) -- the false branch is exercised implicitly by every test that
|
|
35
|
+
* doesn't plant a listening socket.
|
|
36
|
+
* - checkLoadedLibraries reads the real /proc/self/maps, which does not
|
|
37
|
+
* exist on macOS (only Linux) -- mapsFile.exists() is false there, so
|
|
38
|
+
* this check deterministically contributes false regardless of
|
|
39
|
+
* platform. On a Linux CI runner it would read the actual test JVM's
|
|
40
|
+
* real maps, which won't contain Frida/Xposed library names in a normal
|
|
41
|
+
* run either. Not independently forced true in this suite (would
|
|
42
|
+
* require a fake /proc/self/maps, not feasible without a mockable JDK
|
|
43
|
+
* File).
|
|
44
|
+
* - checkHookingFrameworks' Class.forName() checks (XposedBridge,
|
|
45
|
+
* substrate.MS) genuinely throw ClassNotFoundException on the plain JVM
|
|
46
|
+
* test classpath, exercising their catch-and-return-false branches for
|
|
47
|
+
* real -- no mocking needed.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately OUT OF SCOPE: getFridaCheckDetails() as a whole. It builds
|
|
50
|
+
* its return value as a single `mapOf(...)` literal, so the "isDebuggable"
|
|
51
|
+
* entry's Build.FINGERPRINT/Build.TAGS access (static final fields under
|
|
52
|
+
* the mockable android.jar, NOT interceptable by Mockito) is evaluated
|
|
53
|
+
* eagerly and unconditionally -- confirmed empirically to throw an
|
|
54
|
+
* uncaught NullPointerException before the function can even return.
|
|
55
|
+
* There is no reachable subset of that function's behavior to test
|
|
56
|
+
* without touching those two fields, so it is skipped entirely (see the
|
|
57
|
+
* dedicated comment above its section for detail) rather than chased for
|
|
58
|
+
* partial coverage.
|
|
59
|
+
*/
|
|
60
|
+
class FridaDetectorTest {
|
|
61
|
+
|
|
62
|
+
// ============================================================
|
|
63
|
+
// isFridaDetected -- no hooking indicators present (baseline)
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `isFridaDetected returns false in a clean test environment with no listener and no suspicious files`() {
|
|
68
|
+
// Unlike the positive tests below (which need a specific port
|
|
69
|
+
// bound and should fail loudly if that's not possible), this
|
|
70
|
+
// negative assertion's only real dependency is that NOTHING is
|
|
71
|
+
// listening on Frida's ports right now -- if something is (even,
|
|
72
|
+
// ironically, a real frida-server on the host), that's an
|
|
73
|
+
// environment fact this test can't control, so skip rather than
|
|
74
|
+
// fail. checkPortFree(...) reports that state without binding
|
|
75
|
+
// anything itself, avoiding a bind-vs-connect race with the
|
|
76
|
+
// detector's own check.
|
|
77
|
+
org.junit.Assume.assumeTrue(
|
|
78
|
+
"Something is already listening on port 27042 or 27043 in this environment — skipping",
|
|
79
|
+
checkPortFree(27042) && checkPortFree(27043)
|
|
80
|
+
)
|
|
81
|
+
assertFalse(FridaDetector.isFridaDetected())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================
|
|
85
|
+
// isFridaDetected -- Frida port check forced positive
|
|
86
|
+
// ============================================================
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
fun `isFridaDetected returns true when a listener is bound on Frida's default port 27042`() {
|
|
90
|
+
val server = bindOrFailLoudly(27042)
|
|
91
|
+
try {
|
|
92
|
+
assertTrue(FridaDetector.isFridaDetected())
|
|
93
|
+
} finally {
|
|
94
|
+
server.close()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun `isFridaDetected returns true when a listener is bound on Frida's alternate port 27043`() {
|
|
100
|
+
val server = bindOrFailLoudly(27043)
|
|
101
|
+
try {
|
|
102
|
+
assertTrue(FridaDetector.isFridaDetected())
|
|
103
|
+
} finally {
|
|
104
|
+
server.close()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================
|
|
109
|
+
// checkFridaFiles indirectly, via isFridaDetected -- files absent
|
|
110
|
+
// ============================================================
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun `isFridaDetected does not false-positive on unrelated existing files`() {
|
|
114
|
+
// Sanity check that checkFridaFiles is checking specific hardcoded
|
|
115
|
+
// paths, not "any file exists somewhere" -- a real file existing
|
|
116
|
+
// elsewhere on disk must not trip detection.
|
|
117
|
+
val unrelated = File.createTempFile("not-frida", ".tmp")
|
|
118
|
+
try {
|
|
119
|
+
assertFalse(FridaDetector.isFridaDetected())
|
|
120
|
+
} finally {
|
|
121
|
+
unrelated.delete()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================
|
|
126
|
+
// getFridaCheckDetails -- NOT tested, entirely out of scope.
|
|
127
|
+
//
|
|
128
|
+
// Empirically confirmed (java.lang.NullPointerException at the
|
|
129
|
+
// Build.FINGERPRINT.lowercase() call): the function builds its
|
|
130
|
+
// return value as a single Kotlin `mapOf(...)` literal, which
|
|
131
|
+
// eagerly evaluates every entry -- including the "isDebuggable"
|
|
132
|
+
// entry's inline `Build.FINGERPRINT.lowercase().contains(...) ||
|
|
133
|
+
// Build.TAGS.lowercase().contains(...)` expression -- before the
|
|
134
|
+
// map is ever constructed. Build.FINGERPRINT/Build.TAGS are null
|
|
135
|
+
// under the mockable android.jar (isReturnDefaultValues=true), so
|
|
136
|
+
// calling getFridaCheckDetails() at all throws uncaught, with no
|
|
137
|
+
// try/catch anywhere in the function to absorb it. Unlike
|
|
138
|
+
// EmulatorDetector.getEmulatorCheckDetails or FridaDetector's own
|
|
139
|
+
// isFridaDetected() (which wraps checks in try/catch and degrades to
|
|
140
|
+
// false), there is no reachable code path here that avoids touching
|
|
141
|
+
// those two fields. Per the task's Build.* limitation, this function
|
|
142
|
+
// is skipped entirely rather than partially asserted on.
|
|
143
|
+
// ============================================================
|
|
144
|
+
// helpers
|
|
145
|
+
// ============================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Binds a real listening socket on the given loopback port. Frida's
|
|
149
|
+
* default ports (27042/27043) are vanishingly unlikely to already be
|
|
150
|
+
* occupied on a dev/CI machine, but fail loudly with a clear message
|
|
151
|
+
* rather than producing a mystery assertion failure if they are.
|
|
152
|
+
*
|
|
153
|
+
* Named to reflect what it actually does (previously `bindOrSkip`,
|
|
154
|
+
* which never skipped anything -- CodeRabbit flagged the mismatch).
|
|
155
|
+
* These positive-detection tests intentionally do NOT tolerate a
|
|
156
|
+
* bind failure the way the negative test above does: if the port is
|
|
157
|
+
* already occupied, that's worth knowing loudly rather than quietly
|
|
158
|
+
* skipping a test meant to prove isFridaDetected() actually detects
|
|
159
|
+
* something.
|
|
160
|
+
*/
|
|
161
|
+
private fun bindOrFailLoudly(port: Int): ServerSocket {
|
|
162
|
+
return try {
|
|
163
|
+
ServerSocket().apply {
|
|
164
|
+
reuseAddress = true
|
|
165
|
+
bind(InetSocketAddress("127.0.0.1", port))
|
|
166
|
+
}
|
|
167
|
+
} catch (e: Exception) {
|
|
168
|
+
throw IllegalStateException(
|
|
169
|
+
"Could not bind port $port for FridaDetector test -- is something " +
|
|
170
|
+
"already listening on it (ironically, maybe even a real frida-server)?",
|
|
171
|
+
e
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reports whether a loopback port currently has nothing listening on
|
|
178
|
+
* it, without holding a bind itself (so it can't race the detector's
|
|
179
|
+
* own connect attempt in isFridaDetected()). Used only to decide
|
|
180
|
+
* whether the clean-environment negative test should run at all.
|
|
181
|
+
*/
|
|
182
|
+
private fun checkPortFree(port: Int): Boolean {
|
|
183
|
+
return try {
|
|
184
|
+
ServerSocket().use {
|
|
185
|
+
it.reuseAddress = true
|
|
186
|
+
it.bind(InetSocketAddress("127.0.0.1", port))
|
|
187
|
+
}
|
|
188
|
+
true
|
|
189
|
+
} catch (e: Exception) {
|
|
190
|
+
false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
package io.yourname.androidproject.security
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import kotlinx.coroutines.test.runTest
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import org.junit.After
|
|
8
|
+
import org.junit.Assert.assertEquals
|
|
9
|
+
import org.junit.Assert.assertFalse
|
|
10
|
+
import org.junit.Assert.assertNull
|
|
11
|
+
import org.junit.Assert.assertTrue
|
|
12
|
+
import org.junit.Before
|
|
13
|
+
import org.junit.Test
|
|
14
|
+
import org.mockito.kotlin.any
|
|
15
|
+
import org.mockito.kotlin.doAnswer
|
|
16
|
+
import org.mockito.kotlin.doReturn
|
|
17
|
+
import org.mockito.kotlin.mock
|
|
18
|
+
import java.io.ByteArrayInputStream
|
|
19
|
+
import java.net.InetSocketAddress
|
|
20
|
+
import java.net.ServerSocket
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unit tests for SecurityCheckManager (Android coverage batch 3),
|
|
24
|
+
* previously entirely untested (0/~67 lines).
|
|
25
|
+
*
|
|
26
|
+
* SecurityCheckManager is an `object` that only needs a Context (for
|
|
27
|
+
* `RootBeer(context)` and `context.getSharedPreferences(...)`) -- no
|
|
28
|
+
* Views. It orchestrates EmulatorDetector / FridaDetector / RootBeer via
|
|
29
|
+
* coroutines (Dispatchers.Default, real -- not Dispatchers.Main, so
|
|
30
|
+
* kotlinx-coroutines-test's virtual-time scheduler does not intercept it;
|
|
31
|
+
* runTest{} is used only to satisfy the `suspend` signature, matching this
|
|
32
|
+
* project's established pattern for suspend functions under test, e.g.
|
|
33
|
+
* WebCacheManagerTest).
|
|
34
|
+
*
|
|
35
|
+
* Key empirically-verified facts this suite relies on (see task history):
|
|
36
|
+
* - `RootBeer(context).isRooted` throws an uncaught NullPointerException
|
|
37
|
+
* against a plain Mockito Context mock (PackageManager is null under
|
|
38
|
+
* the mockable jar) -- but checkRooted() wraps the call in try/catch,
|
|
39
|
+
* so in every test here isRooted resolves to false. This suite does NOT
|
|
40
|
+
* attempt to force isRooted=true: RootBeer is constructed directly
|
|
41
|
+
* inside checkRooted() (not injected), and RootBeer is a real
|
|
42
|
+
* non-Android library class with no Context-driven seam to intercept
|
|
43
|
+
* its result -- forcing a rooted result would require Mockito's
|
|
44
|
+
* inline mockConstruction, which is unverified to fire reliably across
|
|
45
|
+
* the coroutine worker thread checkRooted() actually runs on
|
|
46
|
+
* (Dispatchers.Default), so it is deliberately not attempted.
|
|
47
|
+
* - EmulatorDetector.isEmulator(mockContext) returns false cleanly (its
|
|
48
|
+
* own outer try/catch absorbs the Build.FINGERPRINT-driven NPE path;
|
|
49
|
+
* see FridaDetectorTest and EmulatorDetector's own known Build.* limits)
|
|
50
|
+
* -- so isEmulator is always false in this suite. Forcing it true would
|
|
51
|
+
* require mocking Build.* static final fields, which Mockito cannot do
|
|
52
|
+
* (see project-wide rule).
|
|
53
|
+
* - FridaDetector.isFridaDetected() CAN be deterministically forced true
|
|
54
|
+
* by binding a real listening socket on Frida's default port 127.0.0.1:
|
|
55
|
+
* 27042 (see FridaDetectorTest) -- this is therefore the only one of
|
|
56
|
+
* the three local checks this suite drives to the BLOCK-triggering
|
|
57
|
+
* branch, and it is enough to exercise isCompromised=true /
|
|
58
|
+
* recommendation="BLOCK" end-to-end through performSecurityChecks
|
|
59
|
+
* without touching Build.* or RootBeer at all.
|
|
60
|
+
*
|
|
61
|
+
* Covered: performSecurityChecks' ALLOW path (nothing detected) and BLOCK
|
|
62
|
+
* path (via the Frida-socket trick), its JSONObject field shape,
|
|
63
|
+
* saveLatestResults/getLatestSecurityResults round-trip through a mocked
|
|
64
|
+
* SharedPreferences, calculateRecommendation's branches (exercised
|
|
65
|
+
* indirectly through performSecurityChecks, since it's private), and
|
|
66
|
+
* getSecurityMode's present/default/malformed-config branches via a mocked
|
|
67
|
+
* AssetManager.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately OUT OF SCOPE: createErrorResponse() is only reachable when
|
|
70
|
+
* the try block in performSecurityChecks throws -- every checked call
|
|
71
|
+
* inside it (checkRooted/checkEmulator/checkFridaDetected) has its own
|
|
72
|
+
* internal try/catch that swallows exceptions and returns false, and
|
|
73
|
+
* saveLatestResults also catches internally, so there is no reachable
|
|
74
|
+
* failure path to trigger it from the public API surface without
|
|
75
|
+
* reflection or bytecode manipulation. Not attempted. All of the
|
|
76
|
+
* commented-out Play Integrity code (getGoogleToken,
|
|
77
|
+
* performPlayIntegrityCheck, cachePlayIntegrityResult, etc.) is inactive
|
|
78
|
+
* source (commented out) and not compiled, so there is nothing to test.
|
|
79
|
+
*/
|
|
80
|
+
class SecurityCheckManagerTest {
|
|
81
|
+
|
|
82
|
+
private lateinit var prefsStorage: MutableMap<String, String?>
|
|
83
|
+
private lateinit var sharedPreferences: SharedPreferences
|
|
84
|
+
private lateinit var editor: SharedPreferences.Editor
|
|
85
|
+
private lateinit var context: Context
|
|
86
|
+
|
|
87
|
+
@Before
|
|
88
|
+
fun setUp() {
|
|
89
|
+
prefsStorage = mutableMapOf()
|
|
90
|
+
|
|
91
|
+
editor = mock {
|
|
92
|
+
on { putString(any(), any()) } doAnswer { invocation ->
|
|
93
|
+
val key = invocation.getArgument<String>(0)
|
|
94
|
+
val value = invocation.getArgument<String?>(1)
|
|
95
|
+
prefsStorage[key] = value
|
|
96
|
+
editor
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// apply() is Unit-returning; default mock behavior (no-op) is fine,
|
|
100
|
+
// but we still want the stored value visible without invoking a
|
|
101
|
+
// real Android disk write.
|
|
102
|
+
org.mockito.kotlin.doNothing().`when`(editor).apply()
|
|
103
|
+
|
|
104
|
+
sharedPreferences = mock {
|
|
105
|
+
on { getString(any(), org.mockito.kotlin.anyOrNull()) } doAnswer { invocation ->
|
|
106
|
+
val key = invocation.getArgument<String>(0)
|
|
107
|
+
val default = invocation.getArgument<String?>(1)
|
|
108
|
+
prefsStorage[key] ?: default
|
|
109
|
+
}
|
|
110
|
+
on { edit() } doReturn editor
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
context = mock {
|
|
114
|
+
on { getSharedPreferences(any(), any()) } doReturn sharedPreferences
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@After
|
|
119
|
+
fun tearDown() {
|
|
120
|
+
prefsStorage.clear()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================
|
|
124
|
+
// performSecurityChecks -- ALLOW path (clean environment)
|
|
125
|
+
// ============================================================
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun `performSecurityChecks returns ALLOW and not compromised when no local threat is detected`() = runTest {
|
|
129
|
+
val results = SecurityCheckManager.performSecurityChecks(context)
|
|
130
|
+
|
|
131
|
+
assertFalse(results.getBoolean("isRooted"))
|
|
132
|
+
assertFalse(results.getBoolean("isEmulator"))
|
|
133
|
+
assertFalse(results.getBoolean("isFridaDetected"))
|
|
134
|
+
assertFalse(results.getBoolean("isCompromised"))
|
|
135
|
+
assertEquals("ALLOW", results.getString("recommendation"))
|
|
136
|
+
assertFalse(results.getBoolean("pending"))
|
|
137
|
+
assertTrue(results.has("timestamp"))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun `performSecurityChecks does not include a playIntegrity entry while Play Integrity is disabled`() {
|
|
142
|
+
runTest {
|
|
143
|
+
val results = SecurityCheckManager.performSecurityChecks(context)
|
|
144
|
+
assertFalse(results.has("playIntegrity"))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun `performSecurityChecks persists the latest results to SharedPreferences`() = runTest {
|
|
150
|
+
SecurityCheckManager.performSecurityChecks(context)
|
|
151
|
+
|
|
152
|
+
val saved = prefsStorage["latest_security_results"]
|
|
153
|
+
assertTrue(saved != null)
|
|
154
|
+
val savedJson = JSONObject(saved!!)
|
|
155
|
+
assertEquals("ALLOW", savedJson.getString("recommendation"))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================
|
|
159
|
+
// performSecurityChecks -- BLOCK path (Frida socket trick)
|
|
160
|
+
// ============================================================
|
|
161
|
+
|
|
162
|
+
@Test
|
|
163
|
+
fun `performSecurityChecks returns BLOCK and compromised when Frida is detected`() = runTest {
|
|
164
|
+
val server = bindOrSkip(27042)
|
|
165
|
+
try {
|
|
166
|
+
val results = SecurityCheckManager.performSecurityChecks(context)
|
|
167
|
+
|
|
168
|
+
assertTrue(results.getBoolean("isFridaDetected"))
|
|
169
|
+
assertTrue(results.getBoolean("isCompromised"))
|
|
170
|
+
assertEquals("BLOCK", results.getString("recommendation"))
|
|
171
|
+
} finally {
|
|
172
|
+
server.close()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================
|
|
177
|
+
// getLatestSecurityResults
|
|
178
|
+
// ============================================================
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
fun `getLatestSecurityResults returns null when nothing has been saved yet`() {
|
|
182
|
+
assertNull(SecurityCheckManager.getLatestSecurityResults(context))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Test
|
|
186
|
+
fun `getLatestSecurityResults returns the previously saved results`() = runTest {
|
|
187
|
+
SecurityCheckManager.performSecurityChecks(context)
|
|
188
|
+
|
|
189
|
+
val latest = SecurityCheckManager.getLatestSecurityResults(context)
|
|
190
|
+
assertTrue(latest != null)
|
|
191
|
+
assertEquals("ALLOW", latest!!.getString("recommendation"))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@Test
|
|
195
|
+
fun `getLatestSecurityResults returns null when SharedPreferences access throws`() {
|
|
196
|
+
val throwingContext: Context = mock {
|
|
197
|
+
on { getSharedPreferences(any(), any()) } doAnswer { throw RuntimeException("boom") }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
assertNull(SecurityCheckManager.getLatestSecurityResults(throwingContext))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================
|
|
204
|
+
// getSecurityMode
|
|
205
|
+
// ============================================================
|
|
206
|
+
|
|
207
|
+
@Test
|
|
208
|
+
fun `getSecurityMode returns the configured value when present`() {
|
|
209
|
+
val propsContext = contextWithProperties("android.security.mode=strict\n")
|
|
210
|
+
|
|
211
|
+
assertEquals("strict", SecurityCheckManager.getSecurityMode(propsContext))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@Test
|
|
215
|
+
fun `getSecurityMode defaults to 'default' when the key is absent`() {
|
|
216
|
+
val propsContext = contextWithProperties("some.other.key=value\n")
|
|
217
|
+
|
|
218
|
+
assertEquals("default", SecurityCheckManager.getSecurityMode(propsContext))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
fun `getSecurityMode defaults to 'default' when reading the properties file throws`() {
|
|
223
|
+
val throwingAssets: android.content.res.AssetManager = mock {
|
|
224
|
+
on { open(any()) } doAnswer { throw java.io.IOException("no such asset") }
|
|
225
|
+
}
|
|
226
|
+
val propsContext: Context = mock {
|
|
227
|
+
on { getAssets() } doReturn throwingAssets
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
assertEquals("default", SecurityCheckManager.getSecurityMode(propsContext))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================
|
|
234
|
+
// helpers
|
|
235
|
+
// ============================================================
|
|
236
|
+
|
|
237
|
+
private fun contextWithProperties(propertiesContent: String): Context {
|
|
238
|
+
val assets: android.content.res.AssetManager = mock {
|
|
239
|
+
on { open("webview_config.properties") } doReturn ByteArrayInputStream(propertiesContent.toByteArray())
|
|
240
|
+
}
|
|
241
|
+
return mock {
|
|
242
|
+
on { getAssets() } doReturn assets
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private fun bindOrSkip(port: Int): ServerSocket {
|
|
247
|
+
return try {
|
|
248
|
+
ServerSocket().apply {
|
|
249
|
+
reuseAddress = true
|
|
250
|
+
bind(InetSocketAddress("127.0.0.1", port))
|
|
251
|
+
}
|
|
252
|
+
} catch (e: Exception) {
|
|
253
|
+
throw IllegalStateException(
|
|
254
|
+
"Could not bind port $port for SecurityCheckManager test -- is something " +
|
|
255
|
+
"already listening on it?",
|
|
256
|
+
e
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package io.yourname.androidproject.security
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import kotlinx.coroutines.Dispatchers
|
|
6
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
7
|
+
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
8
|
+
import kotlinx.coroutines.test.resetMain
|
|
9
|
+
import kotlinx.coroutines.test.runTest
|
|
10
|
+
import kotlinx.coroutines.test.setMain
|
|
11
|
+
import org.junit.After
|
|
12
|
+
import org.junit.Assert.assertEquals
|
|
13
|
+
import org.junit.Assert.assertTrue
|
|
14
|
+
import org.junit.Before
|
|
15
|
+
import org.junit.Test
|
|
16
|
+
import org.mockito.kotlin.any
|
|
17
|
+
import org.mockito.kotlin.doAnswer
|
|
18
|
+
import org.mockito.kotlin.doReturn
|
|
19
|
+
import org.mockito.kotlin.mock
|
|
20
|
+
import java.util.concurrent.CountDownLatch
|
|
21
|
+
import java.util.concurrent.TimeUnit
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Unit tests for SecurityCheckScheduler (Android coverage batch 4),
|
|
25
|
+
* previously entirely untested (0/19 lines).
|
|
26
|
+
*
|
|
27
|
+
* Scope: this is a thin Context + CoroutineScope wrapper that delegates to
|
|
28
|
+
* SecurityCheckManager.performSecurityChecks(context) (already covered in
|
|
29
|
+
* depth by SecurityCheckManagerTest -- its ALLOW/BLOCK branches, JSON
|
|
30
|
+
* shape, and persistence are NOT re-verified here). This suite only
|
|
31
|
+
* verifies that SecurityCheckScheduler.initialize():
|
|
32
|
+
* - launches the check in the given scope and the callback receives the
|
|
33
|
+
* results SecurityCheckManager produced,
|
|
34
|
+
* - works with no callback at all (callback is nullable, `callback?.let`),
|
|
35
|
+
* - does not throw when SecurityCheckManager's own call throws (the
|
|
36
|
+
* launched coroutine's inner try/catch swallows it).
|
|
37
|
+
*
|
|
38
|
+
* Empirically-required setup: initialize() delivers the callback via
|
|
39
|
+
* `withContext(Dispatchers.Main)`. A JVM unit test has no real Android
|
|
40
|
+
* main-thread Looper, so Dispatchers.Main is uninitialized by default and
|
|
41
|
+
* would throw ("Module with the Main dispatcher is missing"). Per
|
|
42
|
+
* kotlinx-coroutines-test (already a project dependency -- see
|
|
43
|
+
* SecurityCheckManagerTest's runTest usage), Dispatchers.setMain(...) with
|
|
44
|
+
* a StandardTestDispatcher is installed in setUp()/torn down with
|
|
45
|
+
* Dispatchers.resetMain() in tearDown(), so the Main hop has somewhere
|
|
46
|
+
* real to resolve on instead of throwing.
|
|
47
|
+
*
|
|
48
|
+
* Note this test does NOT rely on runTest's virtual-time
|
|
49
|
+
* advanceUntilIdle() to observe completion: SecurityCheckManager.
|
|
50
|
+
* performSecurityChecks() internally does `withContext(Dispatchers.Default)`
|
|
51
|
+
* -- the REAL Default dispatcher, backed by real background threads, which
|
|
52
|
+
* runTest's virtual scheduler has no visibility into or control over (only
|
|
53
|
+
* the StandardTestDispatcher installed as Main is virtual-time-controlled
|
|
54
|
+
* here). So instead of advancing virtual time, these tests block the test
|
|
55
|
+
* thread on a CountDownLatch that the callback (or a manual completion
|
|
56
|
+
* signal) counts down, with a real wall-clock timeout -- the same
|
|
57
|
+
* "observe real async completion from a JVM unit test" shape as
|
|
58
|
+
* FridaDetectorTest's socket-based tests elsewhere in this suite.
|
|
59
|
+
*
|
|
60
|
+
* Context/SharedPreferences mocking mirrors SecurityCheckManagerTest's
|
|
61
|
+
* setUp() exactly, since initialize() -> performSecurityChecks() ->
|
|
62
|
+
* saveLatestResults() needs a working getSharedPreferences()/edit() chain
|
|
63
|
+
* to complete without throwing.
|
|
64
|
+
*/
|
|
65
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
66
|
+
class SecurityCheckSchedulerTest {
|
|
67
|
+
|
|
68
|
+
private lateinit var prefsStorage: MutableMap<String, String?>
|
|
69
|
+
private lateinit var sharedPreferences: SharedPreferences
|
|
70
|
+
private lateinit var editor: SharedPreferences.Editor
|
|
71
|
+
private lateinit var context: Context
|
|
72
|
+
|
|
73
|
+
@Before
|
|
74
|
+
fun setUp() {
|
|
75
|
+
Dispatchers.setMain(StandardTestDispatcher())
|
|
76
|
+
|
|
77
|
+
prefsStorage = mutableMapOf()
|
|
78
|
+
|
|
79
|
+
editor = mock {
|
|
80
|
+
on { putString(any(), any()) } doAnswer { invocation ->
|
|
81
|
+
val key = invocation.getArgument<String>(0)
|
|
82
|
+
val value = invocation.getArgument<String?>(1)
|
|
83
|
+
prefsStorage[key] = value
|
|
84
|
+
editor
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
org.mockito.kotlin.doNothing().`when`(editor).apply()
|
|
88
|
+
|
|
89
|
+
sharedPreferences = mock {
|
|
90
|
+
on { getString(any(), org.mockito.kotlin.anyOrNull()) } doAnswer { invocation ->
|
|
91
|
+
val key = invocation.getArgument<String>(0)
|
|
92
|
+
val default = invocation.getArgument<String?>(1)
|
|
93
|
+
prefsStorage[key] ?: default
|
|
94
|
+
}
|
|
95
|
+
on { edit() } doReturn editor
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
context = mock {
|
|
99
|
+
on { getSharedPreferences(any(), any()) } doReturn sharedPreferences
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@After
|
|
104
|
+
fun tearDown() {
|
|
105
|
+
Dispatchers.resetMain()
|
|
106
|
+
prefsStorage.clear()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
fun `initialize invokes the callback with the security check results on the main dispatcher`() {
|
|
111
|
+
val latch = CountDownLatch(1)
|
|
112
|
+
var received: org.json.JSONObject? = null
|
|
113
|
+
val callback = object : SecurityCheckScheduler.SecurityCheckCallback {
|
|
114
|
+
override fun onSecurityCheckComplete(results: org.json.JSONObject) {
|
|
115
|
+
received = results
|
|
116
|
+
latch.countDown()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
runTest {
|
|
121
|
+
SecurityCheckScheduler.initialize(context, this, callback)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
assertTrue("callback should have been invoked within 5s", latch.await(5, TimeUnit.SECONDS))
|
|
125
|
+
assertEquals("ALLOW", received!!.getString("recommendation"))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun `initialize with no callback completes without throwing`() = runTest {
|
|
130
|
+
SecurityCheckScheduler.initialize(context, this, callback = null)
|
|
131
|
+
// Reaching here without an exception is the assertion -- initialize()
|
|
132
|
+
// must not require a callback to be supplied. There is no callback
|
|
133
|
+
// signal to await, so this only proves the synchronous launch path
|
|
134
|
+
// (the try/catch around scope.launch{}) doesn't throw.
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Test
|
|
138
|
+
fun `initialize does not propagate an exception when the underlying check throws`() {
|
|
139
|
+
val throwingContext: Context = mock {
|
|
140
|
+
on { getSharedPreferences(any(), any()) } doAnswer { throw RuntimeException("boom") }
|
|
141
|
+
}
|
|
142
|
+
val latch = CountDownLatch(1)
|
|
143
|
+
val callback = object : SecurityCheckScheduler.SecurityCheckCallback {
|
|
144
|
+
override fun onSecurityCheckComplete(results: org.json.JSONObject) {
|
|
145
|
+
latch.countDown()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// SecurityCheckManager.performSecurityChecks itself catches broadly and
|
|
150
|
+
// returns an error JSONObject rather than throwing (see its own
|
|
151
|
+
// createErrorResponse fallback) -- but even if it didn't, initialize()'s
|
|
152
|
+
// own outer try/catch around scope.launch{} plus the inner try/catch
|
|
153
|
+
// around performSecurityChecks() must keep this call from propagating.
|
|
154
|
+
runTest {
|
|
155
|
+
SecurityCheckScheduler.initialize(throwingContext, this, callback)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The call above completing without throwing is the primary
|
|
159
|
+
// assertion. Whether the callback fires at all depends entirely on
|
|
160
|
+
// SecurityCheckManager's already-covered internals (it does still
|
|
161
|
+
// reach ALLOW here since checkRooted/checkEmulator/checkFridaDetected
|
|
162
|
+
// don't touch getSharedPreferences), so this just confirms
|
|
163
|
+
// initialize() itself stays exception-safe with a throwing Context.
|
|
164
|
+
assertTrue(latch.await(5, TimeUnit.SECONDS))
|
|
165
|
+
}
|
|
166
|
+
}
|