Spaces:
Build error
Build error
import unittest | |
from unittest.mock import patch | |
from fastapi import Request | |
from huggingface_hub.utils import capture_output, is_gradio_available | |
from .testing_utils import require_webhooks | |
if is_gradio_available(): | |
import gradio as gr | |
from fastapi.testclient import TestClient | |
import huggingface_hub._webhooks_server | |
from huggingface_hub import WebhookPayload, WebhooksServer | |
# Taken from https://huggingface.co/docs/hub/webhooks#event | |
WEBHOOK_PAYLOAD_EXAMPLE = { | |
"event": {"action": "create", "scope": "discussion"}, | |
"repo": { | |
"type": "model", | |
"name": "gpt2", | |
"id": "621ffdc036468d709f17434d", | |
"private": False, | |
"url": {"web": "https://huggingface.co/gpt2", "api": "https://huggingface.co/api/models/gpt2"}, | |
"owner": {"id": "628b753283ef59b5be89e937"}, | |
}, | |
"discussion": { | |
"id": "6399f58518721fdd27fc9ca9", | |
"title": "Update co2 emissions", | |
"url": { | |
"web": "https://huggingface.co/gpt2/discussions/19", | |
"api": "https://huggingface.co/api/models/gpt2/discussions/19", | |
}, | |
"status": "open", | |
"author": {"id": "61d2f90c3c2083e1c08af22d"}, | |
"num": 19, | |
"isPullRequest": True, | |
"changes": {"base": "refs/heads/main"}, | |
}, | |
"comment": { | |
"id": "6399f58518721fdd27fc9caa", | |
"author": {"id": "61d2f90c3c2083e1c08af22d"}, | |
"content": "Add co2 emissions information to the model card", | |
"hidden": False, | |
"url": {"web": "https://huggingface.co/gpt2/discussions/19#6399f58518721fdd27fc9caa"}, | |
}, | |
"webhook": {"id": "6390e855e30d9209411de93b", "version": 3}, | |
} | |
class TestWebhooksServerDontRun(unittest.TestCase): | |
def test_add_webhook_implicit_path(self): | |
# Test adding a webhook | |
app = WebhooksServer() | |
async def handler(): | |
pass | |
self.assertIn("/webhooks/handler", app.registered_webhooks) | |
def test_add_webhook_explicit_path(self): | |
# Test adding a webhook | |
app = WebhooksServer() | |
async def handler(): | |
pass | |
self.assertIn("/webhooks/test_webhook", app.registered_webhooks) # still registered under /webhooks | |
def test_add_webhook_twice_should_fail(self): | |
# Test adding a webhook | |
app = WebhooksServer() | |
async def test_webhook(): | |
pass | |
# Registering twice the same webhook should raise an error | |
with self.assertRaises(ValueError): | |
async def test_webhook_2(): | |
pass | |
class TestWebhooksServerRun(unittest.TestCase): | |
HEADERS_VALID_SECRET = {"x-webhook-secret": "my_webhook_secret"} | |
HEADERS_WRONG_SECRET = {"x-webhook-secret": "wrong_webhook_secret"} | |
def setUp(self) -> None: | |
with gr.Blocks() as ui: | |
gr.Markdown("Hello World!") | |
app = WebhooksServer(ui=ui, webhook_secret="my_webhook_secret") | |
# Route to check payload parsing | |
async def test_webhook(payload: WebhookPayload) -> None: | |
return {"scope": payload.event.scope} | |
# Routes to check secret validation | |
# Checks all 4 cases (async/sync, with/without request parameter) | |
async def async_with_request(request: Request) -> None: | |
return {"success": True} | |
def sync_with_request(request: Request) -> None: | |
return {"success": True} | |
async def async_no_request() -> None: | |
return {"success": True} | |
def sync_no_request() -> None: | |
return {"success": True} | |
# Route to check explicit path | |
async def with_explicit_path() -> None: | |
return {"success": True} | |
self.ui = ui | |
self.app = app | |
self.client = self.mocked_run_app() | |
def tearDown(self) -> None: | |
self.ui.server.close() | |
def mocked_run_app(self) -> "TestClient": | |
with patch.object(self.ui, "block_thread"): | |
# Run without blocking | |
with patch.object(huggingface_hub._webhooks_server, "_is_local", False): | |
# Run without tunnel | |
self.app.run() | |
return TestClient(self.app.fastapi_app) | |
def test_run_print_instructions(self): | |
"""Test that the instructions are printed when running the app.""" | |
# Test running the app | |
with capture_output() as output: | |
self.mocked_run_app() | |
instructions = output.getvalue() | |
self.assertIn("Webhooks are correctly setup and ready to use:", instructions) | |
self.assertIn("- POST http://127.0.0.1:7860/webhooks/test_webhook", instructions) | |
def test_run_parse_payload(self): | |
"""Test that the payload is correctly parsed when running the app.""" | |
response = self.client.post( | |
"/webhooks/test_webhook", headers=self.HEADERS_VALID_SECRET, json=WEBHOOK_PAYLOAD_EXAMPLE | |
) | |
self.assertEqual(response.status_code, 200) | |
self.assertEqual(response.json(), {"scope": "discussion"}) | |
def test_with_webhook_secret_should_succeed(self): | |
"""Test success if valid secret is sent.""" | |
for path in ["async_with_request", "sync_with_request", "async_no_request", "sync_no_request"]: | |
with self.subTest(path): | |
response = self.client.post(f"/webhooks/{path}", headers=self.HEADERS_VALID_SECRET) | |
self.assertEqual(response.status_code, 200) | |
self.assertEqual(response.json(), {"success": True}) | |
def test_no_webhook_secret_should_be_unauthorized(self): | |
"""Test failure if valid secret is sent.""" | |
for path in ["async_with_request", "sync_with_request", "async_no_request", "sync_no_request"]: | |
with self.subTest(path): | |
response = self.client.post(f"/webhooks/{path}") | |
self.assertEqual(response.status_code, 401) | |
def test_wrong_webhook_secret_should_be_forbidden(self): | |
"""Test failure if valid secret is sent.""" | |
for path in ["async_with_request", "sync_with_request", "async_no_request", "sync_no_request"]: | |
with self.subTest(path): | |
response = self.client.post(f"/webhooks/{path}", headers=self.HEADERS_WRONG_SECRET) | |
self.assertEqual(response.status_code, 403) | |
def test_route_with_explicit_path(self): | |
"""Test that the route with an explicit path is correctly registered.""" | |
response = self.client.post("/webhooks/explicit_path", headers=self.HEADERS_VALID_SECRET) | |
self.assertEqual(response.status_code, 200) | |