@xenon-device-management/xenon 2.13.2 → 2.15.0
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/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +85 -71
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
23
|
+
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
24
|
+
const UserSessionService_1 = require("../../src/services/UserSessionService");
|
|
25
|
+
const UserService_1 = require("../../src/services/UserService");
|
|
26
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
27
|
+
const prisma_1 = require("../../src/prisma");
|
|
28
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
29
|
+
/**
|
|
30
|
+
* How long the credential behind a request lasts, as authMiddleware sees it.
|
|
31
|
+
*
|
|
32
|
+
* - req.auth.credentialExpiresAt: when a Bearer token or an API key stops
|
|
33
|
+
* working, so what it mints (POST /auth/token, POST /profile/tokens) can't
|
|
34
|
+
* outlive it.
|
|
35
|
+
* - The dashboard cookie's Max-Age, renewed on every request, follows
|
|
36
|
+
* XENON_USER_SESSION_TTL_MS. It was renewed at a fixed 24 hours, so a
|
|
37
|
+
* longer setting signed people out after a day away anyway.
|
|
38
|
+
*/
|
|
39
|
+
describe('authMiddleware: how long the credential lasts', () => {
|
|
40
|
+
let dir;
|
|
41
|
+
let keys;
|
|
42
|
+
let restore;
|
|
43
|
+
let ttlBefore;
|
|
44
|
+
const member = { id: 'u1', role: 'MEMBER', status: 'ACTIVE' };
|
|
45
|
+
function res() {
|
|
46
|
+
const out = { cookies: [] };
|
|
47
|
+
out.status = (c) => {
|
|
48
|
+
out.code = c;
|
|
49
|
+
return out;
|
|
50
|
+
};
|
|
51
|
+
out.json = (b) => {
|
|
52
|
+
out.body = b;
|
|
53
|
+
return out;
|
|
54
|
+
};
|
|
55
|
+
out.cookie = (name, value, opts) => {
|
|
56
|
+
out.cookies.push({ name, value, opts });
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function run(headers) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
const req = { headers, query: {}, path: '/x' };
|
|
64
|
+
const r = res();
|
|
65
|
+
const next = sinon_1.default.spy();
|
|
66
|
+
yield (0, authMiddleware_1.authMiddleware)(req, r, next);
|
|
67
|
+
(0, chai_1.expect)(next.calledOnce, `refused: ${JSON.stringify(r.body)}`).to.equal(true);
|
|
68
|
+
return { req, res: r };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
ttlBefore = process.env.XENON_USER_SESSION_TTL_MS;
|
|
73
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
|
|
74
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-cred-life-'));
|
|
75
|
+
keys = new JwtKeyService_1.JwtKeyService();
|
|
76
|
+
yield keys.init(dir);
|
|
77
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
|
|
78
|
+
typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().resolves(member) });
|
|
79
|
+
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
80
|
+
verifyPair: sinon_1.default.stub().resolves(null),
|
|
81
|
+
verify: sinon_1.default.stub().resolves(null),
|
|
82
|
+
});
|
|
83
|
+
const sessions = new UserSessionService_1.UserSessionService();
|
|
84
|
+
sinon_1.default.stub(sessions, 'resolve').resolves(null);
|
|
85
|
+
typedi_1.Container.set(UserSessionService_1.UserSessionService, sessions);
|
|
86
|
+
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
87
|
+
}));
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
if (ttlBefore === undefined)
|
|
90
|
+
delete process.env.XENON_USER_SESSION_TTL_MS;
|
|
91
|
+
else
|
|
92
|
+
process.env.XENON_USER_SESSION_TTL_MS = ttlBefore;
|
|
93
|
+
sinon_1.default.restore();
|
|
94
|
+
restore();
|
|
95
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
describe('req.auth.credentialExpiresAt', () => {
|
|
98
|
+
it("is a Bearer token's exp", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
const token = yield keys.sign({ sub: 'u1', scopes: 'read' }, { audience: 'xenon-rest', ttlSeconds: 600 });
|
|
100
|
+
const { req } = yield run({ authorization: `Bearer ${token}` });
|
|
101
|
+
const exp = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()).exp;
|
|
102
|
+
(0, chai_1.expect)(req.auth.credentialExpiresAt).to.equal(exp * 1000);
|
|
103
|
+
}));
|
|
104
|
+
it("is an API key's expiresAt, and absent for a key that never expires", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const expiresAt = new Date(Date.now() + 3600000);
|
|
106
|
+
const verifyPair = typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair;
|
|
107
|
+
verifyPair.resolves({ id: 'k1', userId: 'u1', scopes: 'read', rateLimit: 300, expiresAt });
|
|
108
|
+
const pair = { 'x-xenon-access-key': 'xen_a', 'x-xenon-token': 'tk' };
|
|
109
|
+
(0, chai_1.expect)((yield run(pair)).req.auth.credentialExpiresAt).to.equal(expiresAt.getTime());
|
|
110
|
+
verifyPair.resolves({
|
|
111
|
+
id: 'k1',
|
|
112
|
+
userId: 'u1',
|
|
113
|
+
scopes: 'read',
|
|
114
|
+
rateLimit: 300,
|
|
115
|
+
expiresAt: null,
|
|
116
|
+
});
|
|
117
|
+
(0, chai_1.expect)((yield run(pair)).req.auth.credentialExpiresAt).to.equal(undefined);
|
|
118
|
+
}));
|
|
119
|
+
it('is absent for a dashboard sign-in, which renews itself', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve.resolves({
|
|
121
|
+
id: 's1',
|
|
122
|
+
userId: 'u1',
|
|
123
|
+
});
|
|
124
|
+
const { req } = yield run({ cookie: 'xenon_dashboard_session=s1' });
|
|
125
|
+
(0, chai_1.expect)(req.auth.kind).to.equal('user-session');
|
|
126
|
+
(0, chai_1.expect)(req.auth.credentialExpiresAt).to.equal(undefined);
|
|
127
|
+
}));
|
|
128
|
+
});
|
|
129
|
+
describe("the dashboard cookie's renewed Max-Age", () => {
|
|
130
|
+
it('follows XENON_USER_SESSION_TTL_MS, longer than a day included', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
process.env.XENON_USER_SESSION_TTL_MS = String(7 * 24 * 3600000);
|
|
132
|
+
typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve.resolves({
|
|
133
|
+
id: 's1',
|
|
134
|
+
userId: 'u1',
|
|
135
|
+
});
|
|
136
|
+
const { res: r } = yield run({ cookie: 'xenon_dashboard_session=s1' });
|
|
137
|
+
(0, chai_1.expect)(r.cookies[0].opts.maxAge).to.equal(7 * 24 * 3600000);
|
|
138
|
+
}));
|
|
139
|
+
it('is 24 hours by default', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
+
delete process.env.XENON_USER_SESSION_TTL_MS;
|
|
141
|
+
typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve.resolves({
|
|
142
|
+
id: 's1',
|
|
143
|
+
userId: 'u1',
|
|
144
|
+
});
|
|
145
|
+
const { res: r } = yield run({ cookie: 'xenon_dashboard_session=s1' });
|
|
146
|
+
(0, chai_1.expect)(r.cookies[0].opts.maxAge).to.equal(24 * 3600000);
|
|
147
|
+
}));
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -8,9 +8,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
const chai_1 = require("chai");
|
|
13
16
|
const events_1 = require("events");
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const ffmpegPath_1 = require("../../../src/helpers/ffmpegPath");
|
|
14
19
|
const video_slice_1 = require("../../../src/services/bug-report/video-slice");
|
|
15
20
|
class FakeProc extends events_1.EventEmitter {
|
|
16
21
|
constructor() {
|
|
@@ -41,6 +46,20 @@ describe('sliceVideo', () => {
|
|
|
41
46
|
if (!result.ok)
|
|
42
47
|
(0, chai_1.expect)(result.error).to.include('exit 1');
|
|
43
48
|
}));
|
|
49
|
+
it("runs Xenon's own ffmpeg, never a bare `ffmpeg` from PATH", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
// A server started from the Mac app has no shell PATH, and src/index.ts puts
|
|
51
|
+
// Xenon's ffmpeg folder at the end of PATH, behind any system ffmpeg.
|
|
52
|
+
const proc = new FakeProc();
|
|
53
|
+
let command = '';
|
|
54
|
+
const p = (0, video_slice_1.sliceVideo)('/in.mp4', 0, 60, '/tmp/out.mp4', (cmd) => {
|
|
55
|
+
command = cmd;
|
|
56
|
+
return proc;
|
|
57
|
+
});
|
|
58
|
+
setImmediate(() => proc.emit('exit', 0));
|
|
59
|
+
yield p;
|
|
60
|
+
(0, chai_1.expect)(command).to.equal((0, ffmpegPath_1.resolveFfmpegPath)());
|
|
61
|
+
(0, chai_1.expect)(path_1.default.isAbsolute(command)).to.equal(true);
|
|
62
|
+
}));
|
|
44
63
|
it('returns ok=false on spawn error', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
64
|
const proc = new FakeProc();
|
|
46
65
|
const p = (0, video_slice_1.sliceVideo)('/in.mp4', 0, 60, '/tmp/out.mp4', fakeSpawn(proc));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
23
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
24
|
+
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
25
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
26
|
+
const config_1 = require("../../src/config");
|
|
27
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
28
|
+
const device_utils_1 = require("../../src/device-utils");
|
|
29
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
30
|
+
/**
|
|
31
|
+
* The Maintenance page's path from end to end, against a real (scratch)
|
|
32
|
+
* database: what POST /config saves is what the next cleanup run acts on,
|
|
33
|
+
* whatever the server was started with. (Saving more than one setting used to
|
|
34
|
+
* fail outright, see web-config-service.spec.ts.)
|
|
35
|
+
*/
|
|
36
|
+
describe('a retention window saved on the Maintenance page', () => {
|
|
37
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
38
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
39
|
+
let context;
|
|
40
|
+
let savedArgs;
|
|
41
|
+
let savedRecordings;
|
|
42
|
+
let tmp;
|
|
43
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
45
|
+
savedArgs = context.pluginArgs;
|
|
46
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { buildCleanupDays: 30, buildCleanupMaxCount: 100 });
|
|
47
|
+
savedRecordings = config_1.config.recordingsAssetsPath;
|
|
48
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-cleanup-db-'));
|
|
49
|
+
config_1.config.recordingsAssetsPath = path_1.default.join(tmp, 'recordings');
|
|
50
|
+
yield scratch.db.webConfig.deleteMany({});
|
|
51
|
+
yield scratch.db.build.deleteMany({});
|
|
52
|
+
const ago = (days) => new Date(Date.now() - days * DAY_MS);
|
|
53
|
+
yield scratch.db.build.create({ data: { id: 'b-10-days', createdAt: ago(10) } });
|
|
54
|
+
yield scratch.db.build.create({ data: { id: 'b-1-day', createdAt: ago(1) } });
|
|
55
|
+
}));
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
(0, device_utils_1.stopAllTimers)();
|
|
58
|
+
context.pluginArgs = savedArgs;
|
|
59
|
+
config_1.config.recordingsAssetsPath = savedRecordings;
|
|
60
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
function app() {
|
|
63
|
+
const a = (0, express_1.default)();
|
|
64
|
+
a.use(express_1.default.json());
|
|
65
|
+
a.use((req, _res, next) => {
|
|
66
|
+
req.auth = {
|
|
67
|
+
kind: 'user-session',
|
|
68
|
+
userId: 'usr_x',
|
|
69
|
+
role: 'SUPER_ADMIN',
|
|
70
|
+
scopes: 'admin,devices,sessions,read',
|
|
71
|
+
rateLimit: 1000,
|
|
72
|
+
};
|
|
73
|
+
next();
|
|
74
|
+
});
|
|
75
|
+
dashboard_1.default.register(a);
|
|
76
|
+
return a;
|
|
77
|
+
}
|
|
78
|
+
const buildIds = () => __awaiter(void 0, void 0, void 0, function* () { return (yield scratch.db.build.findMany({ select: { id: true } })).map((b) => b.id).sort(); });
|
|
79
|
+
it('is what the next cleanup run purges by, over the 30 days the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
|
+
const saved = yield (0, loopbackRequest_1.default)(app())
|
|
81
|
+
.post('/config')
|
|
82
|
+
.send({ buildCleanupDays: 5, buildCleanupMaxCount: 50, deleteBuildAssets: false })
|
|
83
|
+
.timeout(5000);
|
|
84
|
+
(0, chai_1.expect)(saved.status).to.equal(200);
|
|
85
|
+
const shown = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
86
|
+
(0, chai_1.expect)(shown.body).to.include({
|
|
87
|
+
buildCleanupDays: 5,
|
|
88
|
+
buildCleanupMaxCount: 50,
|
|
89
|
+
deleteBuildAssets: false,
|
|
90
|
+
});
|
|
91
|
+
yield typedi_1.Container.get(CleanupService_1.CleanupService).runCleanup(context.pluginArgs);
|
|
92
|
+
(0, chai_1.expect)(yield buildIds()).to.deep.equal(['b-1-day']);
|
|
93
|
+
}));
|
|
94
|
+
it('leaves both builds when nothing was saved and the server started with 30 days', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
yield typedi_1.Container.get(CleanupService_1.CleanupService).runCleanup(context.pluginArgs);
|
|
96
|
+
(0, chai_1.expect)(yield buildIds()).to.deep.equal(['b-1-day', 'b-10-days']);
|
|
97
|
+
}));
|
|
98
|
+
it('purges by a cap saved over the 100 the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
yield (0, loopbackRequest_1.default)(app()).post('/config').send({ buildCleanupMaxCount: 1 }).timeout(5000);
|
|
100
|
+
yield typedi_1.Container.get(CleanupService_1.CleanupService).runCleanup(context.pluginArgs);
|
|
101
|
+
(0, chai_1.expect)(yield buildIds()).to.deep.equal(['b-1-day']);
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const node_schedule_1 = __importDefault(require("node-schedule"));
|
|
21
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
22
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
23
|
+
const prisma_1 = require("../../src/prisma");
|
|
24
|
+
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
25
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
26
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
27
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
28
|
+
const device_utils_1 = require("../../src/device-utils");
|
|
29
|
+
/**
|
|
30
|
+
* The Maintenance page saves the cleanup values (POST /config -> WebConfig),
|
|
31
|
+
* but the cleanup job read only the plugin options the server started with,
|
|
32
|
+
* so a saved retention window, build cap, asset purge or schedule did
|
|
33
|
+
* nothing until it matched what the server was started with, and never took
|
|
34
|
+
* effect at all. These pin the rule that replaced it: a value saved in the
|
|
35
|
+
* dashboard wins over the startup option, applies at the next run without a
|
|
36
|
+
* restart, and a new schedule replaces the old timer at once.
|
|
37
|
+
*/
|
|
38
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
39
|
+
function startupArgs(over = {}) {
|
|
40
|
+
return Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { buildCleanupDays: 30, buildCleanupMaxCount: 100, buildCleanupSchedule: '0 0 * * *', deleteBuildAssets: true }), over);
|
|
41
|
+
}
|
|
42
|
+
/** How many days back the first build query looks: the retention window the run used. */
|
|
43
|
+
function daysBack(findMany) {
|
|
44
|
+
const cutoff = findMany.firstCall.args[0].where.createdAt.lt;
|
|
45
|
+
return Math.round((Date.now() - cutoff.getTime()) / DAY_MS);
|
|
46
|
+
}
|
|
47
|
+
/** A WebConfig table in memory: what POST /config saves is what GET reads. */
|
|
48
|
+
function fakeWebConfig(initial = {}) {
|
|
49
|
+
const saved = Object.assign({}, initial);
|
|
50
|
+
const svc = typedi_1.Container.get(web_config_service_1.WebConfigService);
|
|
51
|
+
sinon_1.default.stub(svc, 'getConfig').callsFake(() => __awaiter(this, void 0, void 0, function* () { return (Object.assign({}, saved)); }));
|
|
52
|
+
sinon_1.default.stub(svc, 'setConfig').callsFake((c) => __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
for (const [k, v] of Object.entries(c))
|
|
54
|
+
if (v !== undefined)
|
|
55
|
+
saved[k] = v;
|
|
56
|
+
}));
|
|
57
|
+
return saved;
|
|
58
|
+
}
|
|
59
|
+
describe('cleanup settings saved in the dashboard', () => {
|
|
60
|
+
let context;
|
|
61
|
+
let savedArgs;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
64
|
+
savedArgs = context.pluginArgs;
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
(0, device_utils_1.stopAllTimers)();
|
|
68
|
+
context.pluginArgs = savedArgs;
|
|
69
|
+
sinon_1.default.restore();
|
|
70
|
+
});
|
|
71
|
+
describe('the cleanup run', () => {
|
|
72
|
+
function stubDatabase() {
|
|
73
|
+
const findMany = sinon_1.default.stub(prisma_1.prisma.build, 'findMany').resolves([]);
|
|
74
|
+
sinon_1.default.stub(prisma_1.prisma.build, 'count').resolves(0);
|
|
75
|
+
const service = typedi_1.Container.get(CleanupService_1.CleanupService);
|
|
76
|
+
const purgeBuild = sinon_1.default.stub(service, 'purgeBuild').resolves();
|
|
77
|
+
sinon_1.default.stub(service, 'purgeOrphanSessions').resolves();
|
|
78
|
+
sinon_1.default.stub(service, 'purgeRecordings').resolves();
|
|
79
|
+
return { service, findMany, purgeBuild };
|
|
80
|
+
}
|
|
81
|
+
it('uses the retention window saved in the dashboard, not the startup option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
fakeWebConfig({ buildCleanupDays: 7 });
|
|
83
|
+
const { service, findMany } = stubDatabase();
|
|
84
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 30 }));
|
|
85
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(7);
|
|
86
|
+
}));
|
|
87
|
+
it('uses the build cap and the asset purge saved in the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
fakeWebConfig({ buildCleanupMaxCount: 2, deleteBuildAssets: false });
|
|
89
|
+
const { service, findMany, purgeBuild } = stubDatabase();
|
|
90
|
+
prisma_1.prisma.build.count.resolves(3);
|
|
91
|
+
findMany.callsFake(((args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
var _a;
|
|
93
|
+
if ((args === null || args === void 0 ? void 0 : args.take) === 2)
|
|
94
|
+
return [{ id: 'b3' }, { id: 'b2' }];
|
|
95
|
+
if ((_a = args === null || args === void 0 ? void 0 : args.where) === null || _a === void 0 ? void 0 : _a.createdAt)
|
|
96
|
+
return [];
|
|
97
|
+
return [{ id: 'b1' }, { id: 'b2' }, { id: 'b3' }];
|
|
98
|
+
})));
|
|
99
|
+
yield service.runCleanup(startupArgs({ buildCleanupMaxCount: 100, deleteBuildAssets: true }));
|
|
100
|
+
(0, chai_1.expect)(purgeBuild.calledOnce).to.equal(true);
|
|
101
|
+
(0, chai_1.expect)(purgeBuild.firstCall.args).to.deep.equal(['b1', false]);
|
|
102
|
+
}));
|
|
103
|
+
it('falls back to the startup options when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
fakeWebConfig({});
|
|
105
|
+
const { service, findMany } = stubDatabase();
|
|
106
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 12 }));
|
|
107
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(12);
|
|
108
|
+
}));
|
|
109
|
+
it('still runs on the startup options when the saved settings cannot be read', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig').rejects(new Error('db down'));
|
|
111
|
+
const { service, findMany } = stubDatabase();
|
|
112
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 12 }));
|
|
113
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(12);
|
|
114
|
+
}));
|
|
115
|
+
it('ignores a saved value that would purge everything', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
fakeWebConfig({ buildCleanupDays: 0, buildCleanupMaxCount: 0 });
|
|
117
|
+
const { service, findMany } = stubDatabase();
|
|
118
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 12 }));
|
|
119
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(12);
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
describe('the schedule', () => {
|
|
123
|
+
it('starts on the schedule saved in the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
fakeWebConfig({ buildCleanupSchedule: '0 3 * * *' });
|
|
125
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
126
|
+
yield (0, device_utils_1.setupCronCleanupBuilds)(startupArgs({ buildCleanupSchedule: '0 0 * * *' }));
|
|
127
|
+
(0, chai_1.expect)(scheduleJob.firstCall.args[0]).to.equal('0 3 * * *');
|
|
128
|
+
}));
|
|
129
|
+
it('starts on the startup schedule when none was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
fakeWebConfig({});
|
|
131
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
132
|
+
yield (0, device_utils_1.setupCronCleanupBuilds)(startupArgs({ buildCleanupSchedule: '30 1 * * *' }));
|
|
133
|
+
(0, chai_1.expect)(scheduleJob.firstCall.args[0]).to.equal('30 1 * * *');
|
|
134
|
+
}));
|
|
135
|
+
});
|
|
136
|
+
describe('POST /config', () => {
|
|
137
|
+
function app() {
|
|
138
|
+
const a = (0, express_1.default)();
|
|
139
|
+
a.use(express_1.default.json());
|
|
140
|
+
a.use((req, _res, next) => {
|
|
141
|
+
req.auth = {
|
|
142
|
+
kind: 'user-session',
|
|
143
|
+
userId: 'usr_x',
|
|
144
|
+
role: 'SUPER_ADMIN',
|
|
145
|
+
scopes: 'admin,devices,sessions,read',
|
|
146
|
+
rateLimit: 1000,
|
|
147
|
+
};
|
|
148
|
+
next();
|
|
149
|
+
});
|
|
150
|
+
dashboard_1.default.register(a);
|
|
151
|
+
return a;
|
|
152
|
+
}
|
|
153
|
+
it('replaces the running cleanup timer when the schedule changes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
|
+
fakeWebConfig({});
|
|
155
|
+
context.pluginArgs = startupArgs({ buildCleanupSchedule: '0 0 * * *' });
|
|
156
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
157
|
+
yield (0, device_utils_1.setupCronCleanupBuilds)(context.pluginArgs);
|
|
158
|
+
const running = scheduleJob.firstCall.returnValue;
|
|
159
|
+
const cancelRunning = sinon_1.default.spy(running, 'cancel');
|
|
160
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
161
|
+
.post('/config')
|
|
162
|
+
.send({ buildCleanupSchedule: '0 4 * * *' })
|
|
163
|
+
.timeout(5000);
|
|
164
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
165
|
+
(0, chai_1.expect)(cancelRunning.calledOnce, 'the old timer was cancelled').to.equal(true);
|
|
166
|
+
(0, chai_1.expect)(scheduleJob.lastCall.args[0]).to.equal('0 4 * * *');
|
|
167
|
+
(0, chai_1.expect)(scheduleJob.lastCall.returnValue).to.not.equal(running);
|
|
168
|
+
}));
|
|
169
|
+
it('does not touch the timer when the schedule is not in the request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
|
+
fakeWebConfig({});
|
|
171
|
+
context.pluginArgs = startupArgs();
|
|
172
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
173
|
+
const r = yield (0, loopbackRequest_1.default)(app()).post('/config').send({ buildCleanupDays: 14 }).timeout(5000);
|
|
174
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
175
|
+
(0, chai_1.expect)(scheduleJob.called).to.equal(false);
|
|
176
|
+
}));
|
|
177
|
+
for (const [label, body] of [
|
|
178
|
+
['a retention window of zero', { buildCleanupDays: 0 }],
|
|
179
|
+
['a negative retention window', { buildCleanupDays: -3 }],
|
|
180
|
+
['a fractional build cap', { buildCleanupMaxCount: 2.5 }],
|
|
181
|
+
['a build cap that is not a number', { buildCleanupMaxCount: null }],
|
|
182
|
+
['a schedule that is not a cron expression', { buildCleanupSchedule: 'every night' }],
|
|
183
|
+
['an asset purge that is not a boolean', { deleteBuildAssets: 'yes' }],
|
|
184
|
+
]) {
|
|
185
|
+
it(`refuses ${label} and saves nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
const saved = fakeWebConfig({});
|
|
187
|
+
context.pluginArgs = startupArgs();
|
|
188
|
+
const r = yield (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
|
|
189
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
190
|
+
(0, chai_1.expect)(r.body.error).to.equal('invalid_setting');
|
|
191
|
+
(0, chai_1.expect)(r.body.message).to.be.a('string').and.not.equal('');
|
|
192
|
+
(0, chai_1.expect)(saved).to.deep.equal({});
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
describe('GET /config', () => {
|
|
197
|
+
function app() {
|
|
198
|
+
const a = (0, express_1.default)();
|
|
199
|
+
a.use((req, _res, next) => {
|
|
200
|
+
req.auth = {
|
|
201
|
+
kind: 'user-session',
|
|
202
|
+
userId: 'usr_x',
|
|
203
|
+
role: 'ADMIN',
|
|
204
|
+
scopes: 'admin,read',
|
|
205
|
+
rateLimit: 1000,
|
|
206
|
+
};
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
dashboard_1.default.register(a);
|
|
210
|
+
return a;
|
|
211
|
+
}
|
|
212
|
+
it('shows the value the server runs with when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
213
|
+
fakeWebConfig({});
|
|
214
|
+
context.pluginArgs = startupArgs({
|
|
215
|
+
buildCleanupDays: 12,
|
|
216
|
+
buildCleanupSchedule: '30 1 * * *',
|
|
217
|
+
});
|
|
218
|
+
const r = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
219
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
220
|
+
(0, chai_1.expect)(r.body.buildCleanupDays).to.equal(12);
|
|
221
|
+
(0, chai_1.expect)(r.body.buildCleanupSchedule).to.equal('30 1 * * *');
|
|
222
|
+
}));
|
|
223
|
+
it('shows the saved value over the startup one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
fakeWebConfig({ buildCleanupDays: 7 });
|
|
225
|
+
context.pluginArgs = startupArgs({ buildCleanupDays: 12 });
|
|
226
|
+
const r = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
227
|
+
(0, chai_1.expect)(r.body.buildCleanupDays).to.equal(7);
|
|
228
|
+
}));
|
|
229
|
+
it("sends the server's own defaults, so no page needs a number of its own", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
+
fakeWebConfig({});
|
|
231
|
+
context.pluginArgs = startupArgs();
|
|
232
|
+
const r = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
233
|
+
(0, chai_1.expect)(r.body.defaults).to.deep.equal({
|
|
234
|
+
enableSelfHealing: true,
|
|
235
|
+
healthCheckIntervalMs: 300000,
|
|
236
|
+
buildCleanupDays: 30,
|
|
237
|
+
buildCleanupMaxCount: 100,
|
|
238
|
+
buildCleanupSchedule: '0 0 * * *',
|
|
239
|
+
deleteBuildAssets: true,
|
|
240
|
+
});
|
|
241
|
+
(0, chai_1.expect)(r.body.healthCheckIntervalMs).to.equal(300000);
|
|
242
|
+
}));
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("reflect-metadata");
|
|
7
|
+
const chai_1 = require("chai");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const index_1 = require("../../src/app/index");
|
|
11
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
12
|
+
const expressRoutes_1 = require("../helpers/expressRoutes");
|
|
13
|
+
/**
|
|
14
|
+
* Every address the dashboard calls must be one the server serves.
|
|
15
|
+
*
|
|
16
|
+
* "Assign team" called `PUT /xenon/api/grid/device/:udid/team` while the
|
|
17
|
+
* server mounts that route at the API root (`/xenon/api/device/:udid/team`).
|
|
18
|
+
* Nothing mounts `/grid`, so every assignment from the Devices page was a
|
|
19
|
+
* 404, and the only test mocked the whole api-service, so it never saw it.
|
|
20
|
+
*
|
|
21
|
+
* This reads the paths the dashboard's source writes down (`/xenon/api/...`
|
|
22
|
+
* literals and the `makeGETRequest('/...')` family, which the api client
|
|
23
|
+
* prefixes with `/xenon/api`) and checks each against the routes the router
|
|
24
|
+
* really serves.
|
|
25
|
+
*/
|
|
26
|
+
const WEB_SRC = path_1.default.resolve(__dirname, '../../web/src');
|
|
27
|
+
const API = '/xenon/api';
|
|
28
|
+
/** "/xenon/api/control/:udid/tap" and "/xenon/api/control/${udid}/tap" both -> ".../control/{}/tap" */
|
|
29
|
+
const shape = (p) => p
|
|
30
|
+
.replace(/\$\{[^}]*\}/g, '{}')
|
|
31
|
+
.replace(/:[A-Za-z_]\w*/g, '{}')
|
|
32
|
+
.replace(/\/+$/, '');
|
|
33
|
+
// Addresses written in the dashboard that are not served, each with why.
|
|
34
|
+
const KNOWN_NOT_SERVED = {
|
|
35
|
+
[`${API}/session/{}/xenon/omni-scan`]: 'XenonApiService.omniScan: an Appium plugin route, not under /xenon/api; no caller',
|
|
36
|
+
[`${API}/session/{}/xenon/test-locator`]: 'XenonApiService.testAiLocator: an Appium plugin route, not under /xenon/api; no caller',
|
|
37
|
+
[`${API}/sessions`]: "ApiKeyGate's sign-in probe: only the 401 matters, and the login check answers before routing",
|
|
38
|
+
};
|
|
39
|
+
// Written as a base that the same file extends (`const BASE = '/xenon/api/auth'`,
|
|
40
|
+
// then `${BASE}/me`), so the base alone is not a route.
|
|
41
|
+
const BASES = [`${API}/auth`, `${API}/interceptor`, `${API}/profile`];
|
|
42
|
+
function sources(dir, out = []) {
|
|
43
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const full = path_1.default.join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
if (entry.name !== 'node_modules')
|
|
47
|
+
sources(full, out);
|
|
48
|
+
}
|
|
49
|
+
else if (/\.tsx?$/.test(entry.name) && !/\.(test|spec)\.tsx?$/.test(entry.name)) {
|
|
50
|
+
out.push(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Each address a source file writes down, with the file it is in. */
|
|
56
|
+
function dashboardAddresses() {
|
|
57
|
+
const found = [];
|
|
58
|
+
for (const file of sources(WEB_SRC)) {
|
|
59
|
+
const text = fs_1.default.readFileSync(file, 'utf8');
|
|
60
|
+
const rel = path_1.default.relative(WEB_SRC, file);
|
|
61
|
+
// '/xenon/api/...' in a string or template; not '/xenon/api-docs', nor the
|
|
62
|
+
// api client's bare `/xenon/api${url}` prefix.
|
|
63
|
+
for (const m of text.matchAll(/['"`]\/xenon\/api(\/[^'"`\s?]*)/g)) {
|
|
64
|
+
found.push({ address: `${API}${m[1]}`, file: rel });
|
|
65
|
+
}
|
|
66
|
+
// apiClient.makeGETRequest('/queue') -> the client adds the prefix.
|
|
67
|
+
for (const m of text.matchAll(/make(?:GET|POST|PUT|DELETE|PATCH)Request\(\s*['"`](\/[^'"`?]*)/g)) {
|
|
68
|
+
found.push({ address: `${API}${m[1]}`, file: rel });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
describe('the addresses the dashboard calls', () => {
|
|
74
|
+
const served = [
|
|
75
|
+
...(0, expressRoutes_1.routesOf)((0, index_1.createRouter)(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: 'http://hub.example:4723' })), '/xenon'),
|
|
76
|
+
]
|
|
77
|
+
.filter((r) => r.includes(' /xenon/api'))
|
|
78
|
+
.map((r) => shape(r.slice(r.indexOf(' ') + 1)));
|
|
79
|
+
const isServed = (address) => served.includes(shape(address));
|
|
80
|
+
const isBase = (address) => BASES.includes(shape(address));
|
|
81
|
+
const underBase = (base) => served.some((s) => s.startsWith(`${base}/`));
|
|
82
|
+
it("finds the dashboard's addresses (the scan is not reading nothing)", () => {
|
|
83
|
+
const all = dashboardAddresses();
|
|
84
|
+
(0, chai_1.expect)(all.length).to.be.greaterThan(60);
|
|
85
|
+
(0, chai_1.expect)(all.map((a) => shape(a.address))).to.include(`${API}/device/{}/team`);
|
|
86
|
+
});
|
|
87
|
+
it('are all served by the API router', () => {
|
|
88
|
+
const missing = dashboardAddresses()
|
|
89
|
+
.filter(({ address }) => !isServed(address) && !isBase(address) && !(shape(address) in KNOWN_NOT_SERVED))
|
|
90
|
+
.map(({ address, file }) => `${address} (web/src/${file})`);
|
|
91
|
+
(0, chai_1.expect)([...new Set(missing)].sort(), 'called by the dashboard but not served').to.deep.equal([]);
|
|
92
|
+
});
|
|
93
|
+
it('has no allowance for an address that is now served or no longer written', () => {
|
|
94
|
+
const written = new Set(dashboardAddresses().map(({ address }) => shape(address)));
|
|
95
|
+
const stale = [
|
|
96
|
+
...Object.keys(KNOWN_NOT_SERVED).filter((a) => isServed(a) || !written.has(a)),
|
|
97
|
+
...BASES.filter((b) => !written.has(b) || !underBase(b)),
|
|
98
|
+
];
|
|
99
|
+
(0, chai_1.expect)(stale, 'remove these from KNOWN_NOT_SERVED / BASES').to.deep.equal([]);
|
|
100
|
+
});
|
|
101
|
+
});
|