Coverage for tests/test_http.py: 17%

401 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-22 03:00 -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. 

11 

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 

26 

27try: 

28 from cheroot import wsgi 

29 from wsgidav.wsgidav_app import WsgiDAVApp 

30except ImportError: 

31 WsgiDAVApp = None 

32 

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 

41 

42TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

43 

44 

45class GenericHttpTestCase(GenericTestCase, unittest.TestCase): 

46 scheme = "http" 

47 netloc = "server.example" 

48 

49 

50class HttpReadWriteWebdavTestCase(GenericReadWriteTestCase, unittest.TestCase): 

51 """Test with a real webDAV server, as opposed to mocking responses.""" 

52 

53 scheme = "http" 

54 

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 

60 

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() 

85 

86 # Wait for it to start 

87 time.sleep(1) 

88 

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 ) 

96 

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() 

105 

106 # Remove local temporary files 

107 for file in cls.local_files_to_remove: 

108 if os.path.exists(file): 

109 os.remove(file) 

110 

111 # Remove temp dir 

112 if cls.webdav_tmpdir: 

113 shutil.rmtree(cls.webdav_tmpdir, ignore_errors=True) 

114 

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() 

119 

120 super().setUp() 

121 

122 def tearDown(self): 

123 # Remove the work directory 

124 if self.work_dir.exists(): 

125 self.work_dir.remove() 

126 

127 super().tearDown() 

128 

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)) 

134 

135 # Test that the correct handle is returned. 

136 with remote_file.open("rb") as handle: 

137 self.assertIsInstance(handle, HttpReadResourceHandle) 

138 

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)) 

149 

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) 

157 

158 # Verify reading as a string handle works as expected. 

159 with remote_file.open("r") as handle: 

160 self.assertIsInstance(handle, io.TextIOWrapper) 

161 

162 handle = cast(io.TextIOWrapper, handle) 

163 self.assertIsInstance(handle.buffer, HttpReadResourceHandle) 

164 

165 # Check if string methods work. 

166 result = handle.read() 

167 self.assertEqual(result, contents) 

168 

169 # Verify that write modes invoke the default base method 

170 with remote_file.open("w") as handle: 

171 self.assertIsInstance(handle, io.StringIO) 

172 

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) 

176 

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()) 

182 

183 # Creating an existing remote directory must succeed 

184 self.assertIsNone(subdir.mkdir()) 

185 

186 # Deletion of an existing directory must succeed 

187 self.assertIsNone(subdir.remove()) 

188 

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()) 

193 

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() 

200 

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) 

205 

206 # Write without overwrite must raise since target file exists 

207 with self.assertRaises(FileExistsError): 

208 remote_file.write(data, overwrite=False) 

209 

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) 

218 

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)) 

223 

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) 

229 

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()) 

237 

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)) 

242 

243 self.assertTrue(remote_file.exists()) 

244 self.assertEqual(remote_file.size(), file_size) 

245 self.assertTrue(remote_file.parent().exists()) 

246 

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) 

252 

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) 

263 

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()) 

270 

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) 

274 

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()) 

283 

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) 

290 

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()) 

300 

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) 

305 

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]) 

314 

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() 

320 

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) 

327 

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()) 

332 

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) 

344 

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 

350 

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 

356 

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 

366 

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. 

370 

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. 

373 

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() 

405 

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() 

416 

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)) 

421 

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") 

426 

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") 

431 

432 def _generate_file(self, remove_when_done=True) -> Tuple[str, int]: 

433 """Create a local file of random size with random contents. 

434 

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) 

448 

449 if remove_when_done: 

450 self.local_files_to_remove.append(path) 

451 

452 return path, size 

453 

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() 

460 

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 

472 

473 

474class WebdavUtilsTestCase(unittest.TestCase): 

475 """Test for the Webdav related utilities.""" 

476 

477 def setUp(self): 

478 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR)) 

479 

480 def tearDown(self): 

481 if self.tmpdir: 

482 if self.tmpdir.isLocal: 

483 removeTestTempDir(self.tmpdir.ospath) 

484 

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)) 

490 

491 plainHttpEndpoint = "http://www.lsstwithoutwebdav.org" 

492 responses.add(responses.OPTIONS, plainHttpEndpoint, status=200) 

493 self.assertFalse(_is_webdav_endpoint(plainHttpEndpoint)) 

494 

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) 

501 

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) 

505 

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)) 

517 

518 def test_is_protected(self): 

519 self.assertFalse(_is_protected("/this-file-does-not-exist")) 

520 

521 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f: 

522 f.write("XXXX") 

523 file_path = f.name 

524 

525 os.chmod(file_path, stat.S_IRUSR) 

526 self.assertTrue(_is_protected(file_path)) 

527 

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)) 

531 

532 

533class BearerTokenAuthTestCase(unittest.TestCase): 

534 """Test for the BearerTokenAuth class.""" 

535 

536 def setUp(self): 

537 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR)) 

538 self.token = "ABCDE1234" 

539 

540 def tearDown(self): 

541 if self.tmpdir and self.tmpdir.isLocal: 

542 removeTestTempDir(self.tmpdir.ospath) 

543 

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) 

554 

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}") 

562 

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 

570 

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}") 

577 

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) 

584 

585 

586class SessionStoreTestCase(unittest.TestCase): 

587 """Test for the SessionStore class.""" 

588 

589 def setUp(self): 

590 self.tmpdir = ResourcePath(makeTestTempDir(TESTDIR)) 

591 self.rpath = ResourcePath("https://example.org") 

592 

593 def tearDown(self): 

594 if self.tmpdir and self.tmpdir.isLocal: 

595 removeTestTempDir(self.tmpdir.ospath) 

596 

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 

604 

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) 

608 

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 """ 

613 

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 

618 

619 with tempfile.NamedTemporaryFile(mode="wt", dir=self.tmpdir.ospath, delete=False) as f: 

620 f.write("KEY") 

621 client_key = f.name 

622 

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) 

628 

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) 

632 

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) 

645 

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) 

652 

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) 

663 

664 def test_sessions(self): 

665 """Ensure the session caching mechanism works.""" 

666 

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) 

672 

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))) 

677 

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))) 

681 

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))) 

687 

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))) 

692 

693 

694if __name__ == "__main__": 694 ↛ 695line 694 didn't jump to line 695, because the condition on line 694 was never true

695 unittest.main()