Skip to content

Commit 5f3bd17

Browse files
authored
Merge branch 'dev' into dependabot/npm_and_yarn/npm_and_yarn-ca90b5e8d5
2 parents b02dc74 + 23ee51b commit 5f3bd17

44 files changed

Lines changed: 1229 additions & 385 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DEV-GUIDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Dev Guide
2+
3+
## File size limit used by file browser
4+
5+
The file open size limit is configured in [src/toolbarActions/toolbarFunctions.js](src/toolbarActions/toolbarFunctions.js).
6+
7+
- `MAX_FILE_SIZE_MB` controls the limit in MB.
8+
- `MAX_FILE_SIZE` is derived from it as bytes.
9+
10+
To change the limit, update `MAX_FILE_SIZE_MB` only.

package-lock.json

Lines changed: 28 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/controller/workflow.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
from model.workflows import *
1+
from model.workflows import WorkFlowModel
22
from flask import request, make_response, Blueprint
33
import defusedxml.ElementTree as ET
44

55
workFlow = Blueprint('workflow', __name__)
66
workFlowModel = WorkFlowModel()
77

88

9+
def isMissingWorkflow(graphml):
10+
if graphml is None:
11+
return True
12+
# Backward-compatible guard for legacy model return type.
13+
return isinstance(graphml, tuple) and len(graphml) > 0 and graphml[0] is False
14+
15+
916
def getLasteshActionHash(root):
1017
xmlns = root.tag[root.tag.index('{')+1:root.tag.rindex('}')]
1118
return root.find(f'{{{xmlns}}}graph')\
@@ -23,7 +30,7 @@ def getAllActionHash(root):
2330
def postWorkflow():
2431
try:
2532
lastestHash = getLasteshActionHash(ET.fromstring(request.data))
26-
except:
33+
except Exception:
2734
return "Invalid GraphML", 400
2835
graphML = request.data.decode('utf')
2936
return workFlowModel.insert(graphML, lastestHash)
@@ -32,7 +39,7 @@ def postWorkflow():
3239
@workFlow.route("/<serverID>")
3340
def getWorkflow(serverID):
3441
graphml = workFlowModel.get(serverID)
35-
if graphml is None:
42+
if isMissingWorkflow(graphml):
3643
return "Not Found", 404
3744
if('X-Latest-Hash' in request.headers):
3845
latestHash = request.headers['X-Latest-Hash']
@@ -53,7 +60,7 @@ def updateWorkflow(serverID):
5360
latestHash = getLasteshActionHash(root)
5461
if(not forceUpdate):
5562
allHash = getAllActionHash(root)
56-
except:
63+
except Exception:
5764
return "Invalid GraphML", 400
5865
graphML = request.data.decode('utf')
5966
if(forceUpdate):

server/model/workflows.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from pymongo import MongoClient
2-
from pymongo import MongoClient
2+
from pymongo.errors import DuplicateKeyError
33
import time
44
from bson.objectid import ObjectId
55
from bson.errors import InvalidId
66
import os
77
import xml.etree.ElementTree as ET
8-
import random
8+
import secrets
99
import string
1010
from dotenv import load_dotenv
1111
load_dotenv()
@@ -14,42 +14,42 @@ class WorkFlowModel:
1414
def __init__(self) -> None:
1515
self.collection = MongoClient(os.getenv('MongoURL'))[
1616
os.getenv('dbName')][os.getenv('tableName')]
17+
self.collection.create_index('serverID', unique=True)
1718

1819
def get_random_string(self, length):
1920
letters = string.ascii_letters+string.digits
20-
return ''.join(random.choice(letters) for i in range(length))
21+
return ''.join(secrets.choice(letters) for i in range(length))
2122

2223
def insert(self, graphml, latestHash):
23-
serverID = ""
2424
while(True):
2525
serverID = self.get_random_string(6)
26-
if(not self.collection.find_one({'serverID': serverID})):
27-
break
28-
self.collection.insert_one(
29-
{'graphml': graphml, 'latestHash': latestHash, 'serverID': serverID})
30-
return serverID
26+
try:
27+
self.collection.insert_one(
28+
{'graphml': graphml, 'latestHash': latestHash, 'serverID': serverID})
29+
return serverID
30+
except DuplicateKeyError:
31+
continue
3132

3233
def get(self, serverID):
3334
cl = self.collection.find_one({'serverID': serverID})
3435
if not cl:
35-
return False, 'Record Not Found'
36+
return None
3637
return cl['graphml']
3738

3839
def update(self, serverID, graphml, latestHash, allHash):
39-
existingRecord = self.collection.find_one({'serverID': serverID})
40+
existingRecord = self.collection.find_one_and_update(
41+
{'serverID': serverID, 'latestHash': {'$in': allHash}},
42+
{"$set": {'graphml': graphml, 'latestHash': latestHash}})
4043
if existingRecord is None:
41-
return False, 'serverID do not exists.'
42-
latestExistingHash = existingRecord['latestHash']
43-
if latestExistingHash not in allHash:
44+
if not self.collection.find_one({'serverID': serverID}):
45+
return False, 'serverID do not exists.'
4446
return False, 'Can not update as provided graph do not has latest changes.'
45-
self.collection.update_one({'serverID': serverID}, {
46-
"$set": {'graphml': graphml, 'latestHash': latestHash}})
4747
return True, latestHash
4848

4949
def forceUpdate(self, serverID, graphml, latestHash):
50-
existingRecord = self.collection.find_one({'serverID': serverID})
50+
existingRecord = self.collection.find_one_and_update(
51+
{'serverID': serverID},
52+
{"$set": {'graphml': graphml, 'latestHash': latestHash}})
5153
if existingRecord is None:
5254
return False, 'serverID do not exists.'
53-
self.collection.update_one({'serverID': serverID},
54-
{"$set": {'graphml': graphml, 'latestHash': latestHash}})
5555
return True, latestHash

