diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d3ecaee --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.log +node_modules +sidebar.js +web-ext-artifacts/ +lib/* +yarn-error.log +rentgen.zip + +# Generated PNG icons (build artifacts) +assets/icons/*.png +assets/icon-addon-*.png + +# Exception: do not ignore the `browser-api` directory inside `lib` +!/lib/browser-api/ + +Dockerfile diff --git a/.gitignore b/.gitignore index 88a0e70..e2a5bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules sidebar.js /web-ext-artifacts/ +/artifacts/ lib/* /yarn-error.log /rentgen.zip @@ -11,4 +12,6 @@ lib/* /assets/icon-addon-*.png # Exception: do not ignore the `browser-api` directory inside `lib` -!/lib/browser-api/ \ No newline at end of file +!/lib/browser-api/ + +.claude diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3162661 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,91 @@ +# Rentgen Browser Extension - Docker Build +# See README.md for detailed usage instructions + +# Build stage +FROM node:lts AS builder + +WORKDIR /app + +# Copy package files for dependency installation (better layer caching) +COPY package.json package-lock.json ./ + +# Install dependencies +RUN npm install + +# FIXME: COPY . . invalidates cache, so we need to optionally install Firefox +# and jump back to the correct stage. It might be too complex though, so we +# either need to use build args (if cache properly) or heavily document the +# stage transitions, maybe even with a graph. + +# Copy source code (respecting .dockerignore) +COPY . . + +# Build the extension for Firefox (default) +RUN npm run build + +# Create the package +RUN npm run create-package + +# Code quality stage - for running quality checks +FROM builder AS code_quality +RUN npm run typecheck && npm run lint + +# Artifacts stage - only contains the built artifacts (for --output) +FROM scratch AS artifacts + +# Copy only the built extension zip file to root +COPY --from=builder /app/web-ext-artifacts/*.zip / + +# Default stage - full development environment +FROM builder + +# Default command shows the built artifact +CMD ["ls", "-lh", "/app/web-ext-artifacts/"] + +# Runtime stage - for running extension in Firefox +FROM node:lts AS runtime + +WORKDIR /app + +# Copy built extension from builder +COPY --from=builder /app /app + +# Install Firefox and Xvfb for headless execution (cached layer) +RUN apt-get update && apt-get install -y \ + firefox-esr \ + xvfb \ + procps \ + && rm -rf /var/lib/apt/lists/* + +# Install Python and pip in a separate layer for better caching +RUN apt-get update && apt-get install -y \ + python3-pip \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install geckodriver for WebDriver protocol +RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v0.34.0/geckodriver-v0.34.0-linux64.tar.gz \ + && tar -xzf geckodriver-v0.34.0-linux64.tar.gz \ + && mv geckodriver /usr/local/bin/ \ + && rm geckodriver-v0.34.0-linux64.tar.gz \ + && chmod +x /usr/local/bin/geckodriver + +# Install Python dependencies for testing +RUN pip3 install --break-system-packages marionette_driver + +# Set display for Xvfb +ENV DISPLAY=:99 + +# Start script (not used in verify stage) +CMD ["echo", "Use verify stage for testing"] + +# Integration test stage - automated testing with exit code +FROM runtime AS integration_test + +# Copy verification scripts +COPY tests/test_verify.py /app/tests/test_verify.py +COPY tests/test-lib.js /app/tests/test-lib.js +RUN chmod +x /app/tests/test_verify.py + +# Run verification and exit with proper exit code +CMD ["python3", "/app/tests/test_verify.py"] diff --git a/background.ts b/background.ts index 4e2e800..a25475c 100644 --- a/background.ts +++ b/background.ts @@ -1,3 +1,30 @@ import { init } from "./memory"; +// Use global browser object directly (available in extension context) +declare const browser: any; + init(); + +// Test verification handler for Marionette tests +// This proves the background script is executing and can communicate with content scripts +browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: any) => { + if (message.type === 'RENTGEN_TEST_VERIFICATION') { + // Perform a computation to prove the background script is running + // This is not just an echo - we're doing actual processing + const inputValue = message.inputValue || 0; + const computed = (inputValue * 2) + 3; + + // Send back a response with computed value and metadata + const response = { + success: true, + computed: computed, + formula: `(${inputValue} * 2) + 3 = ${computed}`, + backgroundTimestamp: Date.now(), + receivedFrom: message.url || 'unknown', + originalInput: inputValue + }; + + sendResponse(response); + return true; // Keep channel open for async response + } +}); diff --git a/components/report-window/use-survey.ts b/components/report-window/use-survey.ts index 4a1a85f..db0e47c 100644 --- a/components/report-window/use-survey.ts +++ b/components/report-window/use-survey.ts @@ -8,7 +8,7 @@ import verbs, { v } from './verbs'; export default function useSurvey( clusters: RequestCluster[], { onComplete }: { onComplete: (sender: { data: RawAnswers }) => void } -): Survey.ReactSurveyModel | null { +): Survey.Model | null { const [survey, setSurvey] = React.useState(null); React.useEffect(() => { const model = generateSurveyQuestions(clusters); diff --git a/components/sidebar/stolen-data.tsx b/components/sidebar/stolen-data.tsx index e4aac5f..f6f6b60 100644 --- a/components/sidebar/stolen-data.tsx +++ b/components/sidebar/stolen-data.tsx @@ -43,7 +43,6 @@ export function StolenData({ origin={origin} shorthost={cluster.id} key={cluster.id + origin} - refreshToken={eventCounts[cluster.id] || 0} minValueLength={minValueLength} cookiesOnly={cookiesOnly} cookiesOrOriginOnly={cookiesOrOriginOnly} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..ffc925c --- /dev/null +++ b/compose.yml @@ -0,0 +1,21 @@ +services: + rentgen_build: + build: . + + rentgen_check: + build: + context: . + target: code_quality + + rentgen_run: + build: + context: . + target: runtime + stdin_open: true + tty: true + + rentgen_verify: + build: + context: . + target: integration_test + restart: "no" diff --git a/esbuild.config.js b/esbuild.config.js index 873010c..e9fa453 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -48,6 +48,7 @@ esbuild 'components/sidebar/sidebar.tsx', 'components/report-window/report-window.tsx', 'background.ts', + 'tests/test-content-script.js', 'diag.tsx', 'styles/global.scss', 'styles/fonts.scss', diff --git a/memory.ts b/memory.ts index 7b678aa..5c228da 100644 --- a/memory.ts +++ b/memory.ts @@ -13,6 +13,7 @@ function setDomainsCount(counter: number, tabId: number) { export default class Memory extends SaferEmitter { origin_to_history = {} as Record>; + async register(request: ExtendedRequest) { await request.init(); if (!request.isThirdParty()) { @@ -45,7 +46,6 @@ export default class Memory extends SaferEmitter { constructor() { super(); - browser.webRequest.onBeforeRequest.addListener( async (request) => { new ExtendedRequest(request); diff --git a/package.json b/package.json index 8766095..ed0e312 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "create-package:firefox": "web-ext build --overwrite-dest --artifacts-dir ../web-ext-artifacts", "create-package:chrome": "cd dist-chrome && 7z a -tzip ../web-ext-artifacts/rentgen-chrome-0.1.10.zip * && cd ..", "typecheck": "tsc --noEmit", - "lint": "web-ext lint" + "lint": "web-ext lint", + "docker:verify": "docker compose up --force-recreate --build --abort-on-container-exit --exit-code-from rentgen_verify", + "docker:clean": "docker compose down --rmi local --volumes --remove-orphans" }, "repository": { "type": "git", diff --git a/tests/pre-commit b/tests/pre-commit new file mode 100755 index 0000000..e04963a --- /dev/null +++ b/tests/pre-commit @@ -0,0 +1,21 @@ +#!/bin/bash +# Pre-commit hook for Rentgen extension +# Builds and runs verification tests before allowing commit + +set -e + +echo "Running pre-commit checks..." + +# Build all stages +echo "Building Docker images..." +docker compose build + +# Run code quality checks (typecheck + lint) +echo "Running code quality checks..." +docker compose up --abort-on-container-exit --exit-code-from rentgen_check rentgen_check + +# Run integration tests +echo "Running integration tests..." +docker compose up --abort-on-container-exit --exit-code-from rentgen_verify rentgen_verify + +echo "✓ All pre-commit checks passed!" diff --git a/tests/test-content-script.js b/tests/test-content-script.js new file mode 100644 index 0000000..507b67c --- /dev/null +++ b/tests/test-content-script.js @@ -0,0 +1,69 @@ +// Test content script - only for automated testing +// This script proves bidirectional communication between content script and background + +// Set initial DOM marker to prove content script is injected + +(function() { + function setMarker() { + if (document.body) { + document.body.setAttribute('data-rentgen-injected', 'true'); + } else { + // Wait for DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + document.body.setAttribute('data-rentgen-injected', 'true'); + }); + } + } + } + setMarker(); +})(); + +// Listen for test request from Marionette test script +document.addEventListener('rentgen_test_request', async (event) => { + try { + // Mark that we received the event + document.body.setAttribute('data-rentgen-event-received', 'true'); + + // Extract test data from event + const testData = event.detail || {}; + const inputValue = testData.value || 42; + const timestamp = testData.timestamp || Date.now(); + + // Send message to background script and wait for response + // This proves background script is running and responsive + const response = await browser.runtime.sendMessage({ + type: 'RENTGEN_TEST_VERIFICATION', + inputValue: inputValue, + timestamp: timestamp, + url: window.location.href, + title: document.title + }); + + // Store the response from background in DOM + // This provides undeniable proof of bidirectional communication + if (response && response.success) { + document.body.setAttribute('data-rentgen-verified', 'true'); + document.body.setAttribute('data-rentgen-computed', String(response.computed)); + document.body.setAttribute('data-rentgen-formula', response.formula); + document.body.setAttribute('data-rentgen-background-timestamp', String(response.backgroundTimestamp)); + + // Also dispatch a custom event with the results + document.dispatchEvent(new CustomEvent('rentgen_test_complete', { + detail: { + success: true, + computed: response.computed, + formula: response.formula, + backgroundTimestamp: response.backgroundTimestamp + } + })); + } else { + document.body.setAttribute('data-rentgen-verified', 'false'); + document.body.setAttribute('data-rentgen-error', 'No response from background'); + } + } catch (error) { + // Store error in DOM for debugging + document.body.setAttribute('data-rentgen-verified', 'false'); + document.body.setAttribute('data-rentgen-error', String(error)); + } +}); \ No newline at end of file diff --git a/tests/test-lib.js b/tests/test-lib.js new file mode 100644 index 0000000..6ab61d4 --- /dev/null +++ b/tests/test-lib.js @@ -0,0 +1,58 @@ +// Test library for Marionette-based extension verification +// This JavaScript code runs in the browser context via Marionette + +/** + * Inject test content script into the page + * @returns {Promise} - True if injection successful + */ +async function injectTestContentScript() { + // Read the content script file + const response = await fetch(browser.runtime.getURL('lib/tests/test-content-script.js')); + const scriptCode = await response.text(); + + // Inject it into the page + const script = document.createElement('script'); + script.textContent = scriptCode; + document.documentElement.appendChild(script); + script.remove(); + + // Wait a bit for script to initialize + await new Promise(resolve => setTimeout(resolve, 100)); + + return document.body.getAttribute('data-rentgen-injected') === 'true'; +} + +/** + * Test that background script performs computation correctly + * @param {number} testValue - Input value for computation + * @returns {Promise} - Computed result or null on failure + */ +async function testBackgroundComputation(testValue) { + // Inject content script first + const injected = await injectTestContentScript(); + if (!injected) { + return -1; // Content script not loaded + } + + // Dispatch test request to content script + document.dispatchEvent(new CustomEvent('rentgen_test_request', { + detail: { value: testValue, timestamp: Date.now() } + })); + + // Wait for background response + return new Promise((resolve) => { + let attempts = 0; + const checkInterval = setInterval(() => { + attempts++; + const computed = document.body.getAttribute('data-rentgen-computed'); + + if (computed) { + clearInterval(checkInterval); + resolve(parseInt(computed)); + } else if (attempts > 50) { + clearInterval(checkInterval); + resolve(null); + } + }, 100); + }); +} diff --git a/tests/test_verify.py b/tests/test_verify.py new file mode 100755 index 0000000..f61a660 --- /dev/null +++ b/tests/test_verify.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +test_verify.py - Minimal extension verification test + +Verifies the extension background script is executing by testing +bidirectional communication with a simple addition operation. +""" + +import sys +import time +import subprocess +import os +import signal + + +def is_tty(): + """Check if stdout is a TTY.""" + return sys.stdout.isatty() + + +def red(text): + """Return red text if TTY, otherwise plain text.""" + if is_tty(): + return f"\033[91m{text}\033[0m" + return text + + +def start_xvfb(): + """Start Xvfb virtual X server. Returns PID.""" + xvfb = subprocess.Popen( + ["Xvfb", ":99", "-screen", "0", "1024x768x24"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + os.environ["DISPLAY"] = ":99" + time.sleep(2) + return xvfb.pid + + +def start_webext(): + """Start web-ext with Marionette enabled. Returns PID.""" + webext = subprocess.Popen( + ["npx", "web-ext", "run", + "--arg=-marionette", + "--arg=--marionette-port", + "--arg=2828"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + return webext.pid + + +def test_addition(): + """Test background script via Marionette. Returns (success, result).""" + try: + from marionette_driver.marionette import Marionette + + # Wait for Firefox to start + time.sleep(10) + + # Connect to Marionette + client = Marionette(host='localhost', port=2828) + client.start_session() + + # Navigate to any page (needed for content script injection) + client.navigate("https://example.com") + time.sleep(5) + + # Test: background should compute (17 * 2) + 3 = 37 + test_value = 17 + expected = 37 + + # Load test library + test_lib_path = os.path.join(os.path.dirname(__file__), 'test-lib.js') + with open(test_lib_path, 'r') as f: + test_lib = f.read() + + # Execute test + result = client.execute_script( + test_lib + "\nreturn testBackgroundComputation(arguments[0]);", + script_args=[test_value], + script_timeout=10000 + ) + + client.close() + + if result == expected: + return True, expected + else: + return False, result + + except Exception as e: + return False, str(e) + + +def cleanup(xvfb_pid, webext_pid): + """Kill processes.""" + try: + os.kill(webext_pid, signal.SIGTERM) + except: + pass + try: + os.kill(xvfb_pid, signal.SIGTERM) + except: + pass + + +def main(): + """Main test.""" + xvfb_pid = start_xvfb() + webext_pid = start_webext() + + success, result = test_addition() + + cleanup(xvfb_pid, webext_pid) + + if not success: + print(red(f"FAIL: Expected 37, got {result}")) + return 1 + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(130) + except Exception as e: + print(red(f"ERROR: {e}")) + sys.exit(1)