Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
from unittest import mock
from unittest.mock import Mock
 
import pytest
from data_import.api import DownloadStorageData
from data_import.models import FileUpload
from django.conf import settings
from django.http import HttpResponse
from organizations.models import Organization
from rest_framework import status
from rest_framework.test import APIRequestFactory
from users.models import User
 
"""
Comprehensive test suite for DownloadStorageData API
 
This test module validates the complete functionality of the DownloadStorageData view,
which handles secure file downloads from persistent storage (S3, GCS, Azure, etc.).
 
## Test Coverage:
 
### Authentication & Authorization Tests:
- Missing filepath parameter validation
- Invalid filepath security checks  
- FileUpload permission validation
- Organization-based avatar access control
- Cross-organization access prevention
 
### File Serving Mode Tests:
- **NGINX Mode**: X-Accel-Redirect header generation for high-performance serving
- **Direct Mode**: RangedFileResponse streaming with range request support
- Mode switching via USE_NGINX_FOR_UPLOADS setting
 
### File Type Support Tests:
- Upload files (settings.UPLOAD_DIR) with permission checking
- Avatar files (settings.AVATAR_PATH) with organization validation
- Content-Type detection for various formats (PDF, MP3, MP4, JPG, unknown)
- Content-Disposition headers (inline vs attachment)
 
### Edge Case & Security Tests:
- URL encoding/decoding support
- NGINX redirect URL construction
- Mock response handling for different scenarios
- Error response codes (403, 404)
 
## Test Structure:
Each test method focuses on a specific aspect of the API:
1. Sets up mock objects and request data
2. Calls the DownloadStorageData.get() method
3. Validates response status, headers, and behavior
4. Verifies security constraints and permissions
 
## Mocking Strategy:
- FileUpload.objects.filter() for database queries
- User.objects.filter() for avatar access
- RangedFileResponse for direct file serving
- Settings overrides for mode switching
- Organization membership checks
 
This comprehensive test suite ensures the API correctly handles all file types,
security scenarios, and serving modes while maintaining backward compatibility.
"""
 
