|
1 | 1 | import unittest |
| 2 | +from io import BytesIO |
2 | 3 | from unittest import mock |
3 | 4 |
|
4 | 5 | import django |
| 6 | +from django.core.files.base import File |
5 | 7 | from django.core.files.storage import Storage |
6 | | -from django.test import TestCase |
| 8 | +from django.test import TestCase, override_settings |
7 | 9 |
|
| 10 | +from health_check.contrib.s3boto3_storage.backends import S3Boto3StorageHealthCheck |
8 | 11 | from health_check.exceptions import ServiceUnavailable |
9 | 12 | from health_check.storage.backends import ( |
10 | 13 | DefaultFileStorageHealthCheck, |
@@ -45,6 +48,62 @@ def save(self, name, content, max_length=None): |
45 | 48 | self.MOCK_FILE_COUNT += 1 |
46 | 49 |
|
47 | 50 |
|
| 51 | +# Mocking the S3Boto3Storage backend |
| 52 | +class MockS3Boto3Storage: |
| 53 | + """ |
| 54 | + A mock S3Boto3Storage backend to simulate interactions with AWS S3. |
| 55 | + """ |
| 56 | + |
| 57 | + def __init__(self, saves=True, deletes=True): |
| 58 | + self.saves = saves |
| 59 | + self.deletes = deletes |
| 60 | + self.files = {} |
| 61 | + |
| 62 | + def open(self, name, mode="rb"): |
| 63 | + """ |
| 64 | + Simulates opening a file from the mocked S3 storage. |
| 65 | + For simplicity, this doesn't differentiate between read and write modes. |
| 66 | + """ |
| 67 | + if name in self.files: |
| 68 | + # Assuming file content is stored as bytes |
| 69 | + file_content = self.files[name] |
| 70 | + if isinstance(file_content, bytes): |
| 71 | + return File(BytesIO(file_content)) |
| 72 | + else: |
| 73 | + raise ValueError("File content must be bytes.") |
| 74 | + else: |
| 75 | + raise FileNotFoundError(f"The file {name} does not exist.") |
| 76 | + |
| 77 | + def save(self, name, content): |
| 78 | + """ |
| 79 | + Overriding save to ensure content is stored as bytes in a way compatible with open method. |
| 80 | + Assumes content is either a ContentFile, bytes, or a string that needs conversion. |
| 81 | + """ |
| 82 | + if self.saves: |
| 83 | + # Check if content is a ContentFile or similar and read bytes |
| 84 | + if hasattr(content, "read"): |
| 85 | + file_content = content.read() |
| 86 | + elif isinstance(content, bytes): |
| 87 | + file_content = content |
| 88 | + elif isinstance(content, str): |
| 89 | + file_content = content.encode() # Convert string to bytes |
| 90 | + else: |
| 91 | + raise ValueError("Unsupported file content type.") |
| 92 | + |
| 93 | + self.files[name] = file_content |
| 94 | + return name |
| 95 | + raise Exception("Failed to save file.") |
| 96 | + |
| 97 | + def delete(self, name): |
| 98 | + if self.deletes: |
| 99 | + self.files.pop(name, None) |
| 100 | + else: |
| 101 | + raise Exception("Failed to delete file.") |
| 102 | + |
| 103 | + def exists(self, name): |
| 104 | + return name in self.files |
| 105 | + |
| 106 | + |
48 | 107 | def get_file_name(*args, **kwargs): |
49 | 108 | return "mockfile.txt" |
50 | 109 |
|
@@ -156,3 +215,90 @@ def test_file_not_deleted_django_42_plus(self): |
156 | 215 | default_storage_health = DefaultFileStorageHealthCheck() |
157 | 216 | with self.assertRaises(ServiceUnavailable): |
158 | 217 | default_storage_health.check_status() |
| 218 | + |
| 219 | + |
| 220 | +@mock.patch("storages.backends.s3boto3.S3Boto3Storage", new=MockS3Boto3Storage) |
| 221 | +@override_settings( |
| 222 | + STORAGES={ |
| 223 | + "default": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}, |
| 224 | + } |
| 225 | +) |
| 226 | +class HealthCheckS3Boto3StorageTests(TestCase): |
| 227 | + """ |
| 228 | + Tests health check behavior with a mocked S3Boto3Storage backend. |
| 229 | + """ |
| 230 | + |
| 231 | + @unittest.skipUnless(django.VERSION <= (4, 1), "Only for Django 4.1 and earlier") |
| 232 | + @mock.patch( |
| 233 | + "storages.backends.s3boto3.S3Boto3Storage", |
| 234 | + MockS3Boto3Storage(deletes=False), |
| 235 | + ) |
| 236 | + def test_check_delete_success_django_41_earlier(self): |
| 237 | + """Test that check_delete correctly deletes a file when S3Boto3Storage is working.""" |
| 238 | + health_check = S3Boto3StorageHealthCheck() |
| 239 | + mock_storage = health_check.get_storage() |
| 240 | + file_name = "testfile.txt" |
| 241 | + content = BytesIO(b"Test content") |
| 242 | + mock_storage.save(file_name, content) |
| 243 | + |
| 244 | + with self.assertRaises(ServiceUnavailable): |
| 245 | + health_check.check_delete(file_name) |
| 246 | + |
| 247 | + @unittest.skipUnless((4, 2) <= django.VERSION, "Only for Django 4.2+") |
| 248 | + def test_check_delete_success(self): |
| 249 | + """Test that check_delete correctly deletes a file when S3Boto3Storage is working.""" |
| 250 | + health_check = S3Boto3StorageHealthCheck() |
| 251 | + mock_storage = health_check.get_storage() |
| 252 | + file_name = "testfile.txt" |
| 253 | + content = BytesIO(b"Test content") |
| 254 | + mock_storage.save(file_name, content) |
| 255 | + |
| 256 | + health_check.check_delete(file_name) |
| 257 | + self.assertFalse(mock_storage.exists(file_name)) |
| 258 | + |
| 259 | + def test_check_delete_failure(self): |
| 260 | + """Test that check_delete raises ServiceUnavailable when deletion fails.""" |
| 261 | + with mock.patch.object( |
| 262 | + MockS3Boto3Storage, |
| 263 | + "delete", |
| 264 | + side_effect=Exception("Failed to delete file."), |
| 265 | + ): |
| 266 | + health_check = S3Boto3StorageHealthCheck() |
| 267 | + with self.assertRaises(ServiceUnavailable): |
| 268 | + health_check.check_delete("testfile.txt") |
| 269 | + |
| 270 | + @unittest.skipUnless(django.VERSION <= (4, 1), "Only for Django 4.1 and earlier") |
| 271 | + @mock.patch( |
| 272 | + "storages.backends.s3boto3.S3Boto3Storage", |
| 273 | + MockS3Boto3Storage(deletes=False), |
| 274 | + ) |
| 275 | + def test_check_status_working_django_41_earlier(self): |
| 276 | + """Test check_status returns True when S3Boto3Storage can save and delete files.""" |
| 277 | + health_check = S3Boto3StorageHealthCheck() |
| 278 | + with self.assertRaises(ServiceUnavailable): |
| 279 | + self.assertTrue(health_check.check_status()) |
| 280 | + |
| 281 | + @unittest.skipUnless((4, 2) <= django.VERSION, "Only for Django 4.2+") |
| 282 | + def test_check_status_working(self): |
| 283 | + """Test check_status returns True when S3Boto3Storage can save and delete files.""" |
| 284 | + health_check = S3Boto3StorageHealthCheck() |
| 285 | + self.assertTrue(health_check.check_status()) |
| 286 | + |
| 287 | + def test_check_status_failure_on_save(self): |
| 288 | + """Test check_status raises ServiceUnavailable when file cannot be saved.""" |
| 289 | + with mock.patch.object( |
| 290 | + MockS3Boto3Storage, "save", side_effect=Exception("Failed to save file.") |
| 291 | + ): |
| 292 | + health_check = S3Boto3StorageHealthCheck() |
| 293 | + with self.assertRaises(ServiceUnavailable): |
| 294 | + health_check.check_status() |
| 295 | + |
| 296 | + def test_check_status_failure_on_delete(self): |
| 297 | + """Test check_status raises ServiceUnavailable when file cannot be deleted.""" |
| 298 | + with mock.patch.object( |
| 299 | + MockS3Boto3Storage, "exists", new_callable=mock.PropertyMock |
| 300 | + ) as mock_exists: |
| 301 | + mock_exists.return_value = False |
| 302 | + health_check = S3Boto3StorageHealthCheck() |
| 303 | + with self.assertRaises(ServiceUnavailable): |
| 304 | + health_check.check_status() |
0 commit comments