zhongrj
2025-11-24 276323dce9613867abb3f58a4cc2abbfb2fd0dea
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
import datetime
import os
 
from django.contrib.auth.models import User
from guardian.shortcuts import assign_perm, get_objects_for_user
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework_jwt.settings import api_settings
 
from app import pending_actions
from app.models import Project, Task
from app.plugins.signals import processing_node_removed
from app.tests.utils import catch_signal
from nodeodm import status_codes
from nodeodm.models import ProcessingNode
from .classes import BootTestCase
from webodm import settings
 
 
class TestApi(BootTestCase):
    def setUp(self):
        pass
 
    def tearDown(self):
        pass
 
    def test_projects_and_tasks(self):
        client = APIClient()
 
        user = User.objects.get(username="testuser")
        self.assertFalse(user.is_superuser)
 
        other_user = User.objects.get(username="testuser2")
 
        project = Project.objects.create(
            owner=user,
            name="test project"
        )
        other_project = Project.objects.create(
            owner=other_user,
            name="another test project"
        )
 
        # Forbidden without credentials
        res = client.get('/api/projects/')
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
 
        client.login(username="testuser", password="test1234")
        res = client.get('/api/projects/')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) > 0)
 
        res = client.get('/api/projects/?page=1')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data['results']) > 0)
 
        # Can sort
        res = client.get('/api/projects/?ordering=-created_at&page=1')
        last_project = Project.objects.filter(owner=user).latest('created_at')
        self.assertTrue(res.data["results"][0]['id'] == last_project.id)
 
        res = client.get('/api/projects/{}/'.format(project.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
 
        res = client.get('/api/projects/dasjkldas/')
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        res = client.get('/api/projects/{}/'.format(other_project.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can filter
        res = client.get('/api/projects/?name=999&page=1')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data["results"]) == 0)
 
        # Cannot list somebody else's project without permission
        res = client.get('/api/projects/?id={}&page=1'.format(other_project.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data["results"]) == 0)
 
        # Can access individual project
        res = client.get('/api/projects/{}/'.format(project.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(res.data["id"] == project.id)
 
        # Cannot access project for which we have no access to
        res = client.get('/api/projects/{}/'.format(other_project.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can create project, but owner cannot be set
        res = client.post('/api/projects/', {'name': 'test', 'description': 'test descr'})
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        self.assertTrue(Project.objects.get(pk=res.data['id']).owner.id == user.id)
 
        # Cannot leave name empty
        res = client.post('/api/projects/', {'description': 'test descr'})
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
 
 
        # Create some tasks
        task = Task.objects.create(project=project)
        task2 = Task.objects.create(project=project, created_at=task.created_at + datetime.timedelta(0, 1))
        other_task = Task.objects.create(project=other_project)
 
        # Can list project tasks to a project we have access to
        res = client.get('/api/projects/{}/tasks/'.format(project.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 2)
 
        # Can sort
        res = client.get('/api/projects/{}/tasks/?ordering=created_at'.format(project.id))
        self.assertTrue(res.data[0]['id'] == str(task.id))
        self.assertTrue(res.data[1]['id'] == str(task2.id))
 
        res = client.get('/api/projects/{}/tasks/?ordering=-created_at'.format(project.id))
        self.assertTrue(res.data[0]['id'] == str(task2.id))
        self.assertTrue(res.data[1]['id'] == str(task.id))
 
        # Cannot list project tasks for a project we don't have access to
        res = client.get('/api/projects/{}/tasks/'.format(other_project.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Cannot list project tasks for a project that doesn't exist
        res = client.get('/api/projects/999/tasks/')
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can list task details for a task belonging to a project we have access to
        res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, task.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(res.data["id"] == str(task.id))
 
        # images_count field exists
        self.assertTrue(res.data["images_count"] == 0)
 
        # can_rerun_from field exists, should be an empty list at this point
        self.assertTrue(len(res.data["can_rerun_from"]) == 0)
 
        # Get console output
        res = client.get('/api/projects/{}/tasks/{}/output/'.format(project.id, task.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(res.data == "")
 
        data_path = task.data_path()
        if not os.path.exists(data_path):
            os.makedirs(data_path, exist_ok=True)
 
        task.console.reset("line1\nline2\nline3")
        task.save()
 
        res = client.get('/api/projects/{}/tasks/{}/output/'.format(project.id, task.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, task.console.output())
 
        # Console output with line num
        res = client.get('/api/projects/{}/tasks/{}/output/?line=2'.format(project.id, task.id))
        self.assertEqual(res.data, "line3")
 
        # Console output with line num out of bounds
        res = client.get('/api/projects/{}/tasks/{}/output/?line=3'.format(project.id, task.id))
        self.assertEqual(res.data, "")
        res = client.get('/api/projects/{}/tasks/{}/output/?line=-1'.format(project.id, task.id))
        self.assertEqual(res.data, task.console.output())
 
        # Cannot list task details for a task belonging to a project we don't have access to
        res = client.get('/api/projects/{}/tasks/{}/'.format(other_project.id, other_task.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # As above, but by trying to trick the API by using a project we have access to
        res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, other_task.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Cannot duplicate a project we have no access to
        res = client.post('/api/projects/{}/duplicate/'.format(other_project.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can duplicate a project we have access to
        res = client.post('/api/projects/{}/duplicate/'.format(project.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(res.data.get('success'))
        new_project_id = res.data['project']['id']
        self.assertNotEqual(new_project_id, project.id)
 
        # Tasks have been duplicated
        duplicated_project = Project.objects.get(pk=new_project_id)
        self.assertEqual(project.task_set.count(), duplicated_project.task_set.count())
 
        # Cannot access task details for a task that doesn't exist
        res = client.get('/api/projects/{}/tasks/4004d1e9-ed2c-4983-8b93-fc7577ee6d89/'.format(project.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Cannot access task details for a malformed task id
        res = client.get('/api/projects/{}/tasks/0/'.format(project.id))
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can update a task
        res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {'name': 'updated!'}, format='json')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
 
        # Verify the task has been updated
        res = client.get('/api/projects/{}/tasks/{}/'.format(project.id, task.id))
        self.assertTrue(res.data["name"] == "updated!")
 
        # Cannot update a task we have no access to
        res = client.patch('/api/projects/{}/tasks/{}/'.format(other_project.id, other_task.id), {'name': 'updated!'}, format='json')
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can cancel a task for which we have permission
        self.assertTrue(task.pending_action is None)
        res = client.post('/api/projects/{}/tasks/{}/cancel/'.format(project.id, task.id))
        self.assertTrue(res.data["success"])
        task.refresh_from_db()
 
        # Task should have been canceled
        self.assertTrue(task.last_error is None)
        self.assertEqual(task.status, status_codes.CANCELED)
 
        res = client.post('/api/projects/{}/tasks/{}/restart/'.format(project.id, task.id))
        self.assertTrue(res.data["success"])
        task.refresh_from_db()
 
        # Task should have failed to be restarted
        self.assertTrue("has no processing node" in task.last_error)
 
        # Cannot cancel, restart or delete a task for which we don't have permission
        for action in ['cancel', 'remove', 'restart']:
            res = client.post('/api/projects/{}/tasks/{}/{}/'.format(other_project.id, other_task.id, action))
            self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
 
        # Can delete
        res = client.post('/api/projects/{}/tasks/{}/remove/'.format(project.id, task.id))
        self.assertTrue(res.data["success"])
        self.assertFalse(Task.objects.filter(id=task.id).exists())
 
        task = Task.objects.create(project=project)
        temp_project = Project.objects.create(owner=user)
 
        # We have permissions to do anything on a project that we own
        res = client.get('/api/projects/{}/'.format(project.id))
        for perm in ['delete', 'change', 'view', 'add']:
            self.assertTrue(perm in res.data['permissions'])
 
        # Can delete project that we we own
        res = client.delete('/api/projects/{}/'.format(temp_project.id))
        self.assertTrue(res.status_code == status.HTTP_204_NO_CONTENT)
        self.assertTrue(Project.objects.filter(id=temp_project.id).count() == 0) # Really deleted
 
        # Cannot delete a project we don't own
        other_temp_project = Project.objects.create(owner=other_user)
        res = client.delete('/api/projects/{}/'.format(other_temp_project.id))
        self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
 
        assign_perm('view_project', user, other_temp_project)
 
        # We have view permissions only
        res = client.get('/api/projects/{}/'.format(other_temp_project.id))
        self.assertTrue('view' in res.data['permissions'])
        for perm in ['delete', 'change', 'add']:
            self.assertFalse(perm in res.data['permissions'])
 
        # Can delete a project for which we just have view permissions
        # (we will just remove our read permissions without deleting the project)
        res = client.delete('/api/projects/{}/'.format(other_temp_project.id))
        self.assertTrue(res.status_code == status.HTTP_204_NO_CONTENT)
        
        # Project still exists
        self.assertTrue(Project.objects.filter(id=other_temp_project.id).count() == 1)
 
        # We just can't access it
        res = client.get('/api/projects/{}/'.format(other_temp_project.id))
        self.assertTrue(res.status_code == status.HTTP_404_NOT_FOUND)
 
        # A user cannot reassign a task to a
        # project for which he/she has no permissions
        res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {'project': other_project.id},
                           format='json')
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
 
        # A user cannot reassign a task to a
        # project for which he/she has no permissions (using uppercase)
        res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {'PROJECT': other_project.id},
                           format='json')
 
        # Request went through, but no changes were applied
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        task.refresh_from_db()
        self.assertTrue(task.project.id == project.id)
 
        # A user cannot update a task's read only fields
        self.assertTrue(task.pending_action != 0)
        res = client.patch('/api/projects/{}/tasks/{}/'.format(project.id, task.id), {
                'processing_time': 1234,
                'status': -99,
                'last_error': 'yo!',
                'created_at': 0,
                'pending_action': 0,
                'can_rerun_from': ['abc']
            }, format='json')
 
        # Operation should fail without errors, but nothing has changed in the DB
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        task.refresh_from_db()
        self.assertTrue(task.processing_time != 1234)
        self.assertTrue(task.status != -99)
        self.assertTrue(task.last_error != 'yo!')
        self.assertTrue(task.created_at != 0)
        self.assertTrue(task.pending_action != 0)
        self.assertTrue(len(res.data['can_rerun_from']) == 0)
 
    def test_processingnodes(self):
        client = APIClient()
 
        pnode = ProcessingNode.objects.create(
                hostname="localhost",
                port=999
            )
 
        another_pnode = ProcessingNode.objects.create(
            hostname="localhost",
            port=998
        )
 
        # Cannot list processing nodes as guest
        res = client.get('/api/processingnodes/')
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
 
        res = client.get('/api/processingnodes/{}/'.format(pnode.id))
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
 
        # Cannot get options as guest
        res = client.get('/api/processingnodes/options/')
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
 
        client.login(username="testuser", password="test1234")
 
        # Cannot list processing nodes, unless permissions have been granted
        res = client.get('/api/processingnodes/')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 0)
 
        user = User.objects.get(username="testuser")
        self.assertFalse(user.is_staff)
        self.assertFalse(user.is_superuser)
        self.assertFalse(user.has_perm('view_processingnode', pnode))
        assign_perm('view_processingnode', user, pnode)
        self.assertTrue(user.has_perm('view_processingnode', pnode))
 
        # Now we can list processing nodes as normal user
        res = client.get('/api/processingnodes/')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 1)
        self.assertTrue(res.data[0]["hostname"] == "localhost")
 
        # Can use filters
        res = client.get('/api/processingnodes/?id={}'.format(pnode.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 1)
 
        res = client.get('/api/processingnodes/?id={}'.format(another_pnode.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 0)
 
        # Can filter nodes with valid options
        res = client.get('/api/processingnodes/?has_available_options=true')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 0)
 
        res = client.get('/api/processingnodes/?has_available_options=false')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 1)
        self.assertTrue(res.data[0]['hostname'] == 'localhost')
 
 
        # Can get single processing node as normal user
        res = client.get('/api/processingnodes/{}/'.format(pnode.id))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(res.data["hostname"] == "localhost")
 
        # Verify online field exists
        self.assertTrue("online" in res.data)
 
        # Should be set to false
        self.assertFalse(res.data['online'])
 
        # Verify max images field
        self.assertTrue("max_images" in res.data)
 
        # Verify engine version
        self.assertTrue("engine_version" in res.data)
 
        # Verify engine
        self.assertTrue("engine" in res.data)
 
        # label should be hostname:port (since no label is set)
        self.assertEqual(res.data['label'], pnode.hostname + ":" + str(pnode.port))
 
        # If we update the label, the label is used instead
        pnode.label = "Test"
        pnode.save()
 
        res = client.get('/api/processingnodes/{}/'.format(pnode.id))
        self.assertEqual(res.data['label'], "Test")
 
        # Cannot delete a processing node as normal user
        res = client.delete('/api/processingnodes/{}/'.format(pnode.id))
        self.assertTrue(res.status_code, status.HTTP_403_FORBIDDEN)
 
        # Cannot create a processing node as normal user
        res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'})
        self.assertTrue(res.status_code, status.HTTP_403_FORBIDDEN)
 
        client.login(username="testsuperuser", password="test1234")
 
        # Can delete a processing node as super user
        # and a signal is sent when a processing node is deleted
        with catch_signal(processing_node_removed) as h1:
            res = client.delete('/api/processingnodes/{}/'.format(pnode.id))
            self.assertTrue(res.status_code, status.HTTP_200_OK)
        h1.assert_called_once_with(sender=ProcessingNode, processing_node_id=pnode.id, signal=processing_node_removed)
 
        # Can create a processing node as super user
        res = client.post('/api/processingnodes/', {'hostname': 'localhost', 'port':'1000'})
        self.assertTrue(res.status_code, status.HTTP_200_OK)
 
        # Verify node has been created
        res = client.get('/api/processingnodes/')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 2)
        self.assertTrue(res.data[1]["port"] == 1000)
 
        # Test available_options intersection
        # (with normal user)
        client.login(username="testuser", password="test1234")
        user = User.objects.get(username="testuser")
        self.assertFalse(user.is_superuser)
 
        p1 = ProcessingNode.objects.create(hostname="invalid-host", port=11223,
                                           last_refreshed=timezone.now(),
                                           available_options=[{'name': 'a'}, {'name': 'b'}])
        p2 = ProcessingNode.objects.create(hostname="invalid-host-2", port=11223,
                                           last_refreshed=timezone.now(),
                                           available_options=[{'name': 'a'}, {'name': 'c'}])
        p3 = ProcessingNode.objects.create(hostname="invalid-host-3", port=11223,
                                           last_refreshed=timezone.now(),
                                           available_options=[{'name': 'd'}])
        p4 = ProcessingNode.objects.create(hostname="invalid-host-4", port=11223,
                                           last_refreshed=timezone.now() - datetime.timedelta(minutes=settings.NODE_OFFLINE_MINUTES * 2),
                                           available_options=[{'name': 'd'}]) # offline
 
        assign_perm('view_processingnode', user, p1)
        assign_perm('view_processingnode', user, p2)
        assign_perm('view_processingnode', user, p4)
        self.assertFalse(user.has_perm('view_processingnode', p3))
 
        nodes_available = get_objects_for_user(user, 'view_processingnode', ProcessingNode, accept_global_perms=False).exclude(available_options=dict())
        self.assertTrue(len(nodes_available) == 3)
 
        res = client.get('/api/processingnodes/options/')
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertTrue(len(res.data) == 1)
        self.assertTrue(res.data[0]['name'] == 'a')
 
        # Test optimistic mode
        self.assertFalse(p4.is_online())
 
        settings.NODE_OPTIMISTIC_MODE = True
 
        self.assertTrue(p4.is_online())
        res = client.get('/api/processingnodes/')
        self.assertEqual(len(res.data), 3)
        for nodes in res.data:
            self.assertTrue(nodes['online'])
 
        settings.NODE_OPTIMISTIC_MODE = False
 
    def test_token_auth(self):
        client = APIClient()
 
        pnode = ProcessingNode.objects.create(
            hostname="localhost",
            port=999
        )
 
        # Cannot access resources
        res = client.get('/api/processingnodes/')
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
 
        # Cannot generate token with invalid credentials
        res = client.post('/api/token-auth/', {
            'username': 'testuser',
            'password': 'wrongpwd'
        })
        self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
 
        # Can generate token with valid credentials
        res = client.post('/api/token-auth/', {
            'username': 'testuser',
            'password': 'test1234'
        })
        self.assertEqual(res.status_code, status.HTTP_200_OK)
 
        token = res.data['token']
        self.assertTrue(len(token) > 0)
 
        # Can access resources by passing token via querystring
        res = client.get('/api/processingnodes/?jwt={}'.format(token))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
 
        # Can access resources by passing token via header
        client = APIClient(HTTP_AUTHORIZATION="{0} {1}".format(api_settings.JWT_AUTH_HEADER_PREFIX, token))
        res = client.get('/api/processingnodes/')
        self.assertEqual(res.status_code, status.HTTP_200_OK)