Thebull commited on
Commit
0fea559
·
1 Parent(s): f7349dc

Upload test_webhooks_server.py

Browse files
Files changed (1) hide show
  1. test_webhooks_server.py +188 -0
test_webhooks_server.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from unittest.mock import patch
3
+
4
+ from fastapi import Request
5
+
6
+ from huggingface_hub.utils import capture_output, is_gradio_available
7
+
8
+ from .testing_utils import require_webhooks
9
+
10
+
11
+ if is_gradio_available():
12
+ import gradio as gr
13
+ from fastapi.testclient import TestClient
14
+
15
+ import huggingface_hub._webhooks_server
16
+ from huggingface_hub import WebhookPayload, WebhooksServer
17
+
18
+
19
+ # Taken from https://huggingface.co/docs/hub/webhooks#event
20
+ WEBHOOK_PAYLOAD_EXAMPLE = {
21
+ "event": {"action": "create", "scope": "discussion"},
22
+ "repo": {
23
+ "type": "model",
24
+ "name": "gpt2",
25
+ "id": "621ffdc036468d709f17434d",
26
+ "private": False,
27
+ "url": {"web": "https://huggingface.co/gpt2", "api": "https://huggingface.co/api/models/gpt2"},
28
+ "owner": {"id": "628b753283ef59b5be89e937"},
29
+ },
30
+ "discussion": {
31
+ "id": "6399f58518721fdd27fc9ca9",
32
+ "title": "Update co2 emissions",
33
+ "url": {
34
+ "web": "https://huggingface.co/gpt2/discussions/19",
35
+ "api": "https://huggingface.co/api/models/gpt2/discussions/19",
36
+ },
37
+ "status": "open",
38
+ "author": {"id": "61d2f90c3c2083e1c08af22d"},
39
+ "num": 19,
40
+ "isPullRequest": True,
41
+ "changes": {"base": "refs/heads/main"},
42
+ },
43
+ "comment": {
44
+ "id": "6399f58518721fdd27fc9caa",
45
+ "author": {"id": "61d2f90c3c2083e1c08af22d"},
46
+ "content": "Add co2 emissions information to the model card",
47
+ "hidden": False,
48
+ "url": {"web": "https://huggingface.co/gpt2/discussions/19#6399f58518721fdd27fc9caa"},
49
+ },
50
+ "webhook": {"id": "6390e855e30d9209411de93b", "version": 3},
51
+ }
52
+
53
+
54
+ @require_webhooks
55
+ class TestWebhooksServerDontRun(unittest.TestCase):
56
+ def test_add_webhook_implicit_path(self):
57
+ # Test adding a webhook
58
+ app = WebhooksServer()
59
+
60
+ @app.add_webhook
61
+ async def handler():
62
+ pass
63
+
64
+ self.assertIn("/webhooks/handler", app.registered_webhooks)
65
+
66
+ def test_add_webhook_explicit_path(self):
67
+ # Test adding a webhook
68
+ app = WebhooksServer()
69
+
70
+ @app.add_webhook(path="/test_webhook")
71
+ async def handler():
72
+ pass
73
+
74
+ self.assertIn("/webhooks/test_webhook", app.registered_webhooks) # still registered under /webhooks
75
+
76
+ def test_add_webhook_twice_should_fail(self):
77
+ # Test adding a webhook
78
+ app = WebhooksServer()
79
+
80
+ @app.add_webhook("my_webhook")
81
+ async def test_webhook():
82
+ pass
83
+
84
+ # Registering twice the same webhook should raise an error
85
+ with self.assertRaises(ValueError):
86
+
87
+ @app.add_webhook("my_webhook")
88
+ async def test_webhook_2():
89
+ pass
90
+
91
+
92
+ @require_webhooks
93
+ class TestWebhooksServerRun(unittest.TestCase):
94
+ HEADERS_VALID_SECRET = {"x-webhook-secret": "my_webhook_secret"}
95
+ HEADERS_WRONG_SECRET = {"x-webhook-secret": "wrong_webhook_secret"}
96
+
97
+ def setUp(self) -> None:
98
+ with gr.Blocks() as ui:
99
+ gr.Markdown("Hello World!")
100
+ app = WebhooksServer(ui=ui, webhook_secret="my_webhook_secret")
101
+
102
+ # Route to check payload parsing
103
+ @app.add_webhook
104
+ async def test_webhook(payload: WebhookPayload) -> None:
105
+ return {"scope": payload.event.scope}
106
+
107
+ # Routes to check secret validation
108
+ # Checks all 4 cases (async/sync, with/without request parameter)
109
+ @app.add_webhook
110
+ async def async_with_request(request: Request) -> None:
111
+ return {"success": True}
112
+
113
+ @app.add_webhook
114
+ def sync_with_request(request: Request) -> None:
115
+ return {"success": True}
116
+
117
+ @app.add_webhook
118
+ async def async_no_request() -> None:
119
+ return {"success": True}
120
+
121
+ @app.add_webhook
122
+ def sync_no_request() -> None:
123
+ return {"success": True}
124
+
125
+ # Route to check explicit path
126
+ @app.add_webhook(path="/explicit_path")
127
+ async def with_explicit_path() -> None:
128
+ return {"success": True}
129
+
130
+ self.ui = ui
131
+ self.app = app
132
+ self.client = self.mocked_run_app()
133
+
134
+ def tearDown(self) -> None:
135
+ self.ui.server.close()
136
+
137
+ def mocked_run_app(self) -> "TestClient":
138
+ with patch.object(self.ui, "block_thread"):
139
+ # Run without blocking
140
+ with patch.object(huggingface_hub._webhooks_server, "_is_local", False):
141
+ # Run without tunnel
142
+ self.app.run()
143
+ return TestClient(self.app.fastapi_app)
144
+
145
+ def test_run_print_instructions(self):
146
+ """Test that the instructions are printed when running the app."""
147
+ # Test running the app
148
+ with capture_output() as output:
149
+ self.mocked_run_app()
150
+
151
+ instructions = output.getvalue()
152
+ self.assertIn("Webhooks are correctly setup and ready to use:", instructions)
153
+ self.assertIn("- POST http://127.0.0.1:7860/webhooks/test_webhook", instructions)
154
+
155
+ def test_run_parse_payload(self):
156
+ """Test that the payload is correctly parsed when running the app."""
157
+ response = self.client.post(
158
+ "/webhooks/test_webhook", headers=self.HEADERS_VALID_SECRET, json=WEBHOOK_PAYLOAD_EXAMPLE
159
+ )
160
+ self.assertEqual(response.status_code, 200)
161
+ self.assertEqual(response.json(), {"scope": "discussion"})
162
+
163
+ def test_with_webhook_secret_should_succeed(self):
164
+ """Test success if valid secret is sent."""
165
+ for path in ["async_with_request", "sync_with_request", "async_no_request", "sync_no_request"]:
166
+ with self.subTest(path):
167
+ response = self.client.post(f"/webhooks/{path}", headers=self.HEADERS_VALID_SECRET)
168
+ self.assertEqual(response.status_code, 200)
169
+ self.assertEqual(response.json(), {"success": True})
170
+
171
+ def test_no_webhook_secret_should_be_unauthorized(self):
172
+ """Test failure if valid secret is sent."""
173
+ for path in ["async_with_request", "sync_with_request", "async_no_request", "sync_no_request"]:
174
+ with self.subTest(path):
175
+ response = self.client.post(f"/webhooks/{path}")
176
+ self.assertEqual(response.status_code, 401)
177
+
178
+ def test_wrong_webhook_secret_should_be_forbidden(self):
179
+ """Test failure if valid secret is sent."""
180
+ for path in ["async_with_request", "sync_with_request", "async_no_request", "sync_no_request"]:
181
+ with self.subTest(path):
182
+ response = self.client.post(f"/webhooks/{path}", headers=self.HEADERS_WRONG_SECRET)
183
+ self.assertEqual(response.status_code, 403)
184
+
185
+ def test_route_with_explicit_path(self):
186
+ """Test that the route with an explicit path is correctly registered."""
187
+ response = self.client.post("/webhooks/explicit_path", headers=self.HEADERS_VALID_SECRET)
188
+ self.assertEqual(response.status_code, 200)