Coverage for tests/test_http.py: 16%
407 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-09 03:06 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-09 03:06 -0800
1# This file is part of lsst-resources.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12import hashlib
13import importlib
14import io
15import os.path
16import random
17import shutil
18import socket
19import stat
20import string
21import tempfile
22import time
23import unittest
24from threading import Thread
25from typing import Callable, Tuple, cast
27try:
28 from cheroot import wsgi
29 from wsgidav.wsgidav_app import WsgiDAVApp
30except ImportError:
31 WsgiDAVApp = None
33import lsst.resources
34import requests
35import responses
36from lsst.resources import ResourcePath
37from lsst.resources._resourceHandles._httpResourceHandle import HttpReadResourceHandle
38from lsst.resources.http import BearerTokenAuth, SessionStore, _is_protected, _is_webdav_endpoint
39from lsst.resources.tests import GenericReadWriteTestCase, GenericTestCase
40from lsst.resources.utils import makeTestTempDir, removeTestTempDir
42TESTDIR = os.path.abspath(os.path.dirname(__file__))
45class GenericHttpTestCase(GenericTestCase, unittest.TestCase):
46 scheme = "http"
47 netloc = "server.example"
50class HttpReadWriteWebdavTestCase(GenericReadWriteTestCase, unittest.TestCase):
51 """Test with a real webDAV server, as opposed to mocking responses."""
53 scheme = "http"
55 @classmethod
56 def setUpClass(cls):
57 cls.webdav_tmpdir = tempfile.mkdtemp(prefix="webdav-server-test-")
58 cls.local_files_to_remove = []
59 cls.server_thread = None
61 # Should we test against a running server?
62 #
63 # This is convenient for testing against real servers in the
64 # developer environment by initializing the environment variable
65 # LSST_RESOURCES_HTTP_TEST_SERVER_URL with the URL of the server, e.g.
66 # https://dav.example.org:1234/path/to/top/dir
67 if (test_endpoint := os.getenv("LSST_RESOURCES_HTTP_TEST_SERVER_URL")) is not None:
68 # Run this test case against the specified server.
69 uri = ResourcePath(test_endpoint)
70 cls.scheme = uri.scheme
71 cls.netloc = uri.netloc
72 cls.base_path = uri.path
73 elif WsgiDAVApp is not None:
74 # WsgiDAVApp is available, launch a local server in its own
75 # thread to expose a local temporary directory and run this
76 # test case against it.
77 cls.port_number = cls._get_port_number()
78 cls.stop_webdav_server = False
79 cls.server_thread = Thread(
80 target=cls._serve_webdav,
81 args=(cls, cls.webdav_tmpdir, cls.port_number, lambda: cls.stop_webdav_server),
82 daemon=True,
83 )
84 cls.server_thread.start()
86 # Wait for it to start
87 time.sleep(1)
89 # Initialize the server endpoint
90 cls.netloc = f"127.0.0.1:{cls.port_number}"
91 else:
92 cls.skipTest(
93 cls,
94 "neither WsgiDAVApp is available nor a webDAV test endpoint is configured to test against",
95 )
97 @classmethod
98 def tearDownClass(cls):
99 # Stop the WsgiDAVApp server, if any
100 if WsgiDAVApp is not None:
101 # Shut down of the webdav server and wait for the thread to exit
102 cls.stop_webdav_server = True
103 if cls.server_thread is not None:
104 cls.server_thread.join()
106 # Remove local temporary files
107 for file in cls.local_files_to_remove:
108 if os.path.exists(file):
109 os.remove(file)
111 # Remove temp dir
112 if cls.webdav_tmpdir:
113 shutil.rmtree(cls.webdav_tmpdir, ignore_errors=True)
115 def tearDown(self):
116 if self.tmpdir:
117 self.tmpdir.remove()
119 super().tearDown()
121 def test_dav_file_handle(self):
122 # Upload a new file with known contents.
123 contents = "These are some \n bytes to read"
124 remote_file = self.tmpdir.join(self._get_file_name())
125 self.assertIsNone(remote_file.write(data=contents, overwrite=True))
127 # Test that the correct handle is returned.
128 with remote_file.open("rb") as handle:
129 self.assertIsInstance(handle, HttpReadResourceHandle)
131 # Test reading byte ranges works
132 with remote_file.open("rb") as handle:
133 sub_contents = contents[:10]
134 handle = cast(HttpReadResourceHandle, handle)
135 result = handle.read(len(sub_contents)).decode()
136 self.assertEqual(result, sub_contents)
137 # Verify there is no internal buffer.
138 self.assertIsNone(handle._completeBuffer)
139 # Verify the position.
140 self.assertEqual(handle.tell(), len(sub_contents))
142 # Jump back to the beginning and test if reading the whole file
143 # prompts the internal buffer to be read.
144 handle.seek(0)
145 self.assertEqual(handle.tell(), 0)
146 result = handle.read().decode()
147 self.assertIsNotNone(handle._completeBuffer)
148 self.assertEqual(result, contents)
150 # Verify reading as a string handle works as expected.
151 with remote_file.open("r") as handle:
152 self.assertIsInstance(handle, io.TextIOWrapper)
154 handle = cast(io.TextIOWrapper, handle)
155 self.assertIsInstance(handle.buffer, HttpReadResourceHandle)
157 # Check if string methods work.
158 result = handle.read()
159 self.assertEqual(result, contents)
161 # Verify that write modes invoke the default base method
162 with remote_file.open("w") as handle:
163 self.assertIsInstance(handle, io.StringIO)
165 def test_dav_is_dav_enpoint(self):
166 # Ensure the server is a webDAV endpoint
167 self.assertTrue(self.tmpdir.is_webdav_endpoint)
169 def test_dav_mkdir(self):
170 # Check creation and deletion of an empty directory
171 subdir = self.tmpdir.join(self._get_dir_name(), forceDirectory=True)
172 self.assertIsNone(subdir.mkdir())
173 self.assertTrue(subdir.exists())
175 # Creating an existing remote directory must succeed
176 self.assertIsNone(subdir.mkdir())
178 # Deletion of an existing directory must succeed
179 self.assertIsNone(subdir.remove())
181 # Deletion of an non-existing directory must raise
182 subir_not_exists = self.tmpdir.join(self._get_dir_name(), forceDirectory=True)
183 with self.assertRaises(FileNotFoundError):
184 self.assertIsNone(subir_not_exists.remove())
186 # Creation of a directory at a path where a file exists must raise
187 file = self.tmpdir.join(self._get_file_name(), forceDirectory=False)
188 file.write(data=None, overwrite=True)
189 self.assertTrue(file.exists())
191 existing_file = self.tmpdir.join(file.basename(), forceDirectory=True)
192 with self.assertRaises(NotADirectoryError):
193 self.assertIsNone(existing_file.mkdir())
195 def test_dav_upload_download(self):
196 # Test upload a randomly-generated file via write() with and without
197 # overwrite
198 local_file, file_size = self._generate_file()
199 with open(local_file, "rb") as f:
200 data = f.read()
202 remote_file = self.tmpdir.join(self._get_file_name())
203 self.assertIsNone(remote_file.write(data, overwrite=True))
204 self.assertTrue(remote_file.exists())
205 self.assertEqual(remote_file.size(), file_size)
207 # Write without overwrite must raise since target file exists
208 with self.assertRaises(FileExistsError):
209 remote_file.write(data, overwrite=False)
211 # Download the file we just uploaded. Compute and compare a digest of
212 # the uploaded and downloaded data and ensure they match
213 downloaded_data = remote_file.read()
214 self.assertEqual(len(downloaded_data), file_size)
215 upload_digest = self._compute_digest(data)
216 download_digest = self._compute_digest(downloaded_data)
217 self.assertEqual(upload_digest, download_digest)
218 os.remove(local_file)
220 def test_dav_as_local(self):
221 contents = str.encode("12345")
222 remote_file = self.tmpdir.join(self._get_file_name())
223 self.assertIsNone(remote_file.write(data=contents, overwrite=True))
225 local_path, is_temp = remote_file._as_local()
226 self.assertTrue(is_temp)
227 self.assertTrue(os.path.exists(local_path))
228 self.assertEqual(ResourcePath(local_path).read(), contents)
229 os.remove(local_path)
231 def test_dav_size(self):
232 # Size of a non-existent file must raise.
233 remote_file = self.tmpdir.join(self._get_file_name())
234 with self.assertRaises(FileNotFoundError):
235 remote_file.size()
237 # Retrieving the size of a remote directory using a file-like path must
238 # raise
239 remote_dir = self.tmpdir.join(self._get_dir_name(), forceDirectory=True)
240 self.assertIsNone(remote_dir.mkdir())
241 self.assertTrue(remote_dir.exists())
243 dir_as_file = ResourcePath(remote_dir.geturl().rstrip("/"), forceDirectory=False)
244 with self.assertRaises(IsADirectoryError):
245 dir_as_file.size()
247 def test_dav_upload_creates_dir(self):
248 # Uploading a file to a non existing directory must ensure its
249 # parent directories are automatically created and upload succeeds
250 non_existing_dir = self.tmpdir.join(self._get_dir_name(), forceDirectory=True)
251 non_existing_dir = non_existing_dir.join(self._get_dir_name(), forceDirectory=True)
252 non_existing_dir = non_existing_dir.join(self._get_dir_name(), forceDirectory=True)
253 remote_file = non_existing_dir.join(self._get_file_name())
255 local_file, file_size = self._generate_file()
256 with open(local_file, "rb") as f:
257 data = f.read()
258 self.assertIsNone(remote_file.write(data, overwrite=True))
260 self.assertTrue(remote_file.exists())
261 self.assertEqual(remote_file.size(), file_size)
262 self.assertTrue(remote_file.parent().exists())
264 downloaded_data = remote_file.read()
265 upload_digest = self._compute_digest(data)
266 download_digest = self._compute_digest(downloaded_data)
267 self.assertEqual(upload_digest, download_digest)
268 os.remove(local_file)
270 def test_dav_transfer_from(self):
271 # Transfer from local file via "copy", with and without overwrite
272 remote_file = self.tmpdir.join(self._get_file_name())
273 local_file, _ = self._generate_file()
274 source_file = ResourcePath(local_file)
275 self.assertIsNone(remote_file.transfer_from(source_file, transfer="copy", overwrite=True))
276 self.assertTrue(remote_file.exists())
277 self.assertEqual(remote_file.size(), source_file.size())
278 with self.assertRaises(FileExistsError):
279 remote_file.transfer_from(ResourcePath(local_file), transfer="copy", overwrite=False)
281 # Transfer from remote file via "copy", with and without overwrite
282 source_file = remote_file
283 target_file = self.tmpdir.join(self._get_file_name())
284 self.assertIsNone(target_file.transfer_from(source_file, transfer="copy", overwrite=True))
285 self.assertTrue(target_file.exists())
286 self.assertEqual(target_file.size(), source_file.size())
288 # Transfer without overwrite must raise since target resource exists
289 with self.assertRaises(FileExistsError):
290 target_file.transfer_from(source_file, transfer="copy", overwrite=False)
292 # Test transfer from local file via "move", with and without overwrite
293 source_file = ResourcePath(local_file)
294 source_size = source_file.size()
295 target_file = self.tmpdir.join(self._get_file_name())
296 self.assertIsNone(target_file.transfer_from(source_file, transfer="move", overwrite=True))
297 self.assertTrue(target_file.exists())
298 self.assertEqual(target_file.size(), source_size)
299 self.assertFalse(source_file.exists())
301 # Test transfer without overwrite must raise since target resource
302 # exists
303 local_file, file_size = self._generate_file()
304 with self.assertRaises(FileExistsError):
305 source_file = ResourcePath(local_file)
306 target_file.transfer_from(source_file, transfer="move", overwrite=False)
308 # Test transfer from remote file via "move" with and without overwrite
309 # must succeed
310 source_file = target_file
311 source_size = source_file.size()
312 target_file = self.tmpdir.join(self._get_file_name())
313 self.assertIsNone(target_file.transfer_from(source_file, transfer="move", overwrite=True))
314 self.assertTrue(target_file.exists())
315 self.assertEqual(target_file.size(), source_size)
316 self.assertFalse(source_file.exists())
318 # Transfer without overwrite must raise since target resource exists
319 with self.assertRaises(FileExistsError):
320 source_file = ResourcePath(local_file)
321 target_file.transfer_from(source_file, transfer="move", overwrite=False)
323 def test_dav_handle(self):
324 # Resource handle must succeed
325 target_file = self.tmpdir.join(self._get_file_name())
326 data = "abcdefghi"
327 self.assertIsNone(target_file.write(data, overwrite=True))
328 with target_file.open("rb") as handle:
329 handle.seek(1)
330 self.assertEqual(handle.read(4).decode("utf-8"), data[1:5])
332 def test_dav_delete(self):
333 # Deletion of an existing remote file must succeed
334 local_file, file_size = self._generate_file()
335 with open(local_file, "rb") as f:
336 data = f.read()
338 remote_file = self.tmpdir.join(self._get_file_name())
339 self.assertIsNone(remote_file.write(data, overwrite=True))
340 self.assertTrue(remote_file.exists())
341 self.assertEqual(remote_file.size(), file_size)
342 self.assertIsNone(remote_file.remove())
343 os.remove(local_file)
345 # Deletion of a non-existing remote file must raise
346 non_existing_file = self.tmpdir.join(self._get_file_name())
347 with self.assertRaises(FileNotFoundError):
348 self.assertIsNone(non_existing_file.remove())
350 # Deletion of a non-empty remote directory must succeed
351 subdir = self.tmpdir.join(self._get_dir_name(), forceDirectory=True)
352 self.assertIsNone(subdir.mkdir())
353 self.assertTrue(subdir.exists())
354 local_file, _ = self._generate_file()
355 source_file = ResourcePath(local_file)
356 target_file = self.tmpdir.join(self._get_file_name(), forceDirectory=True)
357 self.assertIsNone(target_file.transfer_from(source_file, transfer="copy", overwrite=True))
358 self.assertIsNone(subdir.remove())
359 self.assertFalse(subdir.exists())
360 os.remove(local_file)
362 @classmethod
363 def _get_port_number(cls) -> int:
364 """Return a port number the webDAV server can use to listen to."""
365 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
366 s.bind(("127.0.0.1", 0))
367 s.listen()
368 port = s.getsockname()[1]
369 s.close()
370 return port
372 def _serve_webdav(self, local_path: str, port: int, stop_webdav_server: Callable[[], bool]):
373 """Start a local webDAV server, listening on http://localhost:port
374 and exposing local_path.
376 This server only runs when this test class is instantiated,
377 and then shuts down. The server must be started is a separate thread.
379 Parameters
380 ----------
381 port : `int`
382 The port number on which the server should listen
383 local_path : `str`
384 Path to an existing local directory for the server to expose.
385 stop_webdav_server : `Callable[[], bool]`
386 Boolean function which returns True when the server should be
387 stopped.
388 """
389 try:
390 # Start the wsgi server in a separate thread
391 config = {
392 "host": "127.0.0.1",
393 "port": port,
394 "provider_mapping": {"/": local_path},
395 "http_authenticator": {"domain_controller": None},
396 "simple_dc": {"user_mapping": {"*": True}},
397 "verbose": 0,
398 "lock_storage": False,
399 "dir_browser": {
400 "enable": False,
401 "ms_sharepoint_support": False,
402 "libre_office_support": False,
403 "response_trailer": False,
404 "davmount_links": False,
405 },
406 }
407 server = wsgi.Server(wsgi_app=WsgiDAVApp(config), bind_addr=(config["host"], config["port"]))
408 t = Thread(target=server.start, daemon=True)
409 t.start()
411 # Shut down the server when done: stop_webdav_server() returns
412 # True when this test suite is being teared down
413 while not stop_webdav_server():
414 time.sleep(1)
415 except KeyboardInterrupt:
416 # Caught Ctrl-C, shut down the server
417 pass
418 finally:
419 server.stop()
420 t.join()
422 @classmethod
423 def _get_name(cls, prefix: str) -> str:
424 alphabet = string.ascii_lowercase + string.digits
425 return f"{prefix}-" + "".join(random.choices(alphabet, k=8))
427 @classmethod
428 def _get_dir_name(cls) -> str:
429 """Return a randomly selected name for a file"""
430 return cls._get_name(prefix="dir")
432 @classmethod
433 def _get_file_name(cls) -> str:
434 """Return a randomly selected name for a file"""
435 return cls._get_name(prefix="file")
437 def _generate_file(self, remove_when_done=True) -> Tuple[str, int]:
438 """Create a local file of random size with random contents.
440 Returns
441 -------
442 path : `str`
443 Path to local temporary file. The caller is responsible for
444 removing the file when appropriate.
445 size : `int`
446 Size of the generated file, in bytes.
447 """
448 megabyte = 1024 * 1024
449 size = random.randint(2 * megabyte, 5 * megabyte)
450 tmpfile, path = tempfile.mkstemp()
451 self.assertEqual(os.write(tmpfile, os.urandom(size)), size)
452 os.close(tmpfile)
454 if remove_when_done:
455 self.local_files_to_remove.append(path)
457 return path, size
459 @classmethod
460 def _compute_digest(cls, data: bytes) -> str:
461 """Compute a SHA256 hash of data."""
462 m = hashlib.sha256()
463 m.update(data)
464 return m.hexdigest()
466 @classmethod
467 def _is_server_running(cls, port: int) -> bool:
468 """Return True if there is a server listening on local address
469 127.0.0.1:<port>.
470 """
471 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
472 try:
473 s.connect(("127.0.0.1", port))
474 return True
475 except ConnectionRefusedError:
476 return False
479class WebdavUtilsTestCase(unittest.TestCase):
480 """Test for the Webdav related utilities."""
482 def setUp(self):
483 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR))
485 def tearDown(self):
486 if self.tmpdir:
487 if self.tmpdir.isLocal:
488 removeTestTempDir(self.tmpdir.ospath)
490 @responses.activate
491 def test_is_webdav_endpoint(self):
492 davEndpoint = "http://www.lsstwithwebdav.org"
493 responses.add(responses.OPTIONS, davEndpoint, status=200, headers={"DAV": "1,2,3"})
494 self.assertTrue(_is_webdav_endpoint(davEndpoint))
496 plainHttpEndpoint = "http://www.lsstwithoutwebdav.org"
497 responses.add(responses.OPTIONS, plainHttpEndpoint, status=200)
498 self.assertFalse(_is_webdav_endpoint(plainHttpEndpoint))
500 def test_send_expect_header(self):
501 # Ensure _SEND_EXPECT_HEADER_ON_PUT is correctly initialized from
502 # the environment.
503 os.environ.pop("LSST_HTTP_PUT_SEND_EXPECT_HEADER", None)
504 importlib.reload(lsst.resources.http)
505 self.assertFalse(lsst.resources.http._SEND_EXPECT_HEADER_ON_PUT)
507 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_PUT_SEND_EXPECT_HEADER": "true"}, clear=True):
508 importlib.reload(lsst.resources.http)
509 self.assertTrue(lsst.resources.http._SEND_EXPECT_HEADER_ON_PUT)
511 def test_timeout(self):
512 connect_timeout = 100
513 read_timeout = 200
514 with unittest.mock.patch.dict(
515 os.environ,
516 {"LSST_HTTP_TIMEOUT_CONNECT": str(connect_timeout), "LSST_HTTP_TIMEOUT_READ": str(read_timeout)},
517 clear=True,
518 ):
519 # Force module reload to initialize TIMEOUT.
520 importlib.reload(lsst.resources.http)
521 self.assertEqual(lsst.resources.http.TIMEOUT, (connect_timeout, read_timeout))
523 def test_is_protected(self):
524 self.assertFalse(_is_protected("/this-file-does-not-exist"))
526 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
527 f.write("XXXX")
528 file_path = f.name
530 os.chmod(file_path, stat.S_IRUSR)
531 self.assertTrue(_is_protected(file_path))
533 for mode in (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH):
534 os.chmod(file_path, stat.S_IRUSR | mode)
535 self.assertFalse(_is_protected(file_path))
538class BearerTokenAuthTestCase(unittest.TestCase):
539 """Test for the BearerTokenAuth class."""
541 def setUp(self):
542 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR))
543 self.token = "ABCDE1234"
545 def tearDown(self):
546 if self.tmpdir and self.tmpdir.isLocal:
547 removeTestTempDir(self.tmpdir.ospath)
549 def test_empty_token(self):
550 """Ensure that when no token is provided the request is not
551 modified.
552 """
553 auth = BearerTokenAuth(None)
554 auth._refresh()
555 self.assertIsNone(auth._token)
556 self.assertIsNone(auth._path)
557 req = requests.Request("GET", "https://example.org")
558 self.assertEqual(auth(req), req)
560 def test_token_value(self):
561 """Ensure that when a token value is provided, the 'Authorization'
562 header is added to the requests.
563 """
564 auth = BearerTokenAuth(self.token)
565 req = auth(requests.Request("GET", "https://example.org").prepare())
566 self.assertEqual(req.headers.get("Authorization"), f"Bearer {self.token}")
568 def test_token_file(self):
569 """Ensure when the provided token is a file path, its contents is
570 correctly used in the the 'Authorization' header of the requests.
571 """
572 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
573 f.write(self.token)
574 token_file_path = f.name
576 # Ensure the request's "Authorization" header is set with the right
577 # token value
578 os.chmod(token_file_path, stat.S_IRUSR)
579 auth = BearerTokenAuth(token_file_path)
580 req = auth(requests.Request("GET", "https://example.org").prepare())
581 self.assertEqual(req.headers.get("Authorization"), f"Bearer {self.token}")
583 # Ensure an exception is raised if either group or other can read the
584 # token file
585 for mode in (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH):
586 os.chmod(token_file_path, stat.S_IRUSR | mode)
587 with self.assertRaises(PermissionError):
588 BearerTokenAuth(token_file_path)
591class SessionStoreTestCase(unittest.TestCase):
592 """Test for the SessionStore class."""
594 def setUp(self):
595 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR))
596 self.rpath = ResourcePath("https://example.org")
598 def tearDown(self):
599 if self.tmpdir and self.tmpdir.isLocal:
600 removeTestTempDir(self.tmpdir.ospath)
602 def test_ca_cert_bundle(self):
603 """Ensure a certificate authorities bundle is used to authentify
604 the remote server.
605 """
606 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
607 f.write("CERT BUNDLE")
608 cert_bundle = f.name
610 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_CACERT_BUNDLE": cert_bundle}, clear=True):
611 session = SessionStore().get(self.rpath)
612 self.assertEqual(session.verify, cert_bundle)
614 def test_user_cert(self):
615 """Ensure if user certificate and private key are provided, they are
616 used for authenticating the client.
617 """
619 # Create mock certificate and private key files.
620 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
621 f.write("CERT")
622 client_cert = f.name
624 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
625 f.write("KEY")
626 client_key = f.name
628 # Check both LSST_HTTP_AUTH_CLIENT_CERT and LSST_HTTP_AUTH_CLIENT_KEY
629 # must be initialized.
630 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_AUTH_CLIENT_CERT": client_cert}, clear=True):
631 with self.assertRaises(ValueError):
632 SessionStore().get(self.rpath)
634 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_AUTH_CLIENT_KEY": client_key}, clear=True):
635 with self.assertRaises(ValueError):
636 SessionStore().get(self.rpath)
638 # Check private key file must be accessible only by its owner.
639 with unittest.mock.patch.dict(
640 os.environ,
641 {"LSST_HTTP_AUTH_CLIENT_CERT": client_cert, "LSST_HTTP_AUTH_CLIENT_KEY": client_key},
642 clear=True,
643 ):
644 # Ensure the session client certificate is initialized when
645 # only the owner can read the private key file.
646 os.chmod(client_key, stat.S_IRUSR)
647 session = SessionStore().get(self.rpath)
648 self.assertEqual(session.cert[0], client_cert)
649 self.assertEqual(session.cert[1], client_key)
651 # Ensure an exception is raised if either group or other can access
652 # the private key file.
653 for mode in (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH):
654 os.chmod(client_key, stat.S_IRUSR | mode)
655 with self.assertRaises(PermissionError):
656 SessionStore().get(self.rpath)
658 def test_token_env(self):
659 """Ensure when the token is provided via an environment variable
660 the sessions are equipped with a BearerTokenAuth.
661 """
662 token = "ABCDE"
663 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_AUTH_BEARER_TOKEN": token}, clear=True):
664 session = SessionStore().get(self.rpath)
665 self.assertEqual(type(session.auth), lsst.resources.http.BearerTokenAuth)
666 self.assertEqual(session.auth._token, token)
667 self.assertIsNone(session.auth._path)
669 def test_sessions(self):
670 """Ensure the session caching mechanism works."""
672 # Ensure the store provides a session for a given URL
673 root_url = "https://example.org"
674 store = SessionStore()
675 session = store.get(ResourcePath(root_url))
676 self.assertIsNotNone(session)
678 # Ensure the sessions retrieved from a single store with the same
679 # root URIs are equal
680 for u in (f"{root_url}", f"{root_url}/path/to/file"):
681 self.assertEqual(session, store.get(ResourcePath(u)))
683 # Ensure sessions retrieved for different root URIs are different
684 another_url = "https://another.example.org"
685 self.assertNotEqual(session, store.get(ResourcePath(another_url)))
687 # Ensure the sessions retrieved from a single store for URLs with
688 # different port numbers are different
689 root_url_with_port = f"{another_url}:12345"
690 session = store.get(ResourcePath(root_url_with_port))
691 self.assertNotEqual(session, store.get(ResourcePath(another_url)))
693 # Ensure the sessions retrieved from a single store with the same
694 # root URIs (including port numbers) are equal
695 for u in (f"{root_url_with_port}", f"{root_url_with_port}/path/to/file"):
696 self.assertEqual(session, store.get(ResourcePath(u)))
699if __name__ == "__main__": 699 ↛ 700line 699 didn't jump to line 700, because the condition on line 699 was never true
700 unittest.main()