@@ -31,6 +31,26 @@ def get_client():
3131 return patch .object (df , 'get_http_client' , get_client )
3232
3333
34+ def mock_http_client (test_func ):
35+ """
36+ Replace the diffing server's HTTP client with an instance of
37+ ``MockAsyncHttpClient``. On the resulting mock, call `respond_to()` to set
38+ responses for various URLs:
39+
40+ >>> @mock_http_client
41+ >>> def test_something(client):
42+ >>> client.respond_to(r'/a', body='hello')
43+ >>> client.respond_to(r'/b', code=500, body='goodbye')
44+ >>> self.fetch('/html_source_dmp?a=http://ex.com/a&b=http://ex.com/b')
45+ """
46+ def new_test_func (* args ):
47+ mock = MockAsyncHttpClient ()
48+ with patch .object (df , 'get_http_client' , return_value = mock ):
49+ return test_func (* args , mock )
50+
51+ return new_test_func
52+
53+
3454class DiffingServerTestCase (AsyncHTTPTestCase ):
3555
3656 def get_app (self ):
@@ -54,8 +74,10 @@ def test_version(self):
5474
5575class DiffingServerLocalHandlingTest (DiffingServerTestCase ):
5676
57- def test_one_local (self ):
77+ @mock_http_client
78+ def test_one_local (self , client ):
5879 with tempfile .NamedTemporaryFile () as a :
80+ client .respond_to (r'example.org' , body = 'Whatever' )
5981 response = self .fetch ('/identical_bytes?'
6082 f'a=file://{ a .name } &b=https://example.org' )
6183 self .assertEqual (response .code , 200 )
@@ -100,89 +122,99 @@ def test_healthcheck(self):
100122
101123class DiffingServerFetchTest (DiffingServerTestCase ):
102124
103- def test_pass_headers (self ):
104- mock = MockAsyncHttpClient ()
105- with patch .object (df , 'get_http_client' , return_value = mock ):
106- mock .respond_to (r'/a$' )
107- mock .respond_to (r'/b$' )
125+ @mock_http_client
126+ def test_pass_headers (self , client ):
127+ client .respond_to (r'/a$' )
128+ client .respond_to (r'/b$' )
108129
109- self .fetch ('/html_source_dmp?'
110- 'pass_headers=Authorization,%20User-Agent&'
111- 'a=https://example.org/a&b=https://example.org/b' ,
112- headers = {'User-Agent' : 'Some Agent' ,
113- 'Authorization' : 'Bearer xyz' ,
114- 'Accept' : 'application/json' })
130+ self .fetch ('/html_source_dmp?'
131+ 'pass_headers=Authorization,%20User-Agent&'
132+ 'a=https://example.org/a&b=https://example.org/b' ,
133+ headers = {'User-Agent' : 'Some Agent' ,
134+ 'Authorization' : 'Bearer xyz' ,
135+ 'Accept' : 'application/json' })
115136
116- a_headers = mock .requests ['https://example.org/a' ].headers
117- assert a_headers .get ('User-Agent' ) == 'Some Agent'
118- assert a_headers .get ('Authorization' ) == 'Bearer xyz'
119- assert a_headers .get ('Accept' ) != 'application/json'
137+ a_headers = client .requests ['https://example.org/a' ].headers
138+ assert a_headers .get ('User-Agent' ) == 'Some Agent'
139+ assert a_headers .get ('Authorization' ) == 'Bearer xyz'
140+ assert a_headers .get ('Accept' ) != 'application/json'
120141
121- b_headers = mock .requests ['https://example.org/b' ].headers
122- assert b_headers .get ('User-Agent' ) == 'Some Agent'
123- assert b_headers .get ('Authorization' ) == 'Bearer xyz'
124- assert b_headers .get ('Accept' ) != 'application/json'
142+ b_headers = client .requests ['https://example.org/b' ].headers
143+ assert b_headers .get ('User-Agent' ) == 'Some Agent'
144+ assert b_headers .get ('Authorization' ) == 'Bearer xyz'
145+ assert b_headers .get ('Accept' ) != 'application/json'
125146
126147
127148class DiffingServerExceptionHandlingTest (DiffingServerTestCase ):
128149
129- def test_local_file_disallowed_in_production (self ):
130- original = os .environ .get ('WEB_MONITORING_APP_ENV' )
131- os .environ ['WEB_MONITORING_APP_ENV' ] = 'production'
132- try :
133- with tempfile .NamedTemporaryFile () as a :
134- response = self .fetch ('/identical_bytes?'
135- f'a=file://{ a .name } &b=https://example.org' )
136- self .assertEqual (response .code , 403 )
137- finally :
138- if original is None :
139- del os .environ ['WEB_MONITORING_APP_ENV' ]
140- else :
141- os .environ ['WEB_MONITORING_APP_ENV' ] = original
150+ @patch .dict (os .environ , {'WEB_MONITORING_APP_ENV' : 'production' })
151+ @mock_http_client
152+ def test_local_file_disallowed_in_production (self , client ):
153+ client .respond_to (r'example\.org' , body = 'Whatever' )
154+ with tempfile .NamedTemporaryFile () as a :
155+ response = self .fetch ('/identical_bytes?'
156+ f'a=file://{ a .name } &b=https://example.org' )
157+ self .assertEqual (response .code , 403 )
142158
143- def test_invalid_url_a_format (self ):
159+ @mock_http_client
160+ def test_invalid_url_a_format (self , client ):
161+ client .respond_to (r'example\.org' , body = 'Whatever' )
144162 response = self .fetch ('/html_token?format=json&include=all&'
145163 'a=example.org&b=https://example.org' )
146164 self .json_check (response )
147165 self .assertEqual (response .code , 400 )
148166 self .assertFalse (response .headers .get ('Etag' ))
149167
150- def test_invalid_url_b_format (self ):
168+ @mock_http_client
169+ def test_invalid_url_b_format (self , client ):
170+ client .respond_to (r'example\.org' , body = 'Whatever' )
151171 response = self .fetch ('/html_token?format=json&include=all&'
152172 'a=https://example.org&b=example.org' )
153173 self .json_check (response )
154174 self .assertEqual (response .code , 400 )
155175 self .assertFalse (response .headers .get ('Etag' ))
156176
157- def test_invalid_diffing_method (self ):
177+ @mock_http_client
178+ def test_invalid_diffing_method (self , client ):
179+ client .respond_to (r'example\.org' , body = 'Whatever' )
158180 response = self .fetch ('/non_existing?format=json&include=all&'
159181 'a=example.org&b=https://example.org' )
160182 self .json_check (response )
161183 self .assertEqual (response .code , 404 )
162184 self .assertFalse (response .headers .get ('Etag' ))
163185
164- def test_missing_url_a (self ):
186+ @mock_http_client
187+ def test_missing_url_a (self , client ):
188+ client .respond_to (r'example\.org' , body = 'Whatever' )
165189 response = self .fetch ('/html_token?format=json&include=all&'
166190 'b=https://example.org' )
167191 self .json_check (response )
168192 self .assertEqual (response .code , 400 )
169193 self .assertFalse (response .headers .get ('Etag' ))
170194
171- def test_missing_url_b (self ):
195+ @mock_http_client
196+ def test_missing_url_b (self , client ):
197+ client .respond_to (r'example\.org' , body = 'Whatever' )
172198 response = self .fetch ('/html_token?format=json&include=all&'
173199 'a=https://example.org' )
174200 self .json_check (response )
175201 self .assertEqual (response .code , 400 )
176202 self .assertFalse (response .headers .get ('Etag' ))
177203
178- def test_not_reachable_url_a (self ):
204+ @mock_http_client
205+ def test_not_reachable_url_a (self , client ):
206+ client .respond_to (r'/eeexample\.org' , error = OSError ('Connection error' ))
207+ client .respond_to (r'/example\.org' , body = 'Whatever' )
179208 response = self .fetch ('/html_token?format=json&include=all&'
180209 'a=https://eeexample.org&b=https://example.org' )
181210 self .json_check (response )
182211 self .assertEqual (response .code , 502 )
183212 self .assertFalse (response .headers .get ('Etag' ))
184213
185- def test_not_reachable_url_b (self ):
214+ @mock_http_client
215+ def test_not_reachable_url_b (self , client ):
216+ client .respond_to (r'/eeexample\.org' , error = OSError ('Connection error' ))
217+ client .respond_to (r'/example\.org' , body = 'Whatever' )
186218 response = self .fetch ('/html_token?format=json&include=all&'
187219 'a=https://example.org&b=https://eeexample.org' )
188220 self .json_check (response )
@@ -202,45 +234,50 @@ async def responder(handler):
202234 assert response .code == 504
203235
204236 def test_missing_params_caller_func (self ):
205- response = self . fetch ('http://example.org/' )
237+ response = df . MockResponse ('http://example.org/' , 'Whatever ' )
206238 with self .assertRaises (KeyError ):
207239 df .caller (mock_diffing_method , response , response )
208240
209- def test_a_is_404 (self ):
241+ @mock_http_client
242+ def test_a_is_404 (self , client ):
243+ client .respond_to (r'/404' , code = 404 , body = 'Error 404' )
244+ client .respond_to (r'/success' )
245+
210246 response = self .fetch ('/html_token?format=json&include=all'
211247 '&a=http://httpstat.us/404'
212- '&b=https://example.org' )
248+ '&b=https://example.org/success ' )
213249 # The error is upstream, but the message should indicate it was a 404.
214250 self .assertEqual (response .code , 502 )
215251 assert '404' in json .loads (response .body )['error' ]
216252 self .assertFalse (response .headers .get ('Etag' ))
217253 self .json_check (response )
218254
219- def test_accepts_errors_from_web_archives (self ):
255+ @mock_http_client
256+ def test_accepts_errors_from_web_archives (self , client ):
220257 """
221258 If a page has HTTP status != 2xx but comes from a web archive,
222259 we proceed with diffing.
223260 """
224- mock = MockAsyncHttpClient ()
225- with patch .object (df , 'get_http_client' , return_value = mock ):
226- mock .respond_to (r'/error$' , code = 404 , headers = {'Memento-Datetime' : 'Tue Sep 25 2018 03:38:50' })
227- mock .respond_to (r'/success$' )
261+ client .respond_to (r'/error$' , code = 404 , headers = {'Memento-Datetime' : 'Tue Sep 25 2018 03:38:50' })
262+ client .respond_to (r'/success$' )
228263
229- response = self .fetch ('/html_token?format=json&include=all'
230- '&a=https://archive.org/20180925033850/http://httpstat.us/error'
231- '&b=https://example.org/success' )
264+ response = self .fetch ('/html_token?format=json&include=all'
265+ '&a=https://archive.org/20180925033850/http://httpstat.us/error'
266+ '&b=https://example.org/success' )
232267
233- self .assertEqual (response .code , 200 )
234- assert 'change_count' in json .loads (response .body )
268+ self .assertEqual (response .code , 200 )
269+ assert 'change_count' in json .loads (response .body )
235270
236271 @patch ('web_monitoring_diff.server.server.access_control_allow_origin_header' , '*' )
237- def test_check_cors_headers (self ):
272+ @mock_http_client
273+ def test_check_cors_headers (self , client ):
238274 """
239275 Since we have set Access-Control-Allow-Origin: * on app init,
240276 the response should have a list of HTTP headers required by CORS.
241277 Access-Control-Allow-Origin value equals request Origin header because
242278 we use setting `access_control_allow_origin_header='*'`.
243279 """
280+ client .respond_to (r'example\.org' , body = 'Whatever' )
244281 response = self .fetch ('/html_token?format=json&include=all'
245282 '&a=https://example.org&b=https://example.org' ,
246283 headers = {'Accept' : 'application/json' ,
@@ -252,14 +289,16 @@ def test_check_cors_headers(self):
252289
253290 @patch ('web_monitoring_diff.server.server.access_control_allow_origin_header' ,
254291 'http://one.com,http://two.com,http://three.com' )
255- def test_cors_origin_header (self ):
292+ @mock_http_client
293+ def test_cors_origin_header (self , client ):
256294 """
257295 The allowed origins is a list of URLs. If the request has HTTP
258296 header `Origin` as one of them, the response `Access-Control-Allow-Origin`
259297 should have the same value. If not, there shouldn't be any such header
260298 at all.
261299 This is necessary for CORS requests with credentials to work properly.
262300 """
301+ client .respond_to (r'example\.org' , body = 'Whatever' )
263302 response = self .fetch ('/html_token?format=json&include=all'
264303 '&a=https://example.org&b=https://example.org' ,
265304 headers = {'Accept' : 'application/json' ,
@@ -547,7 +586,7 @@ def __init__(self):
547586 self .requests = {}
548587 self .stub_responses = []
549588
550- def respond_to (self , matcher , code = 200 , body = '' , headers = {}, ** kwargs ):
589+ def respond_to (self , matcher , code = 200 , body = '' , headers = {}, error = None , ** kwargs ):
551590 """
552591 Set up a fake HTTP response. If a request is made and no fake response
553592 set up with `respond_to()` matches it, an error will be raised.
@@ -566,6 +605,8 @@ def respond_to(self, matcher, code=200, body='', headers={}, **kwargs):
566605 The response body to send back.
567606 headers : dict, optional
568607 Any headers to use for the response.
608+ error : Exception, optional
609+ If set, raise an exception instead of returning a mock response.
569610 **kwargs : any, optional
570611 Additional keyword args to pass to the Tornado Response.
571612 Reference: http://www.tornadoweb.org/en/stable/httpclient.html#tornado.httpclient.HTTPResponse
@@ -582,11 +623,16 @@ def respond_to(self, matcher, code=200, body='', headers={}, **kwargs):
582623 'code' : code ,
583624 'body' : body ,
584625 'headers' : headers ,
585- 'extra' : kwargs
626+ 'extra' : kwargs ,
627+ 'error' : error
586628 })
587629
588630 def fetch_impl (self , request , callback ):
589631 stub = self ._find_stub (request )
632+ if stub ['error' ]:
633+ callback (HTTPResponse (request , 600 , error = stub ['error' ]))
634+ return
635+
590636 buffer = BytesIO (utf8 (stub ['body' ]))
591637 headers = HTTPHeaders (stub ['headers' ])
592638 response = HTTPResponse (request , stub ['code' ], buffer = buffer ,
0 commit comments