test_job_status.py 12.2 KB
Newer Older
1
"""
David Mendez's avatar
David Mendez committed
2
Tests for the status namespace
3
"""
4
5
import datetime
import io
David Mendez's avatar
David Mendez committed
6
import json
7
import os
David Mendez's avatar
David Mendez committed
8
import shutil
9
import unittest
David Mendez's avatar
David Mendez committed
10
11
from pathlib import Path

12
from app import create_app
13
from app.authorisation import token_generator
David Mendez's avatar
David Mendez committed
14
from app.db import DB
15
from app.models import delayed_job_models
16
17


David Mendez's avatar
David Mendez committed
18
# pylint: disable=E1101
19
class TestStatus(unittest.TestCase):
David Mendez's avatar
David Mendez committed
20
21
22
    """
    Class that tests the status namespace
    """
23
24
25
26
    def setUp(self):
        self.flask_app = create_app()
        self.client = self.flask_app.test_client()

27
28
29
30
31
    def tearDown(self):

        with self.flask_app.app_context():
            delayed_job_models.delete_all_jobs()

32
    def test_get_existing_job_status(self):
David Mendez's avatar
David Mendez committed
33
34
35
        """
        Tests that the status of an existing job is returned correctly.
        """
36
37
38

        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
39
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
40
41
42
43
44
45
46
47
48
49
50
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }
        with self.flask_app.app_context():
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            job_id = job_must_be.id

            client = self.client
            response = client.get(f'/status/{job_id}')
            resp_data = json.loads(response.data.decode('utf-8'))

David Mendez's avatar
David Mendez committed
51
52
53
54
55
            for prop in ['type', 'status', 'status_comment', 'progress', 'created_at', 'started_at', 'finished_at',
                         'raw_params', 'expires_at', 'api_initial_url', 'timezone']:
                type_must_be = str(getattr(job_must_be, prop))
                type_got = resp_data[prop]
                self.assertEqual(type_must_be, type_got, msg=f'The returned job {prop} is not correct.')
56

57
    def test_get_non_existing_job_status(self):
David Mendez's avatar
David Mendez committed
58
59
60
        """
        Tests that when the status of a non existing job a 404 error is produced.
        """
61

62
        client = self.client
63
64
        response = client.get('/status/some_id')
        self.assertEqual(response.status_code, 404, msg='A 404 not found error should have been produced')
David Mendez's avatar
David Mendez committed
65

66
    def test_a_job_cannot_update_another_job_status(self):
David Mendez's avatar
David Mendez committed
67
68
69
        """
        Tests that a job can not use its token to update another job's status
        """
70
71
72

        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
73
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
74
75
76
77
78
79
80
81
82
83
84
85
86
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }

        with self.flask_app.app_context():
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            job_id = job_must_be.id
            new_data = {
                'status': delayed_job_models.JobStatuses.RUNNING,
                'status_comment': 'Querying from web services',
                'progress': 50
            }

87
            token = token_generator.generate_job_token('another_id')
88
89
90
91
92
93
94
95
            headers = {
                'X-JOB-KEY': token
            }
            client = self.client
            response = client.patch(f'/status/{job_id}', data=new_data, headers=headers)
            self.assertEqual(response.status_code, 401,
                             msg='I should not be authorised to modify the status of another job')

David Mendez's avatar
David Mendez committed
96
    def test_update_job_status(self):
David Mendez's avatar
David Mendez committed
97
98
99
        """
        Tests that a job can update its status
        """
David Mendez's avatar
David Mendez committed
100
101
102

        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
103
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
David Mendez's avatar
David Mendez committed
104
105
106
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }
107

David Mendez's avatar
David Mendez committed
108
        with self.flask_app.app_context():
109
110
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            job_id = job_must_be.id
111
            new_data = {
112
113
114
                'status': delayed_job_models.JobStatuses.RUNNING,
                'status_comment': 'Querying from web services',
                'progress': 50
115
116
            }

117
            token = token_generator.generate_job_token(job_id)
118
119
120
            headers = {
                'X-JOB-KEY': token
            }
121
            client = self.client
122
            response = client.patch(f'/status/{job_id}', data=new_data, headers=headers)
123
124
125
126
            self.assertEqual(response.status_code, 200, msg='The request should have not failed')

            job_got = delayed_job_models.get_job_by_id(job_id)
            # be sure to have a fresh version of the object
David Mendez's avatar
David Mendez committed
127
128
129
            DB.session.rollback()
            DB.session.expire(job_got)
            DB.session.refresh(job_got)
130
131
132
133

            for key, value_must_be in new_data.items():
                value_got = getattr(job_got, key)
                self.assertEqual(value_got, value_must_be, msg=f'The {key} was not updated correctly!')
134
135

    def test_started_at_time_is_calculated_correctly(self):
David Mendez's avatar
David Mendez committed
136
137
138
        """
        Tests that when the status is changed to running, the started_at time is calculated
        """
139
140
141

        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
142
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
143
144
145
146
147
148
149
150
151
152
153
154
155
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }

        with self.flask_app.app_context():
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            job_id = job_must_be.id
            new_data = {
                'status': delayed_job_models.JobStatuses.RUNNING,
                'status_comment': 'Querying from web services',
                'progress': 50
            }

156
            token = token_generator.generate_job_token(job_id)
157
158
159
160
            headers = {
                'X-JOB-KEY': token
            }

161
            client = self.client
162
            response = client.patch(f'/status/{job_id}', data=new_data, headers=headers)
163
164
165
166
167
            started_at_time_must_be = datetime.datetime.utcnow().timestamp()
            self.assertEqual(response.status_code, 200, msg='The request should have not failed')

            job_got = delayed_job_models.get_job_by_id(job_id)
            # be sure to have a fresh version of the object