pytestmark = pytest.mark.django_db
 
 
class TestDownloadStorageData:
    @pytest.fixture
    def api_factory(self):
        return APIRequestFactory()
 
    @pytest.fixture
    def user(self):
        """Create a test user with organization"""
        org = Organization.objects.create(title='Test Org')
        user = User.objects.create_user(email='test@example.com', password='test123')
        user.active_organization = org
        user.save()
        return user
 
    @pytest.fixture
    def other_user(self):
        """Create another user with different organization"""
        other_org = Organization.objects.create(title='Other Org')
        other_user = User.objects.create_user(email='other@example.com', password='test123')
        other_user.active_organization = other_org
        other_user.save()
        return other_user
 
    @pytest.fixture
    def mock_file_upload(self, user):
        """Create a mock FileUpload object"""
        file_upload = Mock(spec=FileUpload)
        file_upload.has_permission = Mock(return_value=True)
 
        # Mock the file object and storage
        mock_file = Mock()
        mock_storage = Mock()
        mock_storage.url = Mock(return_value='http://example.com/test.pdf')
        mock_file.storage = mock_storage
        mock_file.name = 'test.pdf'
        mock_file.open = Mock(return_value=Mock())
 
        file_upload.file = mock_file
        return file_upload
 
    @pytest.fixture
    def view(self):
        return DownloadStorageData()
 
    def test_missing_filepath_returns_404(self, api_factory, user, view):
        """Test that missing filepath parameter returns 404"""
        request = api_factory.get('/storage-data/uploaded/')
        request.user = user
 
        response = view.get(request)
        assert response.status_code == status.HTTP_404_NOT_FOUND
 
    def test_invalid_filepath_returns_403(self, api_factory, user, view):
        """Test that filepath not starting with UPLOAD_DIR or AVATAR_PATH returns 403"""
        request = api_factory.get('/storage-data/uploaded/', {'filepath': 'invalid/path/file.pdf'})
        request.user = user
 
        response = view.get(request)
        assert response.status_code == status.HTTP_403_FORBIDDEN
 
    @mock.patch('data_import.api.FileUpload.objects.filter')
    def test_upload_file_not_found_returns_403(self, mock_filter, api_factory, user, view):
        """Test that non-existent upload file returns 403"""
        mock_filter.return_value.last.return_value = None
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.UPLOAD_DIR}/test.pdf'})
        request.user = user
 
        response = view.get(request)
        assert response.status_code == status.HTTP_403_FORBIDDEN
 
    @mock.patch('data_import.api.FileUpload.objects.filter')
    def test_upload_file_no_permission_returns_403(self, mock_filter, api_factory, user, view, mock_file_upload):
        """Test that upload file without permission returns 403"""
        mock_file_upload.has_permission.return_value = False
        mock_filter.return_value.last.return_value = mock_file_upload
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.UPLOAD_DIR}/test.pdf'})
        request.user = user
 
        response = view.get(request)
        assert response.status_code == status.HTTP_403_FORBIDDEN
        mock_file_upload.has_permission.assert_called_once_with(user)
 
    @mock.patch('data_import.api.User.objects.filter')
    def test_avatar_user_not_found_returns_403(self, mock_filter, api_factory, user, view):
        """Test that non-existent avatar user returns 403"""
        mock_filter.return_value.first.return_value = None
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.AVATAR_PATH}/avatar.jpg'})
        request.user = user
 
        response = view.get(request)
        assert response.status_code == status.HTTP_403_FORBIDDEN
 
    @mock.patch('data_import.api.User.objects.filter')
    def test_avatar_user_no_organization_access_returns_403(self, mock_filter, api_factory, user, other_user, view):
        """Test that avatar user from different organization returns 403"""
        mock_avatar_user = Mock()
        mock_avatar_user.avatar = Mock()
        mock_filter.return_value.first.return_value = mock_avatar_user
 
        # Mock organization access check to return False
        user.active_organization.has_user = Mock(return_value=False)
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.AVATAR_PATH}/avatar.jpg'})
        request.user = user
 
        response = view.get(request)
        assert response.status_code == status.HTTP_403_FORBIDDEN
 
    @mock.patch('data_import.api.FileUpload.objects.filter')
    @mock.patch('data_import.api.settings.USE_NGINX_FOR_UPLOADS', True)
    def test_upload_file_nginx_serving(self, mock_filter, api_factory, user, view, mock_file_upload):
        """Test upload file serving through NGINX"""
        mock_filter.return_value.last.return_value = mock_file_upload
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.UPLOAD_DIR}/test.pdf'})
        request.user = user
 
        response = view.get(request)
 
        assert isinstance(response, HttpResponse)
        assert response.status_code == 200
        assert 'X-Accel-Redirect' in response
        assert 'Content-Disposition' in response
        assert 'inline' in response['Content-Disposition']
        assert 'test.pdf' in response['Content-Disposition']
 
    @mock.patch('data_import.api.FileUpload.objects.filter')
    @mock.patch('data_import.api.settings.USE_NGINX_FOR_UPLOADS', False)
    @mock.patch('data_import.api.RangedFileResponse')
    def test_upload_file_direct_serving(
        self, mock_ranged_response, mock_filter, api_factory, user, view, mock_file_upload
    ):
        """Test upload file serving directly (without NGINX)"""
        mock_filter.return_value.last.return_value = mock_file_upload
        mock_response_instance = Mock()
        mock_response_instance.__setitem__ = Mock()  # Allow item assignment
        mock_ranged_response.return_value = mock_response_instance
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.UPLOAD_DIR}/test.pdf'})
        request.user = user
 
        response = view.get(request)
 
        assert response == mock_response_instance
        mock_ranged_response.assert_called_once()
 
    @mock.patch('data_import.api.User.objects.filter')
    @mock.patch('data_import.api.settings.USE_NGINX_FOR_UPLOADS', True)
    def test_avatar_file_nginx_serving(self, mock_filter, api_factory, user, view):
        """Test avatar file serving through NGINX"""
        mock_avatar_user = Mock()
        mock_avatar_file = Mock()
        mock_storage = Mock()
        mock_storage.url = Mock(return_value='http://example.com/avatar.jpg')
        mock_avatar_file.storage = mock_storage
        mock_avatar_file.name = 'avatar.jpg'
        mock_avatar_user.avatar = mock_avatar_file
        mock_filter.return_value.first.return_value = mock_avatar_user
 
        # Mock organization access check to return True
        user.active_organization.has_user = Mock(return_value=True)
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.AVATAR_PATH}/avatar.jpg'})
        request.user = user
 
        response = view.get(request)
 
        assert isinstance(response, HttpResponse)
        assert response.status_code == 200
        assert 'X-Accel-Redirect' in response
        assert 'Content-Disposition' in response
 
    @mock.patch('data_import.api.User.objects.filter')
    @mock.patch('data_import.api.settings.USE_NGINX_FOR_UPLOADS', False)
    @mock.patch('data_import.api.RangedFileResponse')
    def test_avatar_file_direct_serving(self, mock_ranged_response, mock_filter, api_factory, user, view):
        """Test avatar file serving directly (without NGINX)"""
        mock_avatar_user = Mock()
        mock_avatar_file = Mock()
        mock_avatar_file.open = Mock(return_value=Mock())
        mock_avatar_user.avatar = mock_avatar_file
        mock_filter.return_value.first.return_value = mock_avatar_user
        mock_response_instance = Mock()
        mock_response_instance.__setitem__ = Mock()  # Allow item assignment
        mock_ranged_response.return_value = mock_response_instance
 
        # Mock organization access check to return True
        user.active_organization.has_user = Mock(return_value=True)
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.AVATAR_PATH}/avatar.jpg'})
        request.user = user
 
        response = view.get(request)
 
        assert response == mock_response_instance
        mock_ranged_response.assert_called_once()
 
    @pytest.mark.parametrize(
        'file_extension,expected_content_type',
        [
            ('test.pdf', 'application/pdf'),
            ('test.mp3', 'audio/mpeg'),
            ('test.mp4', 'video/mp4'),
            ('test.jpg', 'image/jpeg'),
            ('unknown.unknownext', 'application/octet-stream'),
        ],
    )
    @mock.patch('data_import.api.FileUpload.objects.filter')
    @mock.patch('data_import.api.settings.USE_NGINX_FOR_UPLOADS', False)
    @mock.patch('data_import.api.RangedFileResponse')
    def test_content_type_detection(
        self,
        mock_ranged_response,
        mock_filter,
        file_extension,
        expected_content_type,
        api_factory,
        user,
        view,
        mock_file_upload,
    ):
        """Test that content types are properly detected for different file extensions"""
        mock_filter.return_value.last.return_value = mock_file_upload
        mock_response_instance = Mock()
        mock_response_instance.__setitem__ = Mock()  # Allow item assignment
        mock_ranged_response.return_value = mock_response_instance
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.UPLOAD_DIR}/{file_extension}'})
        request.user = user
 
        view.get(request)
 
        # Check that RangedFileResponse was called with the expected content type
        args, kwargs = mock_ranged_response.call_args
        assert kwargs['content_type'] == expected_content_type
 
    @mock.patch('data_import.api.FileUpload.objects.filter')
    @mock.patch('data_import.api.settings.USE_NGINX_FOR_UPLOADS', True)
    def test_nginx_redirect_url_construction(self, mock_filter, api_factory, user, view, mock_file_upload):
        """Test that NGINX redirect URL is properly constructed"""
        # Set up mock to return a specific URL
        mock_file_upload.file.storage.url.return_value = 'https://s3.amazonaws.com/bucket/test.pdf'
        mock_filter.return_value.last.return_value = mock_file_upload
 
        request = api_factory.get('/storage-data/uploaded/', {'filepath': f'{settings.UPLOAD_DIR}/test.pdf'})
        request.user = user
 
        response = view.get(request)
 
        # Check that X-Accel-Redirect header contains the expected path
        redirect_header = response['X-Accel-Redirect']
        assert redirect_header == '/file_download/https/s3.amazonaws.com/bucket/test.pdf'
 
    @mock.patch('data_import.api.unquote')
    @mock.patch('data_import.api.FileUpload.objects.filter')
    def test_filepath_url_decoding(self, mock_filter, mock_unquote, api_factory, user, view, mock_file_upload):
        """Test that filepath is properly URL-decoded"""
        mock_unquote.return_value = f'{settings.UPLOAD_DIR}/decoded file.pdf'
        mock_filter.return_value.last.return_value = mock_file_upload
 
        encoded_filepath = f'{settings.UPLOAD_DIR}/encoded%20file.pdf'
        request = api_factory.get('/storage-data/uploaded/', {'filepath': encoded_filepath})
        request.user = user
 
        view.get(request)
 
        mock_unquote.assert_called_once_with(encoded_filepath)