const assert = require("assert"); const fs = require("fs"); const path = require("path"); const vm = require("vm"); const root = path.join(__dirname, ".."); const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-overlays-")); const serviceDir = path.join(sandbox, "src", "services"); fs.mkdirSync(serviceDir, { recursive: true }); for (const file of [ "config.js", "db.js", "settings.js", "web-events.js", "overlay-secrets.js", "obs-browser-defaults.js", "overlay-modules.js", "overlays.js", "overlay-permissions.js", "overlay-connectors.js", "overlay-chat.js", "discord-chat-content.js" ]) { fs.copyFileSync(path.join(root, "src", "services", file), path.join(serviceDir, file)); } let database = null; try { database = require(path.join(serviceDir, "db.js")); database.migrate(); const settings = require(path.join(serviceDir, "settings.js")); settings.ensureDefaults(); settings.setSetting("session_secret", "overlay-verification-session-secret"); const overlays = require(path.join(serviceDir, "overlays.js")); const overlayModules = require(path.join(serviceDir, "overlay-modules.js")); const permissions = require(path.join(serviceDir, "overlay-permissions.js")); const connectors = require(path.join(serviceDir, "overlay-connectors.js")); const providerList = connectors.listOverlayConnectorProviders(); assert(providerList.some((provider) => provider.id === "local_obs_websocket" && provider.available !== false)); assert(providerList.some((provider) => provider.id === "obs_browser_bridge" && provider.available !== false)); assert(providerList.some((provider) => provider.id === "trusted_lumi_client" && provider.available === false)); const bridge = new connectors.ObsBrowserBridgeConnector({ overlay_id: "bridge-test", sync_direction: "obs_to_overlay" }); const bridgeReport = bridge.acceptReport({ instance_id: "obs-browser-test", plugin_version: "2.24.0", control_level: 2, current_scene: "Starting Soon", scenes: ["Starting Soon", "Live"], transitions: ["Cut", "Fade"], current_transition: "Fade", canvas_width: 1920, canvas_height: 1080, outputs: { streaming: true }, source_active: true, source_visible: true }); assert.strictEqual(bridgeReport.accepted, true); assert.strictEqual(bridgeReport.current_scene, "Starting Soon"); assert.deepStrictEqual(bridgeReport.scenes, ["Starting Soon", "Live"]); assert.strictEqual(bridgeReport.control_level_label, "Read scenes and transitions"); assert.strictEqual(bridgeReport.profiles_supported, false); bridge.disconnect(); const sourceTypes = overlayModules.listOverlayModuleTypes(); assert(sourceTypes.some((type) => type.id === "video")); assert(sourceTypes.some((type) => type.id === "audio")); assert(sourceTypes.some((type) => type.id === "chat")); const created = overlays.createOverlay({ name: "Main Stream", sceneName: "Live" }); assert.strictEqual(created.scenes.length, 1, "new overlays must receive a first scene"); assert.strictEqual(created.canvas_width, 1920); assert.strictEqual(created.canvas_height, 1080); assert(created.public_token.length >= 40, "overlay token must be high entropy"); assert(created.scenes[0].public_token.length >= 40, "scene token must be high entropy"); const resolved = overlays.resolvePublicOverlay(created.public_token); assert.strictEqual(resolved.overlay.id, created.id); const fixed = overlays.resolvePublicOverlay(created.public_token, created.scenes[0].public_token); assert.strictEqual(fixed.fixedScene.id, created.scenes[0].id); const headlineId = overlays.addModule(created.scenes[0].id, { name: "Headline", type: "text", enabled: true, config: { text: "Starting soon", x: 5, y: 10, width: 90, height: 20 } }); const state = overlays.buildPublicState(created.id); assert.strictEqual(state.scene.modules.length, 1); assert.strictEqual(state.scene.modules[0].config.text, "Starting soon"); overlays.updateModuleTransform(headlineId, { x: 50, y: 50, width: 40, height: 15, anchor: "center" }); const transformed = overlays.getOverlay(created.id).scenes[0].modules[0]; assert.strictEqual(transformed.config.anchor, "center"); assert.strictEqual(transformed.config.text, "Starting soon", "canvas transforms must preserve source content"); const textConfig = overlayModules.normalizeModuleConfig("text", { horizontalAlign: "right", verticalAlign: "bottom", overflow: "shrink", anchor: "bottom-right" }); assert.strictEqual(textConfig.horizontalAlign, "right"); assert.strictEqual(textConfig.verticalAlign, "bottom"); assert.strictEqual(textConfig.overflow, "shrink"); assert.strictEqual(textConfig.anchor, "bottom-right"); for (const supportedColor of ["#fff", "#ffff", "#ffffff", "#ffffffff"]) { assert.strictEqual(overlayModules.normalizeModuleConfig("text", { color: supportedColor }).color, supportedColor); } assert.strictEqual(overlayModules.normalizeModuleConfig("text", { color: "#fffff" }).color, "#ffffff"); const webConfig = overlayModules.normalizeModuleConfig("web", { url: "https://example.com/alerts", autoRefresh: true, healthCheck: true, refreshIntervalSeconds: 10, retryLimit: 9 }); assert.strictEqual(webConfig.autoRefresh, true); assert.strictEqual(webConfig.healthCheck, true); assert.strictEqual(webConfig.refreshIntervalSeconds, 30); assert.strictEqual(webConfig.retryLimit, 5); assert.strictEqual(webConfig.injectPageCss, true); assert.strictEqual(webConfig.forceTransparentBackground, true); assert.strictEqual(webConfig.customCss, "body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }"); const videoConfig = overlayModules.normalizeModuleConfig("video", { url: "https://example.com/alert.webm", playBehavior: "loop", muted: false, volume: 0.65, playbackRate: 1.25, startAt: 2.5, fit: "cover" }); assert.strictEqual(videoConfig.playBehavior, "loop"); assert.strictEqual(videoConfig.volume, 0.65); assert.strictEqual(videoConfig.playbackRate, 1.25); assert.strictEqual(videoConfig.startAt, 2.5); assert.strictEqual(videoConfig.fit, "cover"); const audioConfig = overlayModules.normalizeModuleConfig("audio", { url: "https://example.com/alert.ogg", playBehavior: "manual", volume: 5 }); assert.strictEqual(audioConfig.playBehavior, "manual"); assert.strictEqual(audioConfig.showControls, true); assert.strictEqual(audioConfig.volume, 1); const chatConfig = overlayModules.normalizeModuleConfig("chat", { platforms: ["twitch", "discord", "unsupported"], channels: "twitch:cozycarnage\ndiscord:community-chat", blockedUsers: "twitch:hidden_viewer|viewer-2\nlumi:linked-user|lumi-user-2", maxMessages: 100, messageTimeoutSeconds: -1, background: "#0000", messageBackground: "#0f172bcc", inAnimation: "scale", outAnimation: "slide-down", customCss: ".message { border: 1px solid white; }" }); assert.deepStrictEqual(chatConfig.platforms, ["twitch", "discord"]); assert.deepStrictEqual(chatConfig.channels, ["twitch:cozycarnage", "discord:community-chat"]); assert.deepStrictEqual(chatConfig.blockedUsers, ["twitch:hidden_viewer|viewer-2", "lumi:linked-user|lumi-user-2"]); assert.strictEqual(chatConfig.maxMessages, 50); assert.strictEqual(chatConfig.messageTimeoutSeconds, 0); assert.strictEqual(chatConfig.background, "#0000"); assert.strictEqual(chatConfig.inAnimation, "scale"); assert(chatConfig.customCss.includes(".message")); assert.strictEqual(overlayModules.normalizeModuleConfig("chat", { inAnimation: "slide-right", outAnimation: "slide-up" }).inAnimation, "slide-right"); assert.strictEqual(overlayModules.normalizeModuleConfig("chat", { inAnimation: "slide-right", outAnimation: "slide-up" }).outAnimation, "slide-up"); assert.deepStrictEqual(overlayModules.normalizeModuleConfig("chat", {}).platforms, ["twitch", "youtube", "discord"]); const styledWebConfig = overlayModules.normalizeModuleConfig("web", { url: "https://example.com/widget", cropLeft: 12.5, zoom: 1.4, customCss: "iframe { filter: grayscale(1); }" }); assert.strictEqual(styledWebConfig.cropLeft, 12.5); assert.strictEqual(styledWebConfig.zoom, 1.4); assert(styledWebConfig.customCss.endsWith("iframe { filter: grayscale(1); }")); assert(styledWebConfig.customCss.startsWith("body { background-color: rgba(0, 0, 0, 0);")); assert.strictEqual(overlayModules.normalizeModuleConfig("web", { injectPageCss: false }).injectPageCss, false); overlays.updateOverlay(created.id, { canvasWidth: 1280, canvasHeight: 720 }); assert.deepStrictEqual(overlays.buildPublicState(created.id).canvas, { width: 1280, height: 720 }); const second = overlays.addScene(created.id, { name: "Break", obsSceneName: "BRB" }); overlays.setActiveScene(created.id, second.id, { source: "verify" }); assert.strictEqual(overlays.buildPublicState(created.id).scene.id, second.id); assert.strictEqual(overlays.buildPublicState(created.id, created.scenes[0].id).scene.name, "Live"); const websiteId = overlays.addModule(created.scenes[0].id, { name: "Alerts", type: "web", enabled: true, config: { url: "https://alerts.example/widget" } }); const chatId = overlays.addModule(created.scenes[0].id, { name: "Combined chat", type: "chat", enabled: true, config: { platforms: ["twitch", "youtube"], maxMessages: 6 } }); const chatState = overlays.buildPublicState(created.id, created.scenes[0].id).scene.modules.find((module) => module.id === chatId); assert.strictEqual(chatState.renderType, "chat"); assert.deepStrictEqual(chatState.config.platforms, ["twitch", "youtube"]); const overlayChat = require(path.join(serviceDir, "overlay-chat.js")); const normalizedChat = overlayChat.normalizeChatMessage({ id: "message-1", platform: "twitch", text: "Hello chat", channel: { id: "123", name: "#cozycarnage" }, author: { id: "viewer-1", name: "Viewer", username: "viewer", color: "#abcdef", badges: ["Subscriber"], lumi: { id: "lumi-user-1", username: "linked-user" } } }); assert.strictEqual(normalizedChat.text, "Hello chat"); assert.strictEqual(normalizedChat.author.badges[0].label, "Subscriber"); assert.strictEqual(overlayChat.messageMatchesConfig(normalizedChat, chatState.config), true); assert.strictEqual(overlayChat.messageMatchesConfig({ ...normalizedChat, platform: "discord" }, chatState.config), false); assert.strictEqual(overlayChat.messageBlockedByConfig(normalizedChat, { blockedUsers: ["twitch:viewer|other-id"] }), true); assert.strictEqual(overlayChat.messageBlockedByConfig(normalizedChat, { blockedUsers: ["lumi:linked-user|other-id"] }), true); assert.strictEqual(overlayChat.messageBlockedByConfig(normalizedChat, { blockedUsers: ["youtube:viewer"] }), false); const mediaOnlyChat = overlayChat.normalizeChatMessage({ id: "message-gif", platform: "discord", text: "", media: [{ url: "https://cdn.example/dance.gif", type: "image", alt: "Dance" }], author: { id: "discord-viewer", name: "Discord viewer" } }); assert.strictEqual(mediaOnlyChat.media[0].url, "https://cdn.example/dance.gif"); assert.strictEqual(mediaOnlyChat.text, ""); const { discordOverlayContent } = require(path.join(serviceDir, "discord-chat-content.js")); const discordContent = discordOverlayContent({ content: "Hello https://cdn.example/dance.gif", attachments: new Map(), embeds: [], stickers: new Map() }); assert.strictEqual(discordContent.text, "Hello "); assert.strictEqual(discordContent.emotes[0].image, "https://cdn.discordapp.com/emojis/123456.gif?size=96&quality=lossless"); assert.strictEqual(discordContent.media[0].url, "https://cdn.example/dance.gif"); overlayChat.publishOverlayChatMessage(normalizedChat); assert(overlayChat.recentOverlayChatMessages(created.id).some((event) => event.payload.id === "message-1")); const websiteState = overlays.buildPublicState(created.id, created.scenes[0].id); const websiteRenderUrl = websiteState.scene.modules.find((module) => module.id === websiteId).config.renderUrl; const websiteTicket = websiteRenderUrl.split("/").at(-1); assert(websiteRenderUrl.startsWith("/overlay-web/")); assert.strictEqual(overlays.resolveWebSourceTicket(websiteTicket).moduleId, websiteId); const oldOverlayToken = created.public_token; const newOverlayToken = overlays.regenerateOverlayToken(created.id); assert.strictEqual(overlays.resolvePublicOverlay(oldOverlayToken), null, "regeneration must revoke old overlay tokens"); assert(overlays.resolvePublicOverlay(newOverlayToken)); assert.strictEqual(overlays.resolveWebSourceTicket(websiteTicket), null, "overlay URL regeneration must revoke website render tickets"); const refreshedWebsiteUrl = overlays.buildPublicState(created.id, created.scenes[0].id).scene.modules.find((module) => module.id === websiteId).config.renderUrl; const refreshedWebsiteTicket = refreshedWebsiteUrl.split("/").at(-1); assert(overlays.resolveWebSourceTicket(refreshedWebsiteTicket)); const refreshed = overlays.getOverlay(created.id, { includeSecrets: true }); const firstScene = refreshed.scenes.find((scene) => scene.name === "Live"); const oldSceneToken = firstScene.public_token; const newSceneToken = overlays.regenerateSceneToken(firstScene.id); assert.strictEqual(overlays.resolvePublicOverlay(newOverlayToken, oldSceneToken), null, "regeneration must revoke old fixed scene tokens"); assert(overlays.resolvePublicOverlay(newOverlayToken, newSceneToken)); assert.strictEqual(overlays.resolveWebSourceTicket(refreshedWebsiteTicket), null, "scene URL regeneration must revoke website render tickets"); overlays.deleteModule(websiteId); overlays.deleteModule(chatId); overlays.deleteScene(second.id); assert.throws(() => overlays.deleteScene(firstScene.id), /at least one scene/); const duplicate = overlays.duplicateOverlay(created.id); assert.notStrictEqual(duplicate.public_token, newOverlayToken); assert.strictEqual(duplicate.scenes.length, 1); assert.strictEqual(duplicate.scenes[0].modules.length, 1); overlays.saveObsSettings(created.id, { enabled: false, provider: "local_obs_websocket", endpoint: "ws://127.0.0.1:4455", password: "verification-secret", syncDirection: "bidirectional", reconnect: true }); const storedObs = database.db.prepare("SELECT password_encrypted FROM overlay_obs_settings WHERE overlay_id = ?").get(created.id); assert(storedObs.password_encrypted && !storedObs.password_encrypted.includes("verification-secret")); assert.strictEqual(overlays.getObsSettings(created.id, { includeSecret: true }).password, "verification-secret"); assert.strictEqual(permissions.canAccessOverlay({ isAdmin: true }, created.id, "view_secrets"), true); assert.strictEqual(permissions.canAccessOverlay({ isMod: true }, created.id, "view_secrets"), false); const unregister = permissions.registerOverlayAccessProvider({ allows: ({ overlayId, capability }) => overlayId === created.id && capability === "manage" }); assert.strictEqual(permissions.canAccessOverlay({ isMod: true }, created.id, "manage"), true); assert.strictEqual(permissions.canAccessOverlay({ isMod: true }, created.id, "view_secrets"), false); unregister(); const editorView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-overlay-detail.ejs"), "utf8"); const editorScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-management.js"), "utf8"); const runtimeScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-runtime.js"), "utf8"); const rendererScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-renderer.js"), "utf8"); const renderView = fs.readFileSync(path.join(root, "src", "web", "views", "overlay-render.ejs"), "utf8"); const { OBS_BROWSER_DEFAULT_CSS } = require(path.join(root, "src", "services", "obs-browser-defaults.js")); const connectorScript = fs.readFileSync(path.join(root, "src", "services", "overlay-connectors.js"), "utf8"); const twitchBadgeScript = fs.readFileSync(path.join(root, "src", "services", "twitch-chat-assets.js"), "utf8"); const routeScript = fs.readFileSync(path.join(root, "src", "services", "overlay-routes.js"), "utf8"); assert(editorView.includes("data-overlay-canvas"), "management page must include a visual canvas"); assert(editorView.includes('data-module-fields="text"') && editorView.includes('data-module-fields="web"')); assert(editorView.includes('data-module-fields="video"') && editorView.includes('data-module-fields="audio"')); assert(editorView.includes('data-module-fields="chat"') && editorView.includes('name="chat_custom_css"')); assert(editorView.includes('name="chat_blocked_users"') && editorView.includes("lumi:internal_name|user-id")); assert(editorView.includes('name="custom_css"') && editorView.includes('name="crop_left"')); assert(editorView.includes('name="inject_page_css"') && editorView.includes("Apply CSS inside the website")); assert(editorView.includes('name="force_transparent_background"') && editorView.includes("Keep the website background transparent")); assert(editorView.includes("data-preview-background") && editorView.includes("data-source-revert")); assert(editorView.includes("obsBrowserDefaultCss") && editorView.includes("OBS Browser Source setup")); assert(editorView.includes("data-color-picker") && editorView.includes("overlay-transform-disclosure")); assert(editorView.includes('data-confirm-action="/admin/overlays/<%= overlay.id %>/modules/<%= module.id %>/delete"'), "source deletion must use the shared timed-confirmation action flow"); assert(editorScript.includes("data-resize-handle") && editorScript.includes("snapTargets")); assert(editorScript.includes("previewForm(form)") && editorScript.includes("Live preview ยท not saved")); assert(editorScript.includes("focusCanvas = true") && editorScript.includes("focusCanvas: false"), "draft rerenders must preserve form focus"); assert(editorScript.includes("--overlay-settings-max-height"), "settings height must follow its viewport position"); assert(runtimeScript.includes("overlay:module-refresh") && runtimeScript.includes("checkWebsiteSource")); assert(runtimeScript.includes("overlay:chat-message") && runtimeScript.includes("handleChatMessage")); assert(runtimeScript.includes("window.obsstudio") && runtimeScript.includes("overlay:obs-browser-command")); assert(rendererScript.includes("buildMedia") && rendererScript.includes("buildWebsite")); assert(rendererScript.includes("buildChat") && rendererScript.includes("chatAccepts")); assert(rendererScript.includes("chatUserBlocked") && rendererScript.includes("/icons/platforms/")); assert(rendererScript.includes("/icons/badges/")); assert(rendererScript.includes("previewChatMessages") && rendererScript.includes("schedulePreviewChat")); assert(rendererScript.includes("width:fit-content") && rendererScript.includes("align-self:${alignment}")); assert(rendererScript.includes("enter-slide-right") && rendererScript.includes("leave-slide-up")); assert(rendererScript.includes('mediaData.type === "video"') && rendererScript.includes('className = "media-row"')); assert(rendererScript.includes('frame.setAttribute("allow", "autoplay; fullscreen")')); assert(rendererScript.includes('frame.setAttribute("allowtransparency", "true")')); assert(rendererScript.includes("values.renderUrl || values.url")); assert(!rendererScript.includes('${values.customCss || ""}'), "website CSS must be injected into the page, not scoped to the outer frame"); assert(rendererScript.includes('className = "lumi-overlay-stage"'), "preview and OBS must share a fixed canvas stage"); assert.strictEqual(OBS_BROWSER_DEFAULT_CSS, "body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }"); assert(renderView.includes('')); const rendererContext = { window: { matchMedia: () => ({ matches: false }) }, URL }; vm.runInNewContext(rendererScript, rendererContext); const previewLayout = rendererContext.window.LumiOverlayRenderer.stageLayout( { clientWidth: 480, clientHeight: 270 }, { width: 1920, height: 1080 } ); const obsLayout = rendererContext.window.LumiOverlayRenderer.stageLayout( { clientWidth: 1920, clientHeight: 1080 }, { width: 1920, height: 1080 } ); assert.strictEqual(previewLayout.width, 1920); assert.strictEqual(previewLayout.height, 1080); assert.strictEqual(previewLayout.scaleX, 0.25); assert.strictEqual(previewLayout.scaleY, 0.25); assert.strictEqual(obsLayout.scaleX, 1); assert.strictEqual(obsLayout.scaleY, 1); assert(connectorScript.includes('id: "obs_browser_bridge"') && connectorScript.includes("acceptReport(report")); assert(twitchBadgeScript.includes("api.twitch.tv/helix/${path}") && twitchBadgeScript.includes("chat/badges") && twitchBadgeScript.includes("image_url_2x")); assert(twitchBadgeScript.includes("resolveTwitchAvatar") && twitchBadgeScript.includes("profile_image_url") && twitchBadgeScript.includes("users?${query}")); assert(routeScript.includes("/obs-bridge") && routeScript.includes("/obs/import")); assert(routeScript.includes('/overlay-web/:ticket') && routeScript.includes("loadInjectedOverlayDocument")); assert(routeScript.includes("/preview") && routeScript.includes("web-preview/:ticket")); console.log("Overlay verification passed: CRUD, fixed-canvas preview parity, transparent OBS defaults, layout controls, external recovery, token revocation, encrypted OBS credentials, and permissions."); } finally { database?.db.close(); fs.rmSync(sandbox, { recursive: true, force: true }); }