server/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
dnspython==2.6.1
2-
Flask==2.2.5
2+
Flask==3.1.3
33
python-dotenv==0.19.0
44
pymongo==4.6.3
55
gunicorn==23.0.0

server/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
load_dotenv()
77

88
app = Flask(__name__)
9+
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024
910
CORS(app)
1011

1112
app.register_blueprint(workFlow, url_prefix='/workflow')
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import importlib
2+
import pathlib
3+
import sys
4+
import types
5+
import unittest
6+
7+
from flask import Flask
8+
9+
VALID_GRAPHML = (
10+
'<graphml xmlns="http://graphml.graphdrawing.org/xmlns">'
11+
'<graph edgedefault="directed">'
12+
'<actionHistory><hash>hash-1</hash></actionHistory>'
13+
'</graph>'
14+
'</graphml>'
15+
)
16+
17+
18+
class FakeWorkFlowModel:
19+
def __init__(self, graph_response):
20+
self.graph_response = graph_response
21+
22+
def get(self, _server_id):
23+
return self.graph_response
24+
25+
def insert(self, graphml, latestHash):
26+
return 'test01'
27+
28+
def update(self, serverID, graphml, latestHash, allHash):
29+
return (True, latestHash)
30+
31+
def forceUpdate(self, serverID, graphml, latestHash):
32+
return (True, latestHash)
33+
34+
35+
class WorkflowControllerTests(unittest.TestCase):
36+
@classmethod
37+
def setUpClass(cls):
38+
server_root = pathlib.Path(__file__).resolve().parents[1]
39+
if str(server_root) not in sys.path:
40+
sys.path.insert(0, str(server_root))
41+
42+
fake_model_pkg = types.ModuleType('model')
43+
fake_model_workflows = types.ModuleType('model.workflows')
44+
45+
class StubWorkFlowModel:
46+
def get(self, _server_id):
47+
return None
48+
49+
fake_model_workflows.WorkFlowModel = StubWorkFlowModel
50+
fake_model_pkg.workflows = fake_model_workflows
51+
52+
sys.modules['model'] = fake_model_pkg
53+
sys.modules['model.workflows'] = fake_model_workflows
54+
55+
if 'controller.workflow' in sys.modules:
56+
del sys.modules['controller.workflow']
57+
cls.workflow_module = importlib.import_module('controller.workflow')
58+
59+
def make_client(self, graph_response):
60+
self.workflow_module.workFlowModel = FakeWorkFlowModel(graph_response)
61+
app = Flask(__name__)
62+
app.register_blueprint(self.workflow_module.workFlow, url_prefix='/workflow')
63+
return app.test_client()
64+
65+
def test_missing_workflow_returns_404_for_none(self):
66+
client = self.make_client(None)
67+
response = client.get('/workflow/missing-id')
68+
self.assertEqual(response.status_code, 404)
69+
self.assertEqual(response.get_data(as_text=True), 'Not Found')
70+
71+
def test_missing_workflow_returns_404_for_legacy_tuple(self):
72+
client = self.make_client((False, 'Record Not Found'))
73+
response = client.get('/workflow/missing-id')
74+
self.assertEqual(response.status_code, 404)
75+
self.assertEqual(response.get_data(as_text=True), 'Not Found')
76+
77+
def test_hash_header_returns_400_for_different_history(self):
78+
client = self.make_client(VALID_GRAPHML)
79+
response = client.get('/workflow/existing-id', headers={'X-Latest-Hash': 'unknown-hash'})
80+
self.assertEqual(response.status_code, 400)
81+
self.assertEqual(response.get_data(as_text=True), 'Different History')
82+
83+
def test_hash_header_returns_200_for_matching_history(self):
84+
client = self.make_client(VALID_GRAPHML)
85+
response = client.get('/workflow/existing-id', headers={'X-Latest-Hash': 'hash-1'})
86+
self.assertEqual(response.status_code, 200)
87+
self.assertEqual(response.get_data(as_text=True), VALID_GRAPHML)
88+
89+
def test_post_workflow_returns_server_id(self):
90+
client = self.make_client(None)
91+
response = client.post('/workflow/', data=VALID_GRAPHML,
92+
content_type='application/xml')
93+
self.assertEqual(response.status_code, 200)
94+
self.assertEqual(response.get_data(as_text=True), 'test01')
95+
96+
def test_post_workflow_invalid_xml_returns_400(self):
97+
client = self.make_client(None)
98+
response = client.post('/workflow/', data=b'not xml',
99+
content_type='application/xml')
100+
self.assertEqual(response.status_code, 400)
101+
102+
def test_update_workflow_returns_200(self):
103+
client = self.make_client(None)
104+
response = client.post('/workflow/test01', data=VALID_GRAPHML,
105+
content_type='application/xml')
106+
self.assertEqual(response.status_code, 200)
107+
108+
def test_update_workflow_invalid_xml_returns_400(self):
109+
client = self.make_client(None)
110+
response = client.post('/workflow/test01', data=b'not xml',
111+
content_type='application/xml')
112+
self.assertEqual(response.status_code, 400)
113+
114+
def test_force_update_workflow_returns_200(self):
115+
client = self.make_client(None)
116+
response = client.post('/workflow/test01?force=true', data=VALID_GRAPHML,
117+
content_type='application/xml')
118+
self.assertEqual(response.status_code, 200)
119+
120+
121+
if __name__ == '__main__':
122+
unittest.main()

0 commit comments

Comments
 (0)