mirror of
http://88.130.71.182:3000/BlitTech/contexta_be.git
synced 2026-06-12 23:23:21 +00:00
feat: add appointments, campaigns, admin, storage, tests and various updates
- 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>
This commit is contained in:
151
tests/test_widget.py
Normal file
151
tests/test_widget.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user