mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-13 08:03:54 +00:00
- Add new routers: admin, appointments, campaigns - Add storage service and logging config - Add migrations directory and test suite with pytest config - Add supabase_migration_features.sql - Update models, dependencies, config, and existing routers - Remove whatsapp_service (deleted) - Update pyproject.toml and uv.lock dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
152 lines
5.7 KiB
Python
152 lines
5.7 KiB
Python
"""
|
|
Tests for the widget.js endpoint and the JS bundle it generates.
|
|
|
|
GET /widget.js must:
|
|
- Return 200 with application/javascript content-type
|
|
- Include CORS header (any site can load it as a <script>)
|
|
- Cache-Control must be set
|
|
- Body must be valid JavaScript (basic structural checks)
|
|
- APP_URL placeholder must be replaced with the real app URL
|
|
- Must NOT expose the raw __APP_URL__ placeholder
|
|
- Must contain the public API surface (window.Contexta)
|
|
- Must contain the chatbot ID read logic (data-chatbot)
|
|
- Must be a self-executing IIFE
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch
|
|
from app.services.widget import generate_widget_js
|
|
|
|
|
|
# ── Unit tests: generate_widget_js() ──────────────────────────────────────────
|
|
|
|
class TestGenerateWidgetJs:
|
|
def test_replaces_app_url_placeholder(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "https://app.example.com" in js
|
|
|
|
def test_strips_trailing_slash(self):
|
|
js = generate_widget_js("https://app.example.com/")
|
|
assert "https://app.example.com/" not in js
|
|
assert "https://app.example.com" in js
|
|
|
|
def test_no_raw_placeholder_in_output(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "__APP_URL__" not in js
|
|
|
|
def test_constructs_chat_url_pattern(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
# The JS concatenates base + '/chat/' + chatbotId
|
|
assert "/chat/" in js
|
|
|
|
def test_is_iife(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "(function" in js
|
|
assert "}());" in js or "})()" in js or "}())" in js or "()})" in js or "}());" in js
|
|
|
|
def test_contains_double_init_guard(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "__ctxa" in js
|
|
|
|
def test_reads_data_chatbot_attribute(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "data-chatbot" in js
|
|
|
|
def test_uses_document_current_script(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "currentScript" in js
|
|
|
|
def test_public_api_exposed(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "window.Contexta" in js
|
|
assert "open" in js
|
|
assert "close" in js
|
|
assert "toggle" in js
|
|
|
|
def test_lazy_iframe_loading(self):
|
|
"""Iframe src should only be set on first open, not at init time."""
|
|
js = generate_widget_js("https://app.example.com")
|
|
# The _loaded flag pattern ensures lazy load
|
|
assert "_loaded" in js or "frameLoaded" in js or "loaded" in js.lower()
|
|
|
|
def test_escape_key_closes_panel(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "Escape" in js
|
|
|
|
def test_aria_attributes_present(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "aria-label" in js
|
|
assert "aria-expanded" in js
|
|
|
|
def test_mobile_responsive_css(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "480px" in js # mobile breakpoint
|
|
|
|
def test_high_z_index(self):
|
|
"""Widget must sit on top of host-page content."""
|
|
js = generate_widget_js("https://app.example.com")
|
|
# 2147483647 is the highest browser-supported z-index
|
|
assert "2147483647" in js
|
|
|
|
def test_sandbox_attribute_on_iframe(self):
|
|
js = generate_widget_js("https://app.example.com")
|
|
assert "sandbox" in js
|
|
assert "allow-scripts" in js
|
|
|
|
def test_returns_string(self):
|
|
result = generate_widget_js("https://app.example.com")
|
|
assert isinstance(result, str)
|
|
assert len(result) > 500 # must be a substantial bundle
|
|
|
|
|
|
# ── Integration tests: GET /widget.js ─────────────────────────────────────────
|
|
|
|
class TestWidgetEndpoint:
|
|
def test_returns_200(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert resp.status_code == 200
|
|
|
|
def test_content_type_is_javascript(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert "javascript" in resp.headers.get("content-type", "")
|
|
|
|
def test_cors_header_allows_any_origin(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert resp.headers.get("access-control-allow-origin") == "*"
|
|
|
|
def test_cache_control_is_set(self, client):
|
|
resp = client.get("/widget.js")
|
|
cc = resp.headers.get("cache-control", "")
|
|
assert "public" in cc
|
|
assert "max-age" in cc
|
|
|
|
def test_x_content_type_options(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert resp.headers.get("x-content-type-options") == "nosniff"
|
|
|
|
def test_body_contains_app_url(self, client):
|
|
resp = client.get("/widget.js")
|
|
# The test client uses the real settings.app_url baked into the JS
|
|
assert "/chat/" in resp.text
|
|
|
|
def test_body_does_not_contain_placeholder(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert "__APP_URL__" not in resp.text
|
|
|
|
def test_body_contains_public_api(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert "window.Contexta" in resp.text
|
|
|
|
def test_body_is_non_empty(self, client):
|
|
resp = client.get("/widget.js")
|
|
assert len(resp.text) > 200
|
|
|
|
def test_no_auth_required(self, client):
|
|
"""Widget.js must be publicly accessible — no Authorization header."""
|
|
resp = client.get("/widget.js")
|
|
assert resp.status_code == 200
|
|
|
|
def test_get_only(self, client):
|
|
"""Should not accept POST."""
|
|
resp = client.post("/widget.js")
|
|
assert resp.status_code == 405
|