1#-----------------------------------------------------------------------------
2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors.
3# All rights reserved.
4#
5# The full license is in the file LICENSE.txt, distributed with this software.
6#-----------------------------------------------------------------------------
7
8#-----------------------------------------------------------------------------
9# Boilerplate
10#-----------------------------------------------------------------------------
11import pytest ; pytest
12
13#-----------------------------------------------------------------------------
14# Imports
15#-----------------------------------------------------------------------------
16
17# Standard library imports
18from types import ModuleType
19
20# External imports
21from tornado.web import RequestHandler
22
23# Bokeh imports
24from bokeh._testing.util.api import verify_all
25from bokeh._testing.util.filesystem import with_file_contents, with_file_contents_async
26
27# Module under test
28import bokeh.server.auth_provider as bsa # isort:skip
29
30#-----------------------------------------------------------------------------
31# Setup
32#-----------------------------------------------------------------------------
33
34ALL = (
35    'AuthModule',
36    'AuthProvider',
37    'NullAuth'
38)
39
40@pytest.fixture
41def null_auth():
42    return bsa.NullAuth()
43
44#-----------------------------------------------------------------------------
45# General API
46#-----------------------------------------------------------------------------
47
48Test___all__ = verify_all(bsa, ALL)
49
50class TestNullAuth:
51    def test_endpoints(self, null_auth) -> None:
52        assert null_auth.endpoints == []
53
54    def test_get_user(self, null_auth) -> None:
55        assert null_auth.get_user == None
56
57    async def test_get_user_async(self, null_auth) -> None:
58        assert null_auth.get_user_async == None
59
60    def test_login_url(self, null_auth) -> None:
61        assert null_auth.login_url == None
62
63    def test_get_login_url(self, null_auth) -> None:
64        assert null_auth.get_login_url == None
65
66    def test_login_handler(self, null_auth) -> None:
67        assert null_auth.login_handler == None
68
69    def test_logout_url(self, null_auth) -> None:
70        assert null_auth.logout_url == None
71
72    def test_logout_handler(self, null_auth) -> None:
73        assert null_auth.logout_handler == None
74
75class TestAuthModule_properties:
76    def test_no_endpoints(self) -> None:
77        def func(filename):
78            am = bsa.AuthModule(filename)
79            assert am.endpoints == []
80
81        with_file_contents("""
82def get_user(): pass
83def get_login_url(): pass
84        """, func, suffix='.py')
85
86        with_file_contents("""
87def get_user(): pass
88login_url = "/foo"
89        """, func, suffix='.py')
90
91    def test_login_url_endpoint(self) -> None:
92        def func(filename):
93            am = bsa.AuthModule(filename)
94            assert am.endpoints[0][0] == '/foo'
95            assert issubclass(am.endpoints[0][1], RequestHandler)
96        with_file_contents("""
97from tornado.web import RequestHandler
98def get_user(): pass
99login_url = "/foo"
100class LoginHandler(RequestHandler): pass
101        """, func, suffix='.py')
102
103    def test_logout_url_endpoint(self) -> None:
104        def func(filename):
105            am = bsa.AuthModule(filename)
106            assert am.endpoints[0][0] == '/bar'
107            assert issubclass(am.endpoints[0][1], RequestHandler)
108        with_file_contents("""
109from tornado.web import RequestHandler
110def get_user(): pass
111login_url = "/foo"
112logout_url = "/bar"
113class LogoutHandler(RequestHandler): pass
114        """, func, suffix='.py')
115
116    def test_login_logout_url_endpoint(self) -> None:
117        def func(filename):
118            am = bsa.AuthModule(filename)
119            endpoints = sorted(am.endpoints)
120            assert endpoints[0][0] == '/bar'
121            assert issubclass(endpoints[0][1], RequestHandler)
122            assert endpoints[1][0] == '/foo'
123            assert issubclass(endpoints[1][1], RequestHandler)
124        with_file_contents("""
125def get_user(): pass
126login_url = "/foo"
127from tornado.web import RequestHandler
128class LoginHandler(RequestHandler): pass
129logout_url = "/bar"
130from tornado.web import RequestHandler
131class LogoutHandler(RequestHandler): pass
132        """, func, suffix='.py')
133
134    def test_get_user(self) -> None:
135        def func(filename):
136            am = bsa.AuthModule(filename)
137            assert am.get_user is not None
138            assert am.get_user('handler') == 10
139
140        with_file_contents("""
141def get_user(handler): return 10
142login_url = "/foo"
143        """, func, suffix='.py')
144
145    async def test_get_user_async(self) -> None:
146        async def func(filename):
147            am = bsa.AuthModule(filename)
148            assert am.get_user_async is not None
149            assert await am.get_user_async('handler') == 10
150
151        await with_file_contents_async("""
152async def get_user_async(handler): return 10
153login_url = "/foo"
154        """, func, suffix='.py')
155
156
157    def test_login_url(self) -> None:
158        def func(filename):
159            am = bsa.AuthModule(filename)
160            assert am.login_url == "/foo"
161            assert am.get_login_url is None
162            assert am.login_handler is None
163            assert am.logout_url is None
164            assert am.logout_handler is None
165
166        with_file_contents("""
167def get_user(handler): return 10
168login_url = "/foo"
169        """, func, suffix='.py')
170
171    def test_get_login_url(self) -> None:
172        def func(filename):
173            am = bsa.AuthModule(filename)
174            assert am.login_url is None
175            assert am.get_login_url('handler') == 20
176            assert am.login_handler is None
177            assert am.logout_url is None
178            assert am.logout_handler is None
179
180        with_file_contents("""
181def get_user(handler): return 10
182def get_login_url(handler): return 20
183        """, func, suffix='.py')
184
185    def test_login_handler(self) -> None:
186        def func(filename):
187            am = bsa.AuthModule(filename)
188            assert am.login_url == "/foo"
189            assert am.get_login_url is None
190            assert issubclass(am.login_handler, RequestHandler)
191            assert am.logout_url is None
192            assert am.logout_handler is None
193
194        with_file_contents("""
195def get_user(handler): return 10
196login_url = "/foo"
197from tornado.web import RequestHandler
198class LoginHandler(RequestHandler): pass
199        """, func, suffix='.py')
200
201    def test_logout_url(self) -> None:
202        def func(filename):
203            am = bsa.AuthModule(filename)
204            assert am.login_url == "/foo"
205            assert am.get_login_url is None
206            assert am.login_handler is None
207            assert am.logout_url == "/bar"
208            assert am.logout_handler is None
209
210        with_file_contents("""
211def get_user(handler): return 10
212login_url = "/foo"
213logout_url = "/bar"
214        """, func, suffix='.py')
215
216    def test_logout_handler(self) -> None:
217        def func(filename):
218            am = bsa.AuthModule(filename)
219            assert am.login_url == "/foo"
220            assert am.get_login_url is None
221            assert am.login_handler is None
222            assert am.logout_url == "/bar"
223            assert issubclass(am.logout_handler, RequestHandler)
224
225        with_file_contents("""
226def get_user(handler): return 10
227login_url = "/foo"
228logout_url = "/bar"
229from tornado.web import RequestHandler
230class LogoutHandler(RequestHandler): pass
231    """, func, suffix='.py')
232
233
234class TestAuthModule_validation:
235    def test_no_file(self) -> None:
236        with pytest.raises(ValueError) as e:
237            bsa.AuthModule("junkjunkjunk")
238            assert str(e).startswith("no file exists at module_path:")
239
240    def test_both_user(self) -> None:
241        def func(filename):
242            with pytest.raises(ValueError) as e:
243                bsa.AuthModule(filename)
244                assert str(e) == "Only one of get_user or get_user_async should be supplied"
245
246        with_file_contents("""
247def get_user(handler): return 10
248async def get_user_async(handler): return 20
249    """, func, suffix='.py')
250
251    @pytest.mark.parametrize('user_func', ['get_user', 'get_user_async'])
252    def test_no_login(self, user_func) -> None:
253        def func(filename):
254            with pytest.raises(ValueError) as e:
255                bsa.AuthModule(filename)
256                assert str(e) == "When user authentication is enabled, one of login_url or get_login_url must be supplied"
257
258        with_file_contents("""
259def %s(handler): return 10
260    """ % user_func, func, suffix='.py')
261
262    def test_both_login(self) -> None:
263        def func(filename):
264            with pytest.raises(ValueError) as e:
265                bsa.AuthModule(filename)
266                assert str(e) == "At most one of login_url or get_login_url should be supplied"
267
268        with_file_contents("""
269def get_user(handler): return 10
270def get_login_url(handler): return 20
271login_url = "/foo"
272    """, func, suffix='.py')
273
274    def test_handler_with_get_login_url(self) -> None:
275        def func(filename):
276            with pytest.raises(ValueError) as e:
277                bsa.AuthModule(filename)
278                assert str(e) == "LoginHandler cannot be used with a get_login_url() function"
279
280        with_file_contents("""
281def get_user(handler): return 10
282def get_login_url(handler): return 20
283from tornado.web import RequestHandler
284class LoginHandler(RequestHandler): pass
285    """, func, suffix='.py')
286
287    def test_login_handler_wrong_type(self) -> None:
288        def func(filename):
289            with pytest.raises(ValueError) as e:
290                bsa.AuthModule(filename)
291                assert str(e) == "LoginHandler must be a Tornado RequestHandler"
292
293        with_file_contents("""
294def get_user(handler): return 10
295login_url = "/foo"
296class LoginHandler(object): pass
297    """, func, suffix='.py')
298
299    @pytest.mark.parametrize('login_url', ['http://foo.com', 'https://foo.com', '//foo.com'])
300    def test_login_handler_wrong_url(self, login_url) -> None:
301        def func(filename):
302            with pytest.raises(ValueError) as e:
303                bsa.AuthModule(filename)
304                assert str(e) == "LoginHandler can only be used with a relative login_url"
305
306        with_file_contents("""
307def get_user(handler): return 10
308login_url = %r
309    """ % login_url, func, suffix='.py')
310
311    def test_logout_handler_wrong_type(self) -> None:
312        def func(filename):
313            with pytest.raises(ValueError) as e:
314                bsa.AuthModule(filename)
315                assert str(e) == "LoginHandler must be a Tornado RequestHandler"
316
317        with_file_contents("""
318def get_user(handler): return 10
319login_url = "/foo"
320class LogoutHandler(object): pass
321    """, func, suffix='.py')
322
323    @pytest.mark.parametrize('logout_url', ['http://foo.com', 'https://foo.com', '//foo.com'])
324    def test_logout_handler_wrong_url(self, logout_url) -> None:
325        def func(filename):
326            with pytest.raises(ValueError) as e:
327                bsa.AuthModule(filename)
328                assert str(e) == "LoginHandler can only be used with a relative login_url"
329
330        with_file_contents("""
331def get_user(handler): return 10
332logout_url = %r
333    """ % logout_url, func, suffix='.py')
334
335_source = """
336def get_login_url():
337    pass
338
339logout_url = "foo"
340
341class LoginHandler(object):
342    pass
343"""
344
345def test_load_auth_module() -> None:
346    def func(filename):
347        m =  bsa.load_auth_module(filename)
348        assert isinstance(m, ModuleType)
349        assert [x for x in sorted(dir(m)) if not x.startswith("__")] == ['LoginHandler', 'get_login_url', 'logout_url']
350    with_file_contents(_source, func, suffix='.py')
351
352def test_probably_relative_url() -> None:
353    assert bsa.probably_relative_url("httpabc")
354    assert bsa.probably_relative_url("httpsabc")
355    assert bsa.probably_relative_url("/abc")
356    assert not bsa.probably_relative_url("http://abc")
357    assert not bsa.probably_relative_url("https://abc")
358    assert not bsa.probably_relative_url("//abc")
359
360#-----------------------------------------------------------------------------
361# Dev API
362#-----------------------------------------------------------------------------
363
364#-----------------------------------------------------------------------------
365# Private API
366#-----------------------------------------------------------------------------
367
368#-----------------------------------------------------------------------------
369# Code
370#-----------------------------------------------------------------------------
371