Coverage for tests/test_http.py: 17%
401 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-16 02:51 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-16 02:51 -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 setUp(self):
116 # Create a work directory for this test case
117 self.work_dir = ResourcePath(self._make_uri(path=self._get_dir_name()), forceDirectory=True)
118 self.work_dir.mkdir()
120 super().setUp()
122 def tearDown(self):
123 # Remove the work directory
124 if self.work_dir.exists():
125 self.work_dir.remove()
127 super().tearDown()
129 def test_dav_file_handle(self):
130 # Upload a new file with known contents.
131 contents = "These are some \n bytes to read"
132 remote_file = self.work_dir.join(self._get_file_name())
133 self.assertIsNone(remote_file.write(data=contents, overwrite=True))
135 # Test that the correct handle is returned.
136 with remote_file.open("rb") as handle:
137 self.assertIsInstance(handle, HttpReadResourceHandle)
139 # Test reading byte ranges works
140 with remote_file.open("rb") as handle:
141 sub_contents = contents[:10]
142 handle = cast(HttpReadResourceHandle, handle)
143 result = handle.read(len(sub_contents)).decode()
144 self.assertEqual(result, sub_contents)
145 # Verify there is no internal buffer.
146 self.assertIsNone(handle._completeBuffer)
147 # Verify the position.
148 self.assertEqual(handle.tell(), len(sub_contents))
150 # Jump back to the beginning and test if reading the whole file
151 # prompts the internal buffer to be read.
152 handle.seek(0)
153 self.assertEqual(handle.tell(), 0)
154 result = handle.read().decode()
155 self.assertIsNotNone(handle._completeBuffer)
156 self.assertEqual(result, contents)
158 # Verify reading as a string handle works as expected.
159 with remote_file.open("r") as handle:
160 self.assertIsInstance(handle, io.TextIOWrapper)
162 handle = cast(io.TextIOWrapper, handle)
163 self.assertIsInstance(handle.buffer, HttpReadResourceHandle)
165 # Check if string methods work.
166 result = handle.read()
167 self.assertEqual(result, contents)
169 # Verify that write modes invoke the default base method
170 with remote_file.open("w") as handle:
171 self.assertIsInstance(handle, io.StringIO)
173 def test_dav_is_dav_enpoint(self):
174 # Ensure the server is a webDAV endpoint
175 self.assertTrue(self.work_dir.is_webdav_endpoint)
177 def test_dav_mkdir(self):
178 # Check creation and deletion of an empty directory
179 subdir = self.work_dir.join(self._get_dir_name(), forceDirectory=True)
180 self.assertIsNone(subdir.mkdir())
181 self.assertTrue(subdir.exists())
183 # Creating an existing remote directory must succeed
184 self.assertIsNone(subdir.mkdir())
186 # Deletion of an existing directory must succeed
187 self.assertIsNone(subdir.remove())
189 # Deletion of an non-existing directory must raise
190 subir_not_exists = self.work_dir.join(self._get_dir_name(), forceDirectory=True)
191 with self.assertRaises(FileNotFoundError):
192 self.assertIsNone(subir_not_exists.remove())
194 def test_dav_upload_download(self):
195 # Test upload a randomly-generated file via write() with and without
196 # overwrite
197 local_file, file_size = self._generate_file()
198 with open(local_file, "rb") as f:
199 data = f.read()
201 remote_file = self.work_dir.join(self._get_file_name())
202 self.assertIsNone(remote_file.write(data, overwrite=True))
203 self.assertTrue(remote_file.exists())
204 self.assertEqual(remote_file.size(), file_size)
206 # Write without overwrite must raise since target file exists
207 with self.assertRaises(FileExistsError):
208 remote_file.write(data, overwrite=False)
210 # Download the file we just uploaded. Compute and compare a digest of
211 # the uploaded and downloaded data and ensure they match
212 downloaded_data = remote_file.read()
213 self.assertEqual(len(downloaded_data), file_size)
214 upload_digest = self._compute_digest(data)
215 download_digest = self._compute_digest(downloaded_data)
216 self.assertEqual(upload_digest, download_digest)
217 os.remove(local_file)
219 def test_dav_as_local(self):
220 contents = str.encode("12345")
221 remote_file = self.work_dir.join(self._get_file_name())
222 self.assertIsNone(remote_file.write(data=contents, overwrite=True))
224 local_path, is_temp = remote_file._as_local()
225 self.assertTrue(is_temp)
226 self.assertTrue(os.path.exists(local_path))
227 self.assertEqual(ResourcePath(local_path).read(), contents)
228 os.remove(local_path)
230 def test_dav_upload_creates_dir(self):
231 # Uploading a file to a non existing directory must ensure its
232 # parent directories are automatically created and upload succeeds
233 non_existing_dir = self.work_dir.join(self._get_dir_name(), forceDirectory=True)
234 non_existing_dir = non_existing_dir.join(self._get_dir_name(), forceDirectory=True)
235 non_existing_dir = non_existing_dir.join(self._get_dir_name(), forceDirectory=True)
236 remote_file = non_existing_dir.join(self._get_file_name())
238 local_file, file_size = self._generate_file()
239 with open(local_file, "rb") as f:
240 data = f.read()
241 self.assertIsNone(remote_file.write(data, overwrite=True))
243 self.assertTrue(remote_file.exists())
244 self.assertEqual(remote_file.size(), file_size)
245 self.assertTrue(remote_file.parent().exists())
247 downloaded_data = remote_file.read()
248 upload_digest = self._compute_digest(data)
249 download_digest = self._compute_digest(downloaded_data)
250 self.assertEqual(upload_digest, download_digest)
251 os.remove(local_file)
253 def test_dav_transfer_from(self):
254 # Transfer from local file via "copy", with and without overwrite
255 remote_file = self.work_dir.join(self._get_file_name())
256 local_file, _ = self._generate_file()
257 source_file = ResourcePath(local_file)
258 self.assertIsNone(remote_file.transfer_from(source_file, transfer="copy", overwrite=True))
259 self.assertTrue(remote_file.exists())
260 self.assertEqual(remote_file.size(), source_file.size())
261 with self.assertRaises(FileExistsError):
262 remote_file.transfer_from(ResourcePath(local_file), transfer="copy", overwrite=False)
264 # Transfer from remote file via "copy", with and without overwrite
265 source_file = remote_file
266 target_file = self.work_dir.join(self._get_file_name())
267 self.assertIsNone(target_file.transfer_from(source_file, transfer="copy", overwrite=True))
268 self.assertTrue(target_file.exists())
269 self.assertEqual(target_file.size(), source_file.size())
271 # Transfer without overwrite must raise since target resource exists
272 with self.assertRaises(FileExistsError):
273 target_file.transfer_from(source_file, transfer="copy", overwrite=False)
275 # Test transfer from local file via "move", with and without overwrite
276 source_file = ResourcePath(local_file)
277 source_size = source_file.size()
278 target_file = self.work_dir.join(self._get_file_name())
279 self.assertIsNone(target_file.transfer_from(source_file, transfer="move", overwrite=True))
280 self.assertTrue(target_file.exists())
281 self.assertEqual(target_file.size(), source_size)
282 self.assertFalse(source_file.exists())
284 # Test transfer without overwrite must raise since target resource
285 # exists
286 local_file, file_size = self._generate_file()
287 with self.assertRaises(FileExistsError):
288 source_file = ResourcePath(local_file)
289 target_file.transfer_from(source_file, transfer="move", overwrite=False)
291 # Test transfer from remote file via "move" with and without overwrite
292 # must succeed
293 source_file = target_file
294 source_size = source_file.size()
295 target_file = self.work_dir.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 # Transfer without overwrite must raise since target resource exists
302 with self.assertRaises(FileExistsError):
303 source_file = ResourcePath(local_file)
304 target_file.transfer_from(source_file, transfer="move", overwrite=False)
306 def test_dav_handle(self):
307 # Resource handle must succeed
308 target_file = self.work_dir.join(self._get_file_name())
309 data = "abcdefghi"
310 self.assertIsNone(target_file.write(data, overwrite=True))
311 with target_file.open("rb") as handle:
312 handle.seek(1)
313 self.assertEqual(handle.read(4).decode("utf-8"), data[1:5])
315 def test_dav_delete(self):
316 # Deletion of an existing remote file must succeed
317 local_file, file_size = self._generate_file()
318 with open(local_file, "rb") as f:
319 data = f.read()
321 remote_file = self.work_dir.join(self._get_file_name())
322 self.assertIsNone(remote_file.write(data, overwrite=True))
323 self.assertTrue(remote_file.exists())
324 self.assertEqual(remote_file.size(), file_size)
325 self.assertIsNone(remote_file.remove())
326 os.remove(local_file)
328 # Deletion of a non-existing remote file must raise
329 non_existing_file = self.work_dir.join(self._get_file_name())
330 with self.assertRaises(FileNotFoundError):
331 self.assertIsNone(non_existing_file.remove())
333 # Deletion of a non-empty remote directory must succeed
334 subdir = self.work_dir.join(self._get_dir_name(), forceDirectory=True)
335 self.assertIsNone(subdir.mkdir())
336 self.assertTrue(subdir.exists())
337 local_file, _ = self._generate_file()
338 source_file = ResourcePath(local_file)
339 target_file = self.work_dir.join(self._get_file_name(), forceDirectory=True)
340 self.assertIsNone(target_file.transfer_from(source_file, transfer="copy", overwrite=True))
341 self.assertIsNone(subdir.remove())
342 self.assertFalse(subdir.exists())
343 os.remove(local_file)
345 @unittest.skip("skipped test_walk() since HttpResourcePath.walk() is not implemented")
346 def test_walk(self):
347 # TODO: remove this test when walk() is implemented so the super
348 # class test_walk is executed.
349 pass
351 @unittest.skip("skipped test_large_walk() since HttpResourcePath.walk() is not implemented")
352 def test_large_walk(self):
353 # TODO: remove this test when walk() is implemented so the super
354 # class test_large_walk is executed.
355 pass
357 @classmethod
358 def _get_port_number(cls) -> int:
359 """Return a port number the webDAV server can use to listen to."""
360 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
361 s.bind(("127.0.0.1", 0))
362 s.listen()
363 port = s.getsockname()[1]
364 s.close()
365 return port
367 def _serve_webdav(self, local_path: str, port: int, stop_webdav_server: Callable[[], bool]):
368 """Start a local webDAV server, listening on http://localhost:port
369 and exposing local_path.
371 This server only runs when this test class is instantiated,
372 and then shuts down. The server must be started is a separate thread.
374 Parameters
375 ----------
376 port : `int`
377 The port number on which the server should listen
378 local_path : `str`
379 Path to an existing local directory for the server to expose.
380 stop_webdav_server : `Callable[[], bool]`
381 Boolean function which returns True when the server should be
382 stopped.
383 """
384 try:
385 # Start the wsgi server in a separate thread
386 config = {
387 "host": "127.0.0.1",
388 "port": port,
389 "provider_mapping": {"/": local_path},
390 "http_authenticator": {"domain_controller": None},
391 "simple_dc": {"user_mapping": {"*": True}},
392 "verbose": 0,
393 "lock_storage": False,
394 "dir_browser": {
395 "enable": False,
396 "ms_sharepoint_support": False,
397 "libre_office_support": False,
398 "response_trailer": False,
399 "davmount_links": False,
400 },
401 }
402 server = wsgi.Server(wsgi_app=WsgiDAVApp(config), bind_addr=(config["host"], config["port"]))
403 t = Thread(target=server.start, daemon=True)
404 t.start()
406 # Shut down the server when done: stop_webdav_server() returns
407 # True when this test suite is being teared down
408 while not stop_webdav_server():
409 time.sleep(1)
410 except KeyboardInterrupt:
411 # Caught Ctrl-C, shut down the server
412 pass
413 finally:
414 server.stop()
415 t.join()
417 @classmethod
418 def _get_name(cls, prefix: str) -> str:
419 alphabet = string.ascii_lowercase + string.digits
420 return f"{prefix}-" + "".join(random.choices(alphabet, k=8))
422 @classmethod
423 def _get_dir_name(cls) -> str:
424 """Return a randomly selected name for a file"""
425 return cls._get_name(prefix="dir")
427 @classmethod
428 def _get_file_name(cls) -> str:
429 """Return a randomly selected name for a file"""
430 return cls._get_name(prefix="file")
432 def _generate_file(self, remove_when_done=True) -> Tuple[str, int]:
433 """Create a local file of random size with random contents.
435 Returns
436 -------
437 path : `str`
438 Path to local temporary file. The caller is responsible for
439 removing the file when appropriate.
440 size : `int`
441 Size of the generated file, in bytes.
442 """
443 megabyte = 1024 * 1024
444 size = random.randint(2 * megabyte, 5 * megabyte)
445 tmpfile, path = tempfile.mkstemp()
446 self.assertEqual(os.write(tmpfile, os.urandom(size)), size)
447 os.close(tmpfile)
449 if remove_when_done:
450 self.local_files_to_remove.append(path)
452 return path, size
454 @classmethod
455 def _compute_digest(cls, data: bytes) -> str:
456 """Compute a SHA256 hash of data."""
457 m = hashlib.sha256()
458 m.update(data)
459 return m.hexdigest()
461 @classmethod
462 def _is_server_running(cls, port: int) -> bool:
463 """Return True if there is a server listening on local address
464 127.0.0.1:<port>.
465 """
466 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
467 try:
468 s.connect(("127.0.0.1", port))
469 return True
470 except ConnectionRefusedError:
471 return False
474class WebdavUtilsTestCase(unittest.TestCase):
475 """Test for the Webdav related utilities."""
477 def setUp(self):
478 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR))
480 def tearDown(self):
481 if self.tmpdir:
482 if self.tmpdir.isLocal:
483 removeTestTempDir(self.tmpdir.ospath)
485 @responses.activate
486 def test_is_webdav_endpoint(self):
487 davEndpoint = "http://www.lsstwithwebdav.org"
488 responses.add(responses.OPTIONS, davEndpoint, status=200, headers={"DAV": "1,2,3"})
489 self.assertTrue(_is_webdav_endpoint(davEndpoint))
491 plainHttpEndpoint = "http://www.lsstwithoutwebdav.org"
492 responses.add(responses.OPTIONS, plainHttpEndpoint, status=200)
493 self.assertFalse(_is_webdav_endpoint(plainHttpEndpoint))
495 def test_send_expect_header(self):
496 # Ensure _SEND_EXPECT_HEADER_ON_PUT is correctly initialized from
497 # the environment.
498 os.environ.pop("LSST_HTTP_PUT_SEND_EXPECT_HEADER", None)
499 importlib.reload(lsst.resources.http)
500 self.assertFalse(lsst.resources.http._SEND_EXPECT_HEADER_ON_PUT)
502 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_PUT_SEND_EXPECT_HEADER": "true"}, clear=True):
503 importlib.reload(lsst.resources.http)
504 self.assertTrue(lsst.resources.http._SEND_EXPECT_HEADER_ON_PUT)
506 def test_timeout(self):
507 connect_timeout = 100
508 read_timeout = 200
509 with unittest.mock.patch.dict(
510 os.environ,
511 {"LSST_HTTP_TIMEOUT_CONNECT": str(connect_timeout), "LSST_HTTP_TIMEOUT_READ": str(read_timeout)},
512 clear=True,
513 ):
514 # Force module reload to initialize TIMEOUT.
515 importlib.reload(lsst.resources.http)
516 self.assertEqual(lsst.resources.http.TIMEOUT, (connect_timeout, read_timeout))
518 def test_is_protected(self):
519 self.assertFalse(_is_protected("/this-file-does-not-exist"))
521 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
522 f.write("XXXX")
523 file_path = f.name
525 os.chmod(file_path, stat.S_IRUSR)
526 self.assertTrue(_is_protected(file_path))
528 for mode in (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH):
529 os.chmod(file_path, stat.S_IRUSR | mode)
530 self.assertFalse(_is_protected(file_path))
533class BearerTokenAuthTestCase(unittest.TestCase):
534 """Test for the BearerTokenAuth class."""
536 def setUp(self):
537 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR))
538 self.token = "ABCDE1234"
540 def tearDown(self):
541 if self.tmpdir and self.tmpdir.isLocal:
542 removeTestTempDir(self.tmpdir.ospath)
544 def test_empty_token(self):
545 """Ensure that when no token is provided the request is not
546 modified.
547 """
548 auth = BearerTokenAuth(None)
549 auth._refresh()
550 self.assertIsNone(auth._token)
551 self.assertIsNone(auth._path)
552 req = requests.Request("GET", "https://example.org")
553 self.assertEqual(auth(req), req)
555 def test_token_value(self):
556 """Ensure that when a token value is provided, the 'Authorization'
557 header is added to the requests.
558 """
559 auth = BearerTokenAuth(self.token)
560 req = auth(requests.Request("GET", "https://example.org").prepare())
561 self.assertEqual(req.headers.get("Authorization"), f"Bearer {self.token}")
563 def test_token_file(self):
564 """Ensure when the provided token is a file path, its contents is
565 correctly used in the the 'Authorization' header of the requests.
566 """
567 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
568 f.write(self.token)
569 token_file_path = f.name
571 # Ensure the request's "Authorization" header is set with the right
572 # token value
573 os.chmod(token_file_path, stat.S_IRUSR)
574 auth = BearerTokenAuth(token_file_path)
575 req = auth(requests.Request("GET", "https://example.org").prepare())
576 self.assertEqual(req.headers.get("Authorization"), f"Bearer {self.token}")
578 # Ensure an exception is raised if either group or other can read the
579 # token file
580 for mode in (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH):
581 os.chmod(token_file_path, stat.S_IRUSR | mode)
582 with self.assertRaises(PermissionError):
583 BearerTokenAuth(token_file_path)
586class SessionStoreTestCase(unittest.TestCase):
587 """Test for the SessionStore class."""
589 def setUp(self):
590 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR))
591 self.rpath = ResourcePath("https://example.org")
593 def tearDown(self):
594 if self.tmpdir and self.tmpdir.isLocal:
595 removeTestTempDir(self.tmpdir.ospath)
597 def test_ca_cert_bundle(self):
598 """Ensure a certificate authorities bundle is used to authentify
599 the remote server.
600 """
601 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
602 f.write("CERT BUNDLE")
603 cert_bundle = f.name
605 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_CACERT_BUNDLE": cert_bundle}, clear=True):
606 session = SessionStore().get(self.rpath)
607 self.assertEqual(session.verify, cert_bundle)
609 def test_user_cert(self):
610 """Ensure if user certificate and private key are provided, they are
611 used for authenticating the client.
612 """
614 # Create mock certificate and private key files.
615 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
616 f.write("CERT")
617 client_cert = f.name
619 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f:
620 f.write("KEY")
621 client_key = f.name
623 # Check both LSST_HTTP_AUTH_CLIENT_CERT and LSST_HTTP_AUTH_CLIENT_KEY
624 # must be initialized.
625 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_AUTH_CLIENT_CERT": client_cert}, clear=True):
626 with self.assertRaises(ValueError):
627 SessionStore().get(self.rpath)
629 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_AUTH_CLIENT_KEY": client_key}, clear=True):
630 with self.assertRaises(ValueError):
631 SessionStore().get(self.rpath)
633 # Check private key file must be accessible only by its owner.
634 with unittest.mock.patch.dict(
635 os.environ,
636 {"LSST_HTTP_AUTH_CLIENT_CERT": client_cert, "LSST_HTTP_AUTH_CLIENT_KEY": client_key},
637 clear=True,
638 ):
639 # Ensure the session client certificate is initialized when
640 # only the owner can read the private key file.
641 os.chmod(client_key, stat.S_IRUSR)
642 session = SessionStore().get(self.rpath)
643 self.assertEqual(session.cert[0], client_cert)
644 self.assertEqual(session.cert[1], client_key)
646 # Ensure an exception is raised if either group or other can access
647 # the private key file.
648 for mode in (stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH):
649 os.chmod(client_key, stat.S_IRUSR | mode)
650 with self.assertRaises(PermissionError):
651 SessionStore().get(self.rpath)
653 def test_token_env(self):
654 """Ensure when the token is provided via an environment variable
655 the sessions are equipped with a BearerTokenAuth.
656 """
657 token = "ABCDE"
658 with unittest.mock.patch.dict(os.environ, {"LSST_HTTP_AUTH_BEARER_TOKEN": token}, clear=True):
659 session = SessionStore().get(self.rpath)
660 self.assertEqual(type(session.auth), lsst.resources.http.BearerTokenAuth)
661 self.assertEqual(session.auth._token, token)
662 self.assertIsNone(session.auth._path)
664 def test_sessions(self):
665 """Ensure the session caching mechanism works."""
667 # Ensure the store provides a session for a given URL
668 root_url = "https://example.org"
669 store = SessionStore()
670 session = store.get(ResourcePath(root_url))
671 self.assertIsNotNone(session)
673 # Ensure the sessions retrieved from a single store with the same
674 # root URIs are equal
675 for u in (f"{root_url}", f"{root_url}/path/to/file"):
676 self.assertEqual(session, store.get(ResourcePath(u)))
678 # Ensure sessions retrieved for different root URIs are different
679 another_url = "https://another.example.org"
680 self.assertNotEqual(session, store.get(ResourcePath(another_url)))
682 # Ensure the sessions retrieved from a single store for URLs with
683 # different port numbers are different
684 root_url_with_port = f"{another_url}:12345"
685 session = store.get(ResourcePath(root_url_with_port))
686 self.assertNotEqual(session, store.get(ResourcePath(another_url)))
688 # Ensure the sessions retrieved from a single store with the same
689 # root URIs (including port numbers) are equal
690 for u in (f"{root_url_with_port}", f"{root_url_with_port}/path/to/file"):
691 self.assertEqual(session, store.get(ResourcePath(u)))
694if __name__ == "__main__": 694 ↛ 695line 694 didn't jump to line 695, because the condition on line 694 was never true
695 unittest.main()