David Mendez's avatar
David Mendez committed
168
169
170
            DB.session.rollback()
            DB.session.expire(job_got)
            DB.session.refresh(job_got)
171
172
173
174
175
176

            started_at_time_got = job_got.started_at.timestamp()
            self.assertAlmostEqual(started_at_time_must_be, started_at_time_got, places=1,
                                   msg='The started at time was not calculated correctly!')

    def test_finished_at_and_expires_time_are_calculated_correctly(self):
David Mendez's avatar
David Mendez committed
177
178
179
180
        """
        Tests that when a job status is set to FINISHED, the finished_at time, and expires_at time are calculated
        correctly
        """
181
182
        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
183
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
184
185
186
187
188
189
190
191
192
193
194
195
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }

        with self.flask_app.app_context():
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            job_id = job_must_be.id
            new_data = {
                'status': delayed_job_models.JobStatuses.FINISHED,
                'progress': 100
            }

196
            token = token_generator.generate_job_token(job_id)
197
198
199
200
            headers = {
                'X-JOB-KEY': token
            }

201
            client = self.client
202
            response = client.patch(f'/status/{job_id}', data=new_data, headers=headers)
203
204
205
206
207
208
209
210
            finished_at_time_must_be = datetime.datetime.utcnow().timestamp()
            expiration_time_must_be = (datetime.datetime.utcnow() +
                                       datetime.timedelta(days=delayed_job_models.DAYS_TO_LIVE)).timestamp()

            self.assertEqual(response.status_code, 200, msg='The request should have not failed')

            job_got = delayed_job_models.get_job_by_id(job_id)
            # be sure to have a fresh version of the object
David Mendez's avatar
David Mendez committed
211
212
213
            DB.session.rollback()
            DB.session.expire(job_got)
            DB.session.refresh(job_got)
214
215
216
217
218
219
220
221
222

            finished_at_time_got = job_got.finished_at.timestamp()
            self.assertAlmostEqual(finished_at_time_must_be, finished_at_time_got, places=1,
                                   msg='The started at time was not calculated correctly!')

            expires_time_got = job_got.expires_at.timestamp()
            self.assertAlmostEqual(expiration_time_must_be, expires_time_got, places=1,
                                   msg='The expiration time was not calculated correctly!')

223
    def test_cannot_upload_file_to_non_existing_job(self):
David Mendez's avatar
David Mendez committed
224
225
226
        """
        Tests that a file cannot be uploaded to a non existing job.
        """
227
228
229
230
231
232
233

        client = self.client
        response = client.post('/status/some_id/file', data={'results_file': (io.BytesIO(b"test"), 'test.txt')},
                               content_type='multipart/form-data')
        self.assertEqual(response.status_code, 404, msg='A 404 not found error should have been produced')

    def test_a_job_cannot_upload_files_for_another_job(self):
David Mendez's avatar
David Mendez committed
234
235
236
        """
        Tests that a job cannot upload files for another job
        """
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

        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }

        with self.flask_app.app_context():
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            client = self.client

            token = token_generator.generate_job_token('another_id')
            headers = {
                'X-JOB-KEY': token
            }

            response = client.post(f'/status/{job_must_be.id}/results_file',
                                   data={'file': (io.BytesIO(b"test"), 'test.txt')},
                                   content_type='multipart/form-data',
                                   headers=headers)

            self.assertEqual(response.status_code, 401,
                             msg='I should not be authorised to upload the file for another job')

    def test_a_job_results_file_is_uploaded(self):
David Mendez's avatar
David Mendez committed
263
264
265
        """
        Tests that a job can upload it's results file correctly.
        """
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

        job_type = delayed_job_models.JobTypes.SIMILARITY
        params = {
            'search_type': str(delayed_job_models.JobTypes.SIMILARITY),
            'structure': '[H]C1(CCCN1C(=N)N)CC1=NC(=NO1)C1C=CC(=CC=1)NC1=NC(=CS1)C1C=CC(Br)=CC=1',
            'threshold': '70'
        }

        with self.flask_app.app_context():
            job_must_be = delayed_job_models.get_or_create(job_type, params)
            tmp_dir = os.path.join(str(Path().absolute()), 'tmp')
            output_dir_path = os.path.join(tmp_dir, job_must_be.id)
            os.makedirs(output_dir_path, exist_ok=True)
            job_must_be.output_dir_path = output_dir_path
            delayed_job_models.save_job(job_must_be)

            client = self.client

            token = token_generator.generate_job_token(job_must_be.id)
            headers = {
                'X-JOB-KEY': token
            }

            file_text = 'test'
            response = client.post(f'/status/{job_must_be.id}/results_file',
                                   data={'file': (io.BytesIO(f'{file_text}'.encode()), 'test.txt')},
                                   content_type='multipart/form-data',
                                   headers=headers)

            self.assertEqual(response.status_code, 200, msg='It was not possible to upload a job results file')
            # be sure to have a fresh version of the object
David Mendez's avatar
David Mendez committed
297
298
299
            DB.session.rollback()
            DB.session.expire(job_must_be)
            DB.session.refresh(job_must_be)
300
301
302
303
304
305
306

            output_file_path_must_be = job_must_be.output_file_path
            with open(output_file_path_must_be, 'r') as file_got:
                self.assertEqual(file_got.read(), file_text, msg='Output file was not saved correctly')

            shutil.rmtree(tmp_dir)

David Mendez's avatar
David Mendez committed
307
308
            # pylint: disable=W0511
            # TODO: reporting to es, etc