From 446f157d774d5d2d19bcc3c45046996484ddddd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Mon, 21 Aug 2023 12:46:16 +0200 Subject: [PATCH 001/133] return date, not time, to cli --- dds_web/api/superadmin_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index 6703dd348..58988d535 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -325,7 +325,7 @@ def get(self): return { "stats": [ { - "Date": str(row.date), + "Date": str(row.date.date()), "Units": row.unit_count, "Researchers": row.researcher_count, "Project Owners": row.project_owner_unique_count, From 6743770ccb5d011c72383522d0e6e91acc53c934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Mon, 21 Aug 2023 12:50:03 +0200 Subject: [PATCH 002/133] sprintlog --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index c8bf2994d..01384690b 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -289,3 +289,4 @@ _Nothing merged in CLI during this sprint_ # 2023-08-21 - 2023-09-01 - Dependency: Bump `certifi` to 2023.07.22 due to security vulnerability alert(s) ([#1452](https://github.com/ScilifelabDataCentre/dds_web/pull/1452)) +- Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) From ebbfad0fad24063bd9f3610f7d1a35ddff2be1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Tue, 22 Aug 2023 10:30:40 +0200 Subject: [PATCH 003/133] fix test --- tests/api/test_superadmin_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index 416651097..ef7c7587a 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -883,7 +883,7 @@ def add_row_to_reporting_table(time): assert len(returned) == 1 reporting_row = models.Reporting.query.first() assert returned[0] == { - "Date": str(reporting_row.date), + "Date": str(reporting_row.date.date()), "Units": reporting_row.unit_count, "Researchers": reporting_row.researcher_count, "Project Owners": reporting_row.project_owner_unique_count, From 9aab5de75b2dd2a480eae4b9dd4d2868990f2108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Tue, 22 Aug 2023 12:14:23 +0200 Subject: [PATCH 004/133] added column descriptions to return --- dds_web/api/superadmin_only.py | 20 +++++++++++++++++++- dds_web/database/models.py | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index 58988d535..af60f05dd 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -322,6 +322,7 @@ class Statistics(flask_restful.Resource): def get(self): """Collect rows from reporting table and return them.""" stat_rows: typing.List = models.Reporting.query.all() + flask.current_app.logger.debug(stat_rows[0].id.__doc__) return { "stats": [ { @@ -343,7 +344,24 @@ def get(self): } for row in stat_rows if stat_rows - ] + ], + "columns": { + "Date": "Date on which the stats were recorded in the database.", + "Units": "Number of SciLifeLab units that are using the DDS for data deliveries.", + "Researchers": "Number of accounts with the role 'Researcher'.", + "Project Owners": "Number of (unique) 'Researcher' accounts with admin permissions in at least one project.", + "Unit Personnel": "Number of accounts with the role 'Unit Personnel'.", + "Unit Admins": "Number of accounts with the role 'Unit Admin'.", + "Super Admins": "Number of employees at the SciLifeLab Data Centre with the DDS account role 'Super Admin'.", + "Total Users": "Total number of accounts. Project Owners are a subrole of 'Researchers' and are therefore not included in the summary.", + "Total Projects": "Sum of active- and inactive projects.", + "Active Projects": "Delivery projects currently used to deliver data. Statuses included are 'In Progress', 'Available' and 'Expired'.", + "Inactive Projects": "Delivery projects that have previously been created and/or used for data deliveries. Statuses included are 'Deleted', 'Archived' (incl. aborted).", + "Data Now (TB)": "Number of terrabytes of data that are currently being delivered with the DDS.", + "Data Uploaded (TB)": "Total number of terrabytes of data that have been uploaded to the DDS temporary storage location since the DDS went into production.", + "TBHours Last Month": "Number of terrabyte hours that were recorded in the DDS the previous month. ", + "TBHours Total": "Total number of terrabyte hours that have been recorded in the DDS since going into production.", + }, } diff --git a/dds_web/database/models.py b/dds_web/database/models.py index c568db36c..4db5f0fc6 100644 --- a/dds_web/database/models.py +++ b/dds_web/database/models.py @@ -1084,7 +1084,7 @@ class Reporting(db.Model): __table_args__ = {"extend_existing": True} # Columns - id = db.Column(db.Integer, primary_key=True, autoincrement=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True, __doc__="ID for this row.") date = db.Column(db.DateTime(), unique=True, nullable=False, default=datetime.date.today) unit_count = db.Column(db.Integer, unique=False, nullable=False) researcher_count = db.Column(db.Integer, unique=False, nullable=False) From 6ca7e7f66e0ffe931a46948ce550ee04b8df6b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 23 Aug 2023 13:16:21 +0200 Subject: [PATCH 005/133] remove doc part in model --- dds_web/api/superadmin_only.py | 1 - dds_web/database/models.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index af60f05dd..6f6270898 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -322,7 +322,6 @@ class Statistics(flask_restful.Resource): def get(self): """Collect rows from reporting table and return them.""" stat_rows: typing.List = models.Reporting.query.all() - flask.current_app.logger.debug(stat_rows[0].id.__doc__) return { "stats": [ { diff --git a/dds_web/database/models.py b/dds_web/database/models.py index 4db5f0fc6..c568db36c 100644 --- a/dds_web/database/models.py +++ b/dds_web/database/models.py @@ -1084,7 +1084,7 @@ class Reporting(db.Model): __table_args__ = {"extend_existing": True} # Columns - id = db.Column(db.Integer, primary_key=True, autoincrement=True, __doc__="ID for this row.") + id = db.Column(db.Integer, primary_key=True, autoincrement=True) date = db.Column(db.DateTime(), unique=True, nullable=False, default=datetime.date.today) unit_count = db.Column(db.Integer, unique=False, nullable=False) researcher_count = db.Column(db.Integer, unique=False, nullable=False) From 9fb576f06be81b3be98381d1c56bf511c8cb2e6f Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:01:58 +0200 Subject: [PATCH 006/133] add get method to maintenance endpoint --- dds_web/api/superadmin_only.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index 6703dd348..45808a5e6 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -285,6 +285,19 @@ def put(self): return {"message": f"Maintenance set to: {setting.upper()}"} + @auth.login_required(role=["Super Admin"]) + @logging_bind_request + @handle_db_error + def get(self): + """Return current Maintenance mode.""" + current_mode = models.Maintenance.query.first() + if not current_mode.active: + mode = "OFF" + else: + mode = "ON" + + return {"message": f"Maintenanse mode is set to: {mode}"} + class AnyProjectsBusy(flask_restful.Resource): """Check if any projects are busy.""" From b0bee2710dacfcc2ee4b1c4ef3f900f43bacf7a6 Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Fri, 25 Aug 2023 16:52:54 +0200 Subject: [PATCH 007/133] fix/add test --- tests/api/test_superadmin_only.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index 416651097..8ac6d95b5 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -629,19 +629,33 @@ def test_set_maintenance_not_superadmin(client: flask.testing.FlaskClient) -> No assert response.status_code == http.HTTPStatus.FORBIDDEN +def test_get_maintenance_status_not_superadmin(client: flask.testing.FlaskClient) -> None: + """Check Maintenance mode status using everything but Super Admin access.""" + no_access_users: typing.Dict = users.copy() + no_access_users.pop("Super Admin") + + for u in no_access_users: + token: typing.Dict = get_token(username=users[u], client=client) + response: werkzeug.test.WrapperTestResponse = client.get( + tests.DDSEndpoint.MAINTENANCE, headers=token, json={"state": "status"} + ) + assert response.status_code == http.HTTPStatus.FORBIDDEN + + def test_set_maintenance_incorrect_method(client: flask.testing.FlaskClient) -> None: - """Only put should be accepted.""" + """Only put and get should be accepted.""" # Authenticate token: typing.Dict = get_token(username=users["Super Admin"], client=client) # Attempt request - for method in [client.get, client.post, client.delete, client.patch]: + for method in [client.post, client.delete, client.patch]: response: werkzeug.test.WrapperTestResponse = method( tests.DDSEndpoint.MAINTENANCE, headers=token, json={"state": "on"} ) assert response.status_code == http.HTTPStatus.METHOD_NOT_ALLOWED + def test_set_maintenance_no_json(client: flask.testing.FlaskClient) -> None: """The request needs json in order to change Maintenance mode.""" # Authenticate From 08fc046e19a7fc250093bc47b604115505d775ab Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:35:09 +0200 Subject: [PATCH 008/133] black --- tests/api/test_superadmin_only.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index 8ac6d95b5..63420739c 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -655,7 +655,6 @@ def test_set_maintenance_incorrect_method(client: flask.testing.FlaskClient) -> assert response.status_code == http.HTTPStatus.METHOD_NOT_ALLOWED - def test_set_maintenance_no_json(client: flask.testing.FlaskClient) -> None: """The request needs json in order to change Maintenance mode.""" # Authenticate From c5a11deca0944b7bfa7fc2e18040c66257c1b735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Tue, 29 Aug 2023 09:59:42 +0200 Subject: [PATCH 009/133] npm update --- dds_web/static/package-lock.json | 1239 +++++++++++++++++++----------- dds_web/static/package.json | 22 +- 2 files changed, 818 insertions(+), 443 deletions(-) diff --git a/dds_web/static/package-lock.json b/dds_web/static/package-lock.json index a274ecf1f..9485590b5 100644 --- a/dds_web/static/package-lock.json +++ b/dds_web/static/package-lock.json @@ -9,55 +9,118 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@popperjs/core": "^2.11.2", + "@popperjs/core": "^2.11.8", "bootstrap": "5.1.3", "bootstrap-dark-5": "1.1.3", - "datatables.net": "^1.11.5", - "datatables.net-bs5": "^1.11.5", - "jquery": "^3.6.0" + "datatables.net": "^1.13.6", + "datatables.net-bs5": "^1.13.6", + "jquery": "^3.7.1" }, "devDependencies": { - "autoprefixer": "^10.4.2", - "node-sass": "^7.0.1", - "nodemon": "^2.0.15", + "autoprefixer": "^10.4.15", + "node-sass": "^7.0.3", + "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", - "postcss": "^8.4.7", + "postcss": "^8.4.28", "postcss-cli": "^9.1.0", "purgecss": "^4.1.3", - "serve": "^13.0.2", - "stylelint": "^14.5.3", - "stylelint-config-twbs-bootstrap": "^3.0.1" + "serve": "^13.0.4", + "stylelint": "^14.16.1", + "stylelint-config-twbs-bootstrap": "^3.2.1" } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -127,19 +190,19 @@ } }, "node_modules/@csstools/selector-specificity": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz", - "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", "dev": true, "engines": { - "node": "^14 || ^16 || >=18" + "node": "^12 || ^14 || >=16" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.4", + "postcss": "^8.2", "postcss-selector-parser": "^6.0.10" } }, @@ -209,9 +272,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -282,13 +345,11 @@ } }, "node_modules/agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dev": true, "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" }, "engines": { @@ -415,6 +476,19 @@ "integrity": "sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w==", "dev": true }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", @@ -427,6 +501,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -479,9 +573,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", + "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", "dev": true, "funding": [ { @@ -491,11 +585,15 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001520", "fraction.js": "^4.2.0", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -654,9 +752,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "funding": [ { @@ -666,13 +764,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -768,9 +870,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001457", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", - "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", + "version": "1.0.30001524", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", + "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", "dev": true, "funding": [ { @@ -780,6 +882,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -1071,9 +1177,9 @@ } }, "node_modules/css-functions-list": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.1.0.tgz", - "integrity": "sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz", + "integrity": "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==", "dev": true, "engines": { "node": ">=12.22" @@ -1104,19 +1210,19 @@ } }, "node_modules/datatables.net": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.2.tgz", - "integrity": "sha512-u5nOU+C9SBp1SyPmd6G+niozZtrBwo1E8xzdOk3JJaAkFYgX/KxF3Gd79R8YLbUfmIs2OLnLe5gaz/qs5U8UDA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.6.tgz", + "integrity": "sha512-rHNcnW+yEP9me82/KmRcid5eKrqPqW3+I/p1TwqCW3c/7GRYYkDyF6aJQOQ9DNS/pw+nyr4BVpjyJ3yoZXiFPg==", "dependencies": { "jquery": ">=1.7" } }, "node_modules/datatables.net-bs5": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-1.13.2.tgz", - "integrity": "sha512-p1JOXFi+VD4wS0AxRmJPHBylJ8AC4xGJ3j0TZ+8hMspuZ+QvAhEqEPt8OEAGVzlN1ElfQimZjBeAoES/9nsuig==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-1.13.6.tgz", + "integrity": "sha512-lXroZoXhLhDulp8gvU7y7wBherg38SbLMGXcHwbnj+XXh4Hvy+d67zSPYbrVI3YiRwYq+aCx15G5qmMj7KjYQg==", "dependencies": { - "datatables.net": ">=1.12.1", + "datatables.net": ">=1.13.4", "jquery": ">=1.7" } }, @@ -1211,15 +1317,6 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", @@ -1252,9 +1349,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.305", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.305.tgz", - "integrity": "sha512-WETy6tG0CT5gm1O+xCbyapWNsCcmIvrn4NHViIGYo2AT8FV2qUCXdaB+WqYxSv/vS5mFqhBYnfZAAkVArjBmUg==", + "version": "1.4.504", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.504.tgz", + "integrity": "sha512-cSMwIAd8yUh54VwitVRVvHK66QqHWE39C3DRj8SWiXitEpVSY3wNPD9y1pxQtLIi4w3UdzF9klLsmuPshz09DQ==", "dev": true }, "node_modules/emoji-regex": { @@ -1307,18 +1404,19 @@ } }, "node_modules/es-abstract": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", - "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -1326,8 +1424,8 @@ "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", @@ -1335,16 +1433,21 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.10" }, "engines": { "node": ">= 0.4" @@ -1446,9 +1549,9 @@ } }, "node_modules/execa/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -1509,9 +1612,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -1595,16 +1698,17 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.7", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { @@ -1646,9 +1750,9 @@ } }, "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.1.tgz", + "integrity": "sha512-/KxoyCnPM0GwYI4NN0Iag38Tqt+od3/mLuguepLgCAKPn0ZhC544nssAW0tG2/00zXEYl9W+7hwAIpLHo6Oc7Q==", "dev": true, "engines": { "node": "*" @@ -1691,9 +1795,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -1711,15 +1815,15 @@ "dev": true }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -1779,13 +1883,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -2008,9 +2113,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/har-schema": { @@ -2145,9 +2250,9 @@ } }, "node_modules/html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true, "engines": { "node": ">=8" @@ -2342,13 +2447,13 @@ "dev": true }, "node_modules/is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" }, "funding": { @@ -2414,9 +2519,9 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -2613,16 +2718,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -2662,9 +2763,9 @@ } }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, "node_modules/isexe": { @@ -2680,9 +2781,9 @@ "dev": true }, "node_modules/jquery": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", - "integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "node_modules/js-base64": { "version": "2.6.4", @@ -2702,6 +2803,12 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -2759,6 +2866,15 @@ "node": ">=0.6.0" } }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2775,9 +2891,9 @@ "dev": true }, "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, "engines": { "node": ">=10" @@ -3135,10 +3251,16 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -3233,9 +3355,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "node_modules/node-sass": { @@ -3269,9 +3391,9 @@ } }, "node_modules/nodemon": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", - "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -3315,9 +3437,9 @@ } }, "node_modules/nodemon/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -3484,9 +3606,9 @@ } }, "node_modules/npm-run-all/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -3827,9 +3949,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", + "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", "dev": true, "funding": [ { @@ -3839,10 +3961,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -3969,9 +4095,9 @@ } }, "node_modules/postcss-scss": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", - "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.7.tgz", + "integrity": "sha512-xPv2GseoyXPa58Nro7M73ZntttusuCmZdeOojUFR5PZDz2BR62vfYx1w9TyOnp1+nYFowgOMipsCBhxzVkAEPw==", "dev": true, "funding": [ { @@ -3981,6 +4107,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "engines": { @@ -3991,9 +4121,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -4258,9 +4388,9 @@ } }, "node_modules/read-pkg-up/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -4315,18 +4445,18 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" } }, "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -4363,14 +4493,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -4452,12 +4582,12 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -4534,6 +4664,24 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4603,9 +4751,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4776,9 +4924,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", - "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4911,9 +5059,9 @@ } }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -4937,9 +5085,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "node_modules/sshpk": { @@ -4988,10 +5136,16 @@ "readable-stream": "^2.0.1" } }, + "node_modules/stdout-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/stdout-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -5058,6 +5212,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -5321,16 +5492,15 @@ } }, "node_modules/stylelint-scss": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.4.0.tgz", - "integrity": "sha512-Qy66a+/30aylFhPmUArHhVsHOun1qrO93LGT15uzLuLjWS7hKDfpFm34mYo1ndR4MCo8W4bEZM1+AlJRJORaaw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.7.0.tgz", + "integrity": "sha512-TSUgIeS0H3jqDZnby1UO1Qv3poi1N8wUYIJY6D1tuUq2MN3lwp/rITVo0wD+1SWTmRm0tNmGO0b7nKInnqF6Hg==", "dev": true, "dependencies": { - "lodash": "^4.17.21", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.6", - "postcss-value-parser": "^4.1.0" + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "stylelint": "^14.5.1 || ^15.0.0" @@ -5462,14 +5632,14 @@ "dev": true }, "node_modules/tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "dev": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -5479,9 +5649,9 @@ } }, "node_modules/tar/node_modules/minipass": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.0.tgz", - "integrity": "sha512-ExlilAIS7zJ2EWUMaVXi14H+FnZ18kr17kFkGemMqBx6jW0m8P6XfqwYVPEG53ENlgsED+alVP9ZxC3JzkK23Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, "engines": { "node": ">=8" @@ -5602,6 +5772,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -5665,9 +5886,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "funding": [ { @@ -5677,6 +5898,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -5684,7 +5909,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -5735,9 +5960,9 @@ } }, "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, "node_modules/validate-npm-package-license": { @@ -5805,17 +6030,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5906,9 +6130,9 @@ } }, "node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -5944,28 +6168,81 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", "dev": true }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", + "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -6022,9 +6299,9 @@ } }, "@csstools/selector-specificity": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz", - "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz", + "integrity": "sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==", "dev": true, "requires": {} }, @@ -6081,9 +6358,9 @@ } }, "@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@tootallnate/once": { "version": "1.1.2", @@ -6141,13 +6418,11 @@ } }, "agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dev": true, "requires": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" } }, @@ -6235,12 +6510,36 @@ "integrity": "sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w==", "dev": true }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-union": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", "dev": true }, + "arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -6281,13 +6580,13 @@ "dev": true }, "autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "version": "10.4.15", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz", + "integrity": "sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==", "dev": true, "requires": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001520", "fraction.js": "^4.2.0", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -6397,15 +6696,15 @@ } }, "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" } }, "bytes": { @@ -6474,9 +6773,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001457", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", - "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", + "version": "1.0.30001524", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz", + "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==", "dev": true }, "caseless": { @@ -6703,9 +7002,9 @@ } }, "css-functions-list": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.1.0.tgz", - "integrity": "sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz", + "integrity": "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==", "dev": true }, "cssesc": { @@ -6724,19 +7023,19 @@ } }, "datatables.net": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.2.tgz", - "integrity": "sha512-u5nOU+C9SBp1SyPmd6G+niozZtrBwo1E8xzdOk3JJaAkFYgX/KxF3Gd79R8YLbUfmIs2OLnLe5gaz/qs5U8UDA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.13.6.tgz", + "integrity": "sha512-rHNcnW+yEP9me82/KmRcid5eKrqPqW3+I/p1TwqCW3c/7GRYYkDyF6aJQOQ9DNS/pw+nyr4BVpjyJ3yoZXiFPg==", "requires": { "jquery": ">=1.7" } }, "datatables.net-bs5": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-1.13.2.tgz", - "integrity": "sha512-p1JOXFi+VD4wS0AxRmJPHBylJ8AC4xGJ3j0TZ+8hMspuZ+QvAhEqEPt8OEAGVzlN1ElfQimZjBeAoES/9nsuig==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-1.13.6.tgz", + "integrity": "sha512-lXroZoXhLhDulp8gvU7y7wBherg38SbLMGXcHwbnj+XXh4Hvy+d67zSPYbrVI3YiRwYq+aCx15G5qmMj7KjYQg==", "requires": { - "datatables.net": ">=1.12.1", + "datatables.net": ">=1.13.4", "jquery": ">=1.7" } }, @@ -6801,12 +7100,6 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, "dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", @@ -6833,9 +7126,9 @@ } }, "electron-to-chromium": { - "version": "1.4.305", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.305.tgz", - "integrity": "sha512-WETy6tG0CT5gm1O+xCbyapWNsCcmIvrn4NHViIGYo2AT8FV2qUCXdaB+WqYxSv/vS5mFqhBYnfZAAkVArjBmUg==", + "version": "1.4.504", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.504.tgz", + "integrity": "sha512-cSMwIAd8yUh54VwitVRVvHK66QqHWE39C3DRj8SWiXitEpVSY3wNPD9y1pxQtLIi4w3UdzF9klLsmuPshz09DQ==", "dev": true }, "emoji-regex": { @@ -6885,18 +7178,19 @@ } }, "es-abstract": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", - "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", "dev": true, "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -6904,8 +7198,8 @@ "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", @@ -6913,16 +7207,21 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.10" } }, "es-set-tostringtag": { @@ -6994,9 +7293,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -7044,9 +7343,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -7115,12 +7414,13 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", "dev": true, "requires": { - "flatted": "^3.1.0", + "flatted": "^3.2.7", + "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, @@ -7157,9 +7457,9 @@ } }, "fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.1.tgz", + "integrity": "sha512-/KxoyCnPM0GwYI4NN0Iag38Tqt+od3/mLuguepLgCAKPn0ZhC544nssAW0tG2/00zXEYl9W+7hwAIpLHo6Oc7Q==", "dev": true }, "fs-extra": { @@ -7189,9 +7489,9 @@ "dev": true }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "optional": true }, @@ -7202,15 +7502,15 @@ "dev": true }, "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, "functions-have-names": { @@ -7252,13 +7552,14 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -7425,9 +7726,9 @@ } }, "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "har-schema": { @@ -7519,9 +7820,9 @@ } }, "html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true }, "http-cache-semantics": { @@ -7675,13 +7976,13 @@ "dev": true }, "is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "requires": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" } }, @@ -7726,9 +8027,9 @@ "dev": true }, "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -7853,16 +8154,12 @@ } }, "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" } }, "is-typedarray": { @@ -7890,9 +8187,9 @@ } }, "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, "isexe": { @@ -7908,9 +8205,9 @@ "dev": true }, "jquery": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz", - "integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "js-base64": { "version": "2.6.4", @@ -7930,6 +8227,12 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -7982,6 +8285,15 @@ "verror": "1.10.0" } }, + "keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -7995,9 +8307,9 @@ "dev": true }, "lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true }, "lines-and-columns": { @@ -8267,9 +8579,9 @@ "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", "dev": true }, "negotiator": { @@ -8343,9 +8655,9 @@ } }, "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "node-sass": { @@ -8372,9 +8684,9 @@ } }, "nodemon": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", - "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", "dev": true, "requires": { "chokidar": "^3.5.2", @@ -8405,9 +8717,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "supports-color": { @@ -8532,9 +8844,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -8783,12 +9095,12 @@ "dev": true }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz", + "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -8861,16 +9173,16 @@ "requires": {} }, "postcss-scss": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", - "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.7.tgz", + "integrity": "sha512-xPv2GseoyXPa58Nro7M73ZntttusuCmZdeOojUFR5PZDz2BR62vfYx1w9TyOnp1+nYFowgOMipsCBhxzVkAEPw==", "dev": true, "requires": {} }, "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", "dev": true, "requires": { "cssesc": "^3.0.0", @@ -9048,9 +9360,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -9117,9 +9429,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "type-fest": { @@ -9131,9 +9443,9 @@ } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -9161,14 +9473,14 @@ } }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" } }, "registry-auth-token": { @@ -9231,12 +9543,12 @@ "dev": true }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -9277,6 +9589,18 @@ "queue-microtask": "^1.2.2" } }, + "safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9323,9 +9647,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -9464,9 +9788,9 @@ "dev": true }, "shell-quote": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", - "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true }, "side-channel": { @@ -9560,9 +9884,9 @@ "dev": true }, "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -9586,9 +9910,9 @@ } }, "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "sshpk": { @@ -9626,10 +9950,16 @@ "readable-stream": "^2.0.1" }, "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -9689,6 +10019,17 @@ "es-abstract": "^1.20.4" } }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, "string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -9929,16 +10270,15 @@ } }, "stylelint-scss": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.4.0.tgz", - "integrity": "sha512-Qy66a+/30aylFhPmUArHhVsHOun1qrO93LGT15uzLuLjWS7hKDfpFm34mYo1ndR4MCo8W4bEZM1+AlJRJORaaw==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-4.7.0.tgz", + "integrity": "sha512-TSUgIeS0H3jqDZnby1UO1Qv3poi1N8wUYIJY6D1tuUq2MN3lwp/rITVo0wD+1SWTmRm0tNmGO0b7nKInnqF6Hg==", "dev": true, "requires": { - "lodash": "^4.17.21", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.6", - "postcss-value-parser": "^4.1.0" + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" } }, "supports-color": { @@ -10006,23 +10346,23 @@ } }, "tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", "dev": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "dependencies": { "minipass": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.0.tgz", - "integrity": "sha512-ExlilAIS7zJ2EWUMaVXi14H+FnZ18kr17kFkGemMqBx6jW0m8P6XfqwYVPEG53ENlgsED+alVP9ZxC3JzkK23Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true } } @@ -10116,6 +10456,42 @@ "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", "dev": true }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -10170,9 +10546,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -10219,9 +10595,9 @@ "dev": true }, "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, "validate-npm-package-license": { @@ -10274,17 +10650,16 @@ } }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, "wide-align": { @@ -10351,9 +10726,9 @@ "dev": true }, "yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { "cliui": "^8.0.1", diff --git a/dds_web/static/package.json b/dds_web/static/package.json index 4150d2589..d27e864bd 100644 --- a/dds_web/static/package.json +++ b/dds_web/static/package.json @@ -25,23 +25,23 @@ "SciLifeLab" ], "dependencies": { - "@popperjs/core": "^2.11.2", + "@popperjs/core": "^2.11.8", "bootstrap": "5.1.3", "bootstrap-dark-5": "1.1.3", - "datatables.net": "^1.11.5", - "datatables.net-bs5": "^1.11.5", - "jquery": "^3.6.0" + "datatables.net": "^1.13.6", + "datatables.net-bs5": "^1.13.6", + "jquery": "^3.7.1" }, "devDependencies": { - "autoprefixer": "^10.4.2", - "node-sass": "^7.0.1", - "nodemon": "^2.0.15", + "autoprefixer": "^10.4.15", + "node-sass": "^7.0.3", + "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", - "postcss": "^8.4.7", + "postcss": "^8.4.28", "postcss-cli": "^9.1.0", "purgecss": "^4.1.3", - "serve": "^13.0.2", - "stylelint": "^14.5.3", - "stylelint-config-twbs-bootstrap": "^3.0.1" + "serve": "^13.0.4", + "stylelint": "^14.16.1", + "stylelint-config-twbs-bootstrap": "^3.2.1" } } From 96f9588b3c19b800bb64e2090e91b68098e8e27f Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:46:40 +0200 Subject: [PATCH 010/133] more tests --- tests/api/test_superadmin_only.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index 63420739c..714c0e54f 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -642,7 +642,7 @@ def test_get_maintenance_status_not_superadmin(client: flask.testing.FlaskClient assert response.status_code == http.HTTPStatus.FORBIDDEN -def test_set_maintenance_incorrect_method(client: flask.testing.FlaskClient) -> None: +def test_maintenance_command_incorrect_method(client: flask.testing.FlaskClient) -> None: """Only put and get should be accepted.""" # Authenticate token: typing.Dict = get_token(username=users["Super Admin"], client=client) @@ -666,6 +666,17 @@ def test_set_maintenance_no_json(client: flask.testing.FlaskClient) -> None: assert "Required data missing from request" in response.json.get("message") +def test_get_maintenance_status_no_json_required(client: flask.testing.FlaskClient) -> None: + """No json needed in order to get the current Maintenance mode.""" + # Authenticate + token: typing.Dict = get_token(username=users["Super Admin"], client=client) + + # Attempt request + response: werkzeug.test.TestResponse = client.get(tests.DDSEndpoint.MAINTENANCE, headers=token) + assert response.status_code == http.HTTPStatus.OK + assert "Maintenanse mode is set to:" in response.json.get("message") + + def test_set_maintenance_incorrect_state(client: flask.testing.FlaskClient) -> None: """The json should be 'on' or 'off'.""" # Authenticate @@ -704,6 +715,11 @@ def test_set_maintenance_on_ok(client: flask.testing.FlaskClient) -> None: assert response.status_code == http.HTTPStatus.OK assert f"Maintenance set to: {setting.upper()}" in response.json.get("message") + # Verify that maintenance is set to ON using the get method + response: werkzeug.test.TestResponse = client.get(tests.DDSEndpoint.MAINTENANCE, headers=token) + assert response.status_code == http.HTTPStatus.OK + assert f"Maintenanse mode is set to: {setting.upper()}" in response.json.get("message") + def test_set_maintenance_off_ok(client: flask.testing.FlaskClient) -> None: """Set Maintenance mode to 'off'.""" @@ -726,6 +742,11 @@ def test_set_maintenance_off_ok(client: flask.testing.FlaskClient) -> None: assert response.status_code == http.HTTPStatus.OK assert f"Maintenance set to: {setting.upper()}" in response.json.get("message") + # Verify that maintenance is set to OFF using the get method + response: werkzeug.test.TestResponse = client.get(tests.DDSEndpoint.MAINTENANCE, headers=token) + assert response.status_code == http.HTTPStatus.OK + assert f"Maintenanse mode is set to: {setting.upper()}" in response.json.get("message") + # AnyProjectsBusy From 89a16ba6861b834bef0c78d7f576e73d8c026cf0 Mon Sep 17 00:00:00 2001 From: Valentin Georgiev Date: Thu, 31 Aug 2023 11:39:42 +0200 Subject: [PATCH 011/133] review suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- dds_web/api/superadmin_only.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index 45808a5e6..1f71fffa2 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -291,12 +291,7 @@ def put(self): def get(self): """Return current Maintenance mode.""" current_mode = models.Maintenance.query.first() - if not current_mode.active: - mode = "OFF" - else: - mode = "ON" - - return {"message": f"Maintenanse mode is set to: {mode}"} +return {"message": f"Maintenance mode is set to: {'ON' if current_mode.active else 'OFF'}"} class AnyProjectsBusy(flask_restful.Resource): From 9218360ae0614cf15d5bda02008e0b3f46e71b4f Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Thu, 31 Aug 2023 11:51:50 +0200 Subject: [PATCH 012/133] fix indentation --- dds_web/api/superadmin_only.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index 1f71fffa2..aa5a1e28d 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -291,7 +291,8 @@ def put(self): def get(self): """Return current Maintenance mode.""" current_mode = models.Maintenance.query.first() -return {"message": f"Maintenance mode is set to: {'ON' if current_mode.active else 'OFF'}"} + + return {"message": f"Maintenance mode is set to: {'ON' if current_mode.active else 'OFF'}"} class AnyProjectsBusy(flask_restful.Resource): From 854bdfa3cb0fe89c122cb585e83b9565e5ea0ccd Mon Sep 17 00:00:00 2001 From: Valentin Georgiev Date: Thu, 31 Aug 2023 11:54:52 +0200 Subject: [PATCH 013/133] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_superadmin_only.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index 714c0e54f..4912e53a9 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -674,7 +674,7 @@ def test_get_maintenance_status_no_json_required(client: flask.testing.FlaskClie # Attempt request response: werkzeug.test.TestResponse = client.get(tests.DDSEndpoint.MAINTENANCE, headers=token) assert response.status_code == http.HTTPStatus.OK - assert "Maintenanse mode is set to:" in response.json.get("message") + assert "Maintenance mode is set to:" in response.json.get("message") def test_set_maintenance_incorrect_state(client: flask.testing.FlaskClient) -> None: @@ -718,7 +718,7 @@ def test_set_maintenance_on_ok(client: flask.testing.FlaskClient) -> None: # Verify that maintenance is set to ON using the get method response: werkzeug.test.TestResponse = client.get(tests.DDSEndpoint.MAINTENANCE, headers=token) assert response.status_code == http.HTTPStatus.OK - assert f"Maintenanse mode is set to: {setting.upper()}" in response.json.get("message") + assert f"Maintenance mode is set to: {setting.upper()}" in response.json.get("message") def test_set_maintenance_off_ok(client: flask.testing.FlaskClient) -> None: @@ -745,7 +745,7 @@ def test_set_maintenance_off_ok(client: flask.testing.FlaskClient) -> None: # Verify that maintenance is set to OFF using the get method response: werkzeug.test.TestResponse = client.get(tests.DDSEndpoint.MAINTENANCE, headers=token) assert response.status_code == http.HTTPStatus.OK - assert f"Maintenanse mode is set to: {setting.upper()}" in response.json.get("message") + assert f"Maintenance mode is set to: {setting.upper()}" in response.json.get("message") # AnyProjectsBusy From b7f408d8c62b15d7077096b27579503decfac373 Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:27:35 +0200 Subject: [PATCH 014/133] change test --- tests/api/test_superadmin_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index 4912e53a9..4d7fab7ec 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -637,7 +637,7 @@ def test_get_maintenance_status_not_superadmin(client: flask.testing.FlaskClient for u in no_access_users: token: typing.Dict = get_token(username=users[u], client=client) response: werkzeug.test.WrapperTestResponse = client.get( - tests.DDSEndpoint.MAINTENANCE, headers=token, json={"state": "status"} + tests.DDSEndpoint.MAINTENANCE, headers=token ) assert response.status_code == http.HTTPStatus.FORBIDDEN From db0a8968fc5873a7c724e72820ac7b2ab2d4a1a7 Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:44:43 +0200 Subject: [PATCH 015/133] add sprintlog entry --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 8a17e9827..71e3a8c1b 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -290,3 +290,4 @@ _Nothing merged in CLI during this sprint_ - Dependency: Bump `certifi` to 2023.07.22 due to security vulnerability alert(s) ([#1452](https://github.com/ScilifelabDataCentre/dds_web/pull/1452)) - New version: 2.5.0 ([#1458](https://github.com/ScilifelabDataCentre/dds_web/pull/1458)) +- Added check for Maintenance mode status in SetMaintenance endpoint ([#1459](https://github.com/ScilifelabDataCentre/dds_web/pull/1459)) From a67e251ce432bb044229a630321bafbe22b97b6b Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:16:02 +0200 Subject: [PATCH 016/133] change endpoint to MaintenanceMode --- SPRINTLOG.md | 2 +- dds_web/api/__init__.py | 2 +- dds_web/api/superadmin_only.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 71e3a8c1b..2fee33be4 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -290,4 +290,4 @@ _Nothing merged in CLI during this sprint_ - Dependency: Bump `certifi` to 2023.07.22 due to security vulnerability alert(s) ([#1452](https://github.com/ScilifelabDataCentre/dds_web/pull/1452)) - New version: 2.5.0 ([#1458](https://github.com/ScilifelabDataCentre/dds_web/pull/1458)) -- Added check for Maintenance mode status in SetMaintenance endpoint ([#1459](https://github.com/ScilifelabDataCentre/dds_web/pull/1459)) +- Added check for Maintenance mode status in MaintenanceMode endpoint ([#1459](https://github.com/ScilifelabDataCentre/dds_web/pull/1459)) diff --git a/dds_web/api/__init__.py b/dds_web/api/__init__.py index 380b8c51f..3a7da23ff 100644 --- a/dds_web/api/__init__.py +++ b/dds_web/api/__init__.py @@ -81,7 +81,7 @@ def output_json(data, code, headers=None): # Super Admins ###################################################################### Super Admins # -api.add_resource(superadmin_only.SetMaintenance, "/maintenance", endpoint="maintenance") +api.add_resource(superadmin_only.MaintenanceMode, "/maintenance", endpoint="maintenance") api.add_resource(superadmin_only.AllUnits, "/unit/info/all", endpoint="all_units") api.add_resource(superadmin_only.MOTD, "/motd", endpoint="motd") api.add_resource(superadmin_only.SendMOTD, "/motd/send", endpoint="send_motd") diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index aa5a1e28d..ecb528c48 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -256,7 +256,7 @@ def put(self): } -class SetMaintenance(flask_restful.Resource): +class MaintenanceMode(flask_restful.Resource): """Change the maintenance mode of the system.""" @auth.login_required(role=["Super Admin"]) From 202b101a23d4ef2828dbaacf97c2bd68c904de2d Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:58:13 +0200 Subject: [PATCH 017/133] added check for the columns in the stats test --- tests/api/test_superadmin_only.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index ef7c7587a..fa46b9343 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -899,6 +899,8 @@ def add_row_to_reporting_table(time): "TBHours Last Month": reporting_row.tbhours, "TBHours Total": reporting_row.tbhours_since_start, } + returned_columns: typing.Dict = response.json.get("columns") + assert returned_columns # UnitUserEmails From 8cfd43c42536df68186246448b286beadab4aad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 09:24:16 +0200 Subject: [PATCH 018/133] add rollback --- dds_web/api/project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dds_web/api/project.py b/dds_web/api/project.py index da18a9340..1311b49a3 100644 --- a/dds_web/api/project.py +++ b/dds_web/api/project.py @@ -678,6 +678,8 @@ def post(self): new_project = project_schemas.CreateProjectSchema().load(p_info) db.session.add(new_project) except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.SQLAlchemyError) as err: + flask.current_app.logger.info("Doing db rollback.") + db.session.rollback() raise DatabaseError(message=str(err), alt_message="Unexpected database error.") if not new_project: From 139f320a157e6238e938e20b205d7f7f01811c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 09:28:43 +0200 Subject: [PATCH 019/133] springlog --- SPRINTLOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 2fee33be4..3ae60d991 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -291,3 +291,7 @@ _Nothing merged in CLI during this sprint_ - Dependency: Bump `certifi` to 2023.07.22 due to security vulnerability alert(s) ([#1452](https://github.com/ScilifelabDataCentre/dds_web/pull/1452)) - New version: 2.5.0 ([#1458](https://github.com/ScilifelabDataCentre/dds_web/pull/1458)) - Added check for Maintenance mode status in MaintenanceMode endpoint ([#1459](https://github.com/ScilifelabDataCentre/dds_web/pull/1459)) + +# 2023-09-04 - 2023-09-15 + +- Bug fix: Database rollback added on project creation failure ([#1461](https://github.com/ScilifelabDataCentre/dds_web/pull/1461)) From 678efdd120c7d9ab7462591493b689443cf36c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 13:04:06 +0200 Subject: [PATCH 020/133] move tests and add new for sqlalchemyerror --- tests/api/test_project.py | 891 +++++++++++++++++++++++++- tests/test_project_creation.py | 836 ------------------------ tests/test_user_remove_association.py | 2 +- tests/test_utils.py | 8 +- 4 files changed, 876 insertions(+), 861 deletions(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index ae51b79b7..f15d795dc 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -1,13 +1,16 @@ # IMPORTS ################################################################################ IMPORTS # # Standard library +import os import http from sqlite3 import OperationalError import pytest import datetime import time +import unittest import unittest.mock + # Installed import boto3 import flask_mail @@ -20,16 +23,43 @@ from dds_web.errors import BucketNotFoundError, DatabaseError, DeletionError import tests from tests.test_files_new import project_row, file_in_db, FIRST_NEW_FILE -from tests.test_project_creation import proj_data_with_existing_users, create_unit_admins from dds_web.database import models from dds_web.api.project import UserProjects +from dds_web.utils import current_time # CONFIG ################################################################################## CONFIG # - proj_data = { "pi": "researchuser@mailtrap.io", "title": "Test proj", "description": "A longer project description", +} + +proj_data_with_existing_users = { + **proj_data, + "users_to_add": [ + {"email": "researchuser@mailtrap.io", "role": "Project Owner"}, + {"email": "researchuser2@mailtrap.io", "role": "Researcher"}, + ], +} +proj_data_with_nonexisting_users = { + **proj_data, + "users_to_add": [ + {"email": "non_existing_user@mailtrap.io", "role": "Project Owner"}, + {"email": "non_existing_user2@mailtrap.io", "role": "Researcher"}, + ], +} +proj_data_with_unsuitable_user_roles = { + **proj_data, + "users_to_add": [ + {"email": "researchuser@mailtrap.io", "role": "Unit Admin"}, + {"email": "researchuser2@mailtrap.io", "role": "Unit Personnel"}, + ], +} + +proj_data_plus_users = { + "pi": "researchuser@mailtrap.io", + "title": "Test proj", + "description": "A longer project description", "users_to_add": [{"email": "researchuser2@mailtrap.io", "role": "Project Owner"}], } fields_set_to_null = [ @@ -45,6 +75,25 @@ ] +def create_unit_admins(num_admins, unit_id=1): + new_admins = [] + for i in range(1, num_admins + 1): + new_admins.append( + models.UnitUser( + **{ + "username": "unit_admin_" + os.urandom(4).hex(), + "name": "Unit Admin " + str(i), + "password": "password", + "is_admin": True, + "unit_id": unit_id, + } + ) + ) + + db.session.add_all(new_admins) + db.session.commit() + + @pytest.fixture(scope="module") def test_project(module_client): """Create a shared test project""" @@ -52,7 +101,7 @@ def test_project(module_client): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) project_id = response.json.get("project_id") # add a file @@ -88,7 +137,7 @@ def test_projectstatus_get_status_without_args(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -125,7 +174,7 @@ def test_projectstatus_get_status_with_empty_args(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -153,7 +202,7 @@ def test_projectstatus_get_status_with_invalid_project(module_client, boto3_sess response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -290,7 +339,7 @@ def test_projectstatus_submit_request_with_invalid_args(module_client, boto3_ses response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -338,7 +387,7 @@ def test_projectstatus_post_operationalerror(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -369,7 +418,7 @@ def test_projectstatus_set_project_to_deleted_from_in_progress(module_client, bo response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -427,7 +476,7 @@ def test_projectstatus_archived_project(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -477,7 +526,7 @@ def test_projectstatus_aborted_project(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -533,7 +582,7 @@ def test_projectstatus_abort_from_in_progress_once_made_available(module_client, response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -624,7 +673,7 @@ def test_projectstatus_check_invalid_transitions_from_in_progress(module_client, response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK @@ -1025,7 +1074,7 @@ def test_projectstatus_post_invalid_deadline_release(module_client, boto3_sessio response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1053,7 +1102,7 @@ def test_projectstatus_post_invalid_deadline_expire(module_client, boto3_session response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1090,7 +1139,7 @@ def test_projectstatus_post_deletion_and_archivation_errors(module_client, boto3 response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1132,7 +1181,7 @@ def test_projectstatus_post_archiving_without_aborting(module_client, boto3_sess response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1183,7 +1232,7 @@ def test_projectstatus_post_deletion_and_archivation_errors(module_client, boto3 response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1238,7 +1287,7 @@ def test_getpublic_publickey_is_none(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1271,7 +1320,7 @@ def test_getpublic_publickey(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data, + json=proj_data_plus_users, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1388,3 +1437,805 @@ def test_project_usage(module_client): # Call project_usage() for the project and check if cost is calculated correctly proj_bhours, proj_cost = UserProjects.project_usage(project=project_0) assert (proj_bhours / 1e9) * cost_gbhour == proj_cost + + +# CreateProject + + +def test_create_project_too_few_unit_admins(client): + """There needs to be at least 2 Unit Admins.""" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.FORBIDDEN + response_json = response.json + assert response_json + assert "Your unit does not have enough Unit Admins" in response_json.get("message") + + +def test_create_project_two_unit_admins(client): + """There needs to be at least 2 Unit Admins.""" + create_unit_admins(num_admins=1) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 2 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + response_json = response.json + assert response_json + assert "Your unit only has 2 Unit Admins" in response_json.get("warning") + + +def test_create_project_two_unit_admins_force(client): + """The force option (not in cli) can be used to create a project even if there are + less than 3 Unit Admins.""" + create_unit_admins(num_admins=1) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 2 + + # Use force + updated_proj_data = proj_data.copy() + updated_proj_data["force"] = True + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=updated_proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + created_proj = models.Project.query.filter_by( + created_by="unitadmin", + title=updated_proj_data["title"], + pi=updated_proj_data["pi"], + description=updated_proj_data["description"], + ).one_or_none() + assert created_proj + + +def test_create_project_two_unit_admins_force(client): + """The force option (not in cli) can be used to create a project even if there are + less than 3 Unit Admins.""" + create_unit_admins(num_admins=1) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 2 + + # Use force + updated_proj_data = proj_data.copy() + updated_proj_data["force"] = "not correct" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=updated_proj_data, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + created_proj = models.Project.query.filter_by( + created_by="unitadmin", + title=updated_proj_data["title"], + pi=updated_proj_data["pi"], + description=updated_proj_data["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_empty(client): + """Make empty request.""" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert response_json + assert "Required data missing from request" in response_json.get("message") + + +def test_create_project_unknown_field(client): + """Make request with unknown field passed.""" + # Make sure there's 3 unit admins for unit + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Attempt creating project + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json={"test": "test"}, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + response_json + and "title" in response_json + and response_json["title"].get("message") == "Title is required." + ) + + +def test_create_project_missing_title(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_no_title = proj_data.copy() + proj_data_no_title.pop("title") + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_no_title, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "title" in response_json and response_json["title"].get("message") == "Title is required." + ) + + +def test_create_project_none_title(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_none_title = proj_data.copy() + proj_data_none_title["title"] = None + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_none_title, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "title" in response_json and response_json["title"].get("message") == "Title is required." + ) + + +def test_create_project_no_description(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_no_description = proj_data.copy() + proj_data_no_description.pop("description") + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_no_description, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "description" in response_json + and response_json["description"].get("message") == "A project description is required." + ) + + +def test_create_project_none_description(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_none_description = proj_data.copy() + proj_data_none_description["description"] = None + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_none_description, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "description" in response_json + and response_json["description"].get("message") == "A project description is required." + ) + + +def test_create_project_no_pi(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_no_pi = proj_data.copy() + proj_data_no_pi.pop("pi") + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_no_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "pi" in response_json + and response_json["pi"].get("message") == "A principal investigator is required." + ) + + +def test_create_project_none_pi(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_none_pi = proj_data.copy() + proj_data_none_pi["pi"] = None + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_none_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "pi" in response_json + and response_json["pi"].get("message") == "A principal investigator is required." + ) + + +def test_create_project_without_credentials(client): + """Create project without valid user credentials.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["researchuser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.FORBIDDEN + created_proj = models.Project.query.filter_by( + created_by="researchuser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert created_proj is None + + +def test_create_project_with_credentials(client, boto3_session): + """Create project with correct credentials.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + time_before_run = datetime.datetime.utcnow() + time.sleep(1) + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert ( + created_proj + and created_proj.date_created > time_before_run + and not created_proj.non_sensitive + ) + + +def test_create_project_no_title(client): + """Create project without a title specified.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json={"pi": "piName"}, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + pi=proj_data["pi"], + ).one_or_none() + assert created_proj is None + + +def test_create_project_title_too_short(client): + """Create a project with too short title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_short_title = proj_data.copy() + proj_data_short_title["title"] = "" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_short_title, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_short_title["title"], + pi=proj_data_short_title["pi"], + description=proj_data_short_title["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_with_malformed_json(client): + """Create a project with malformed project info.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json="", + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + created_proj = models.Project.query.filter_by( + created_by="unituser", + title="", + pi="", + description="", + ).one_or_none() + assert created_proj is None + + +def test_create_project_sensitive(client, boto3_session): + """Create a sensitive project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + p_data = proj_data + p_data["non_sensitive"] = False + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=p_data, + ) + assert response.status_code == http.HTTPStatus.OK + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert created_proj and not created_proj.non_sensitive + + +def test_create_project_description_too_short(client): + """Create a project with too short description.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_short_description = proj_data.copy() + proj_data_short_description["description"] = "" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_short_description, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_short_description["title"], + pi=proj_data_short_description["pi"], + description=proj_data_short_description["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_pi_too_short(client): + """Create a project with too short PI.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_short_pi = proj_data.copy() + proj_data_short_pi["pi"] = "" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_short_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_short_pi["title"], + pi=proj_data_short_pi["pi"], + description=proj_data_short_pi["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_pi_too_long(client): + """Create a project with too long PI.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_long_pi = proj_data.copy() + proj_data_long_pi["pi"] = "pi" * 128 + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_long_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_long_pi["title"], + pi=proj_data_long_pi["pi"], + description=proj_data_long_pi["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_wrong_status(client, boto3_session): + """Create a project with own status, should be overridden.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_wrong_status = proj_data.copy() + proj_data_wrong_status["status"] = "Incorrect Status" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_wrong_status, + ) + assert response.status_code == http.HTTPStatus.OK + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_wrong_status["title"], + pi=proj_data_wrong_status["pi"], + description=proj_data_wrong_status["description"], + ).one_or_none() + assert created_proj and created_proj.current_status == "In Progress" + + +def test_create_project_sensitive_not_boolean(client): + """Create project with incorrect non_sensitive format.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_sensitive_not_boolean = proj_data.copy() + proj_data_sensitive_not_boolean["non_sensitive"] = "test" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_sensitive_not_boolean, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_sensitive_not_boolean["title"], + pi=proj_data_sensitive_not_boolean["pi"], + description=proj_data_sensitive_not_boolean["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_date_created_overridden(client, boto3_session): + """Create project with own date_created, should be overridden.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_date_created_own = proj_data.copy() + proj_data_date_created_own["date_created"] = "test" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_date_created_own, + ) + assert response.status_code == http.HTTPStatus.OK + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_date_created_own["title"], + pi=proj_data_date_created_own["pi"], + description=proj_data_date_created_own["description"], + ).one_or_none() + assert created_proj and created_proj.date_created != proj_data_date_created_own["date_created"] + + +def test_create_project_with_users(client, boto3_session): + """Create project and add users to the project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_with_existing_users, + ) + assert response.status_code == http.HTTPStatus.OK + assert response.json and response.json.get("user_addition_statuses") + for x in response.json.get("user_addition_statuses"): + assert "given access to the Project" in x + + resp_json = response.json + created_proj = models.Project.query.filter_by(public_id=resp_json["project_id"]).one_or_none() + assert created_proj + users = models.ProjectUsers.query.filter_by(project_id=created_proj.id).all() + users_dict_from_db = [] + + for user in users: + users_dict_from_db.append({"username": user.user_id, "owner": user.owner}) + + users_dict_from_email = [] + for user in proj_data_with_existing_users["users_to_add"]: + email = models.Email.query.filter_by(email=user["email"]).one_or_none() + users_dict_from_email.append( + { + "username": email.user_id, + "owner": True if user.get("role") == "Project Owner" else False, + } + ) + + case = unittest.TestCase() + case.assertCountEqual(users_dict_from_db, users_dict_from_email) + + +def test_create_project_with_invited_users(client, boto3_session): + """Create project and invite users to the project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_with_nonexisting_users, + ) + assert response.status_code == http.HTTPStatus.OK + assert response.json and response.json.get("user_addition_statuses") + for x in response.json.get("user_addition_statuses"): + assert "Invitation sent" in x + + +def test_create_project_with_unsuitable_roles(client, boto3_session): + """Create project and add users with unsuitable roles to the project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_with_unsuitable_user_roles, + ) + assert response.status_code == http.HTTPStatus.OK + assert response.json and response.json.get("user_addition_statuses") + for x in response.json.get("user_addition_statuses"): + assert "User Role should be either 'Project Owner' or 'Researcher'" in x + + +def test_create_project_valid_characters(client, boto3_session): + """Create a project with no unicode.""" + # Project info with valid characters + proj_data_val_chars = proj_data.copy() + proj_data_val_chars["description"] = "A longer project description !#¤%&/()=?¡@£$€¥{[]}\\" + + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_val_chars, + ) + assert response.status_code == http.HTTPStatus.OK + + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data_val_chars["description"]) + .first() + ) + assert new_project + + +def test_create_project_invalid_characters(client, boto3_session): + """Create a project with unicode characters.""" + # Project info with invalid characters + proj_data_inval_chars = proj_data.copy() + proj_data_inval_chars["description"] = "A longer project description \U0001F300 \U0001F601" + + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_inval_chars, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + assert ( + response.json + and response.json.get("description") + and isinstance(response.json.get("description"), list) + ) + assert response.json["description"][0] == "This input is not allowed: \U0001F300\U0001F601" + + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data_inval_chars["description"]) + .first() + ) + assert not new_project + + +def test_create_project_sto2(client, boto3_session, capfd): + """Create a project in sto2.""" + # Make sure there are 3 unit admins + create_unit_admins(num_admins=2) + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Use sto2 -- all sto4 vars not set --------------------- + unit: models.Unit = models.Unit.query.filter_by(id=1).first() + assert unit + assert not all( + [ + unit.sto4_start_time, + unit.sto4_endpoint, + unit.sto4_name, + unit.sto4_access, + unit.sto4_secret, + ] + ) + + # Create project + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + + # Verify that new project is created + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data["description"]) + .first() + ) + assert new_project + + # Verify logging + _, err = capfd.readouterr() + assert f"Safespring location for project '{new_project.public_id}': sto2" in err + + +def test_create_project_sto4(client, boto3_session, capfd): + """Create a project in sto4.""" + # Make sure there are 3 unit admins + create_unit_admins(num_admins=2) + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Use sto4 + unit: models.Unit = models.Unit.query.filter_by(id=1).first() + assert unit + unit.sto4_start_time = current_time() + unit.sto4_endpoint = "endpoint" + unit.sto4_name = "name" + unit.sto4_access = "access" + unit.sto4_secret = "secret" + db.session.commit() + assert all( + [ + unit.sto4_start_time, + unit.sto4_endpoint, + unit.sto4_name, + unit.sto4_access, + unit.sto4_secret, + ] + ) + + # Create project + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + + # Verify that new project is created + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data["description"]) + .first() + ) + assert new_project + + # Verify logging + _, err = capfd.readouterr() + assert f"Safespring location for project '{new_project.public_id}': sto4" in err + + +def test_create_project_sqlalchemyerror(client, boto3_session): + """Create project with correct credentials.""" + # Needs to have 3 unit admins + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Create project + time_before_run = datetime.datetime.utcnow() + time.sleep(1) + with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert not ( + created_proj + and created_proj.date_created > time_before_run + and not created_proj.non_sensitive + ) diff --git a/tests/test_project_creation.py b/tests/test_project_creation.py index cef08f656..e69de29bb 100644 --- a/tests/test_project_creation.py +++ b/tests/test_project_creation.py @@ -1,836 +0,0 @@ -# IMPORTS ################################################################################ IMPORTS # - -# Standard library -import http -import datetime -import unittest -import time -import os - -# Installed - -# Own -from dds_web import db -from dds_web.database import models -from dds_web.utils import current_time -import tests - - -# CONFIG ################################################################################## CONFIG # - -proj_data = { - "pi": "researchuser@mailtrap.io", - "title": "Test proj", - "description": "A longer project description", -} -proj_data_with_existing_users = { - **proj_data, - "users_to_add": [ - {"email": "researchuser@mailtrap.io", "role": "Project Owner"}, - {"email": "researchuser2@mailtrap.io", "role": "Researcher"}, - ], -} -proj_data_with_nonexisting_users = { - **proj_data, - "users_to_add": [ - {"email": "non_existing_user@mailtrap.io", "role": "Project Owner"}, - {"email": "non_existing_user2@mailtrap.io", "role": "Researcher"}, - ], -} -proj_data_with_unsuitable_user_roles = { - **proj_data, - "users_to_add": [ - {"email": "researchuser@mailtrap.io", "role": "Unit Admin"}, - {"email": "researchuser2@mailtrap.io", "role": "Unit Personnel"}, - ], -} - - -def create_unit_admins(num_admins, unit_id=1): - new_admins = [] - for i in range(1, num_admins + 1): - new_admins.append( - models.UnitUser( - **{ - "username": "unit_admin_" + os.urandom(4).hex(), - "name": "Unit Admin " + str(i), - "password": "password", - "is_admin": True, - "unit_id": unit_id, - } - ) - ) - - db.session.add_all(new_admins) - db.session.commit() - - -# TESTS #################################################################################### TESTS # - - -def test_create_project_too_few_unit_admins(client): - """There needs to be at least 2 Unit Admins.""" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.FORBIDDEN - response_json = response.json - assert response_json - assert "Your unit does not have enough Unit Admins" in response_json.get("message") - - -def test_create_project_two_unit_admins(client): - """There needs to be at least 2 Unit Admins.""" - create_unit_admins(num_admins=1) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 2 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - response_json = response.json - assert response_json - assert "Your unit only has 2 Unit Admins" in response_json.get("warning") - - -def test_create_project_two_unit_admins_force(client): - """The force option (not in cli) can be used to create a project even if there are - less than 3 Unit Admins.""" - create_unit_admins(num_admins=1) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 2 - - # Use force - updated_proj_data = proj_data.copy() - updated_proj_data["force"] = True - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=updated_proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - created_proj = models.Project.query.filter_by( - created_by="unitadmin", - title=updated_proj_data["title"], - pi=updated_proj_data["pi"], - description=updated_proj_data["description"], - ).one_or_none() - assert created_proj - - -def test_create_project_two_unit_admins_force(client): - """The force option (not in cli) can be used to create a project even if there are - less than 3 Unit Admins.""" - create_unit_admins(num_admins=1) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 2 - - # Use force - updated_proj_data = proj_data.copy() - updated_proj_data["force"] = "not correct" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=updated_proj_data, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - created_proj = models.Project.query.filter_by( - created_by="unitadmin", - title=updated_proj_data["title"], - pi=updated_proj_data["pi"], - description=updated_proj_data["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_empty(client): - """Make empty request.""" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert response_json - assert "Required data missing from request" in response_json.get("message") - - -def test_create_project_unknown_field(client): - """Make request with unknown field passed.""" - # Make sure there's 3 unit admins for unit - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Attempt creating project - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json={"test": "test"}, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - response_json - and "title" in response_json - and response_json["title"].get("message") == "Title is required." - ) - - -def test_create_project_missing_title(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_no_title = proj_data.copy() - proj_data_no_title.pop("title") - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_no_title, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "title" in response_json and response_json["title"].get("message") == "Title is required." - ) - - -def test_create_project_none_title(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_none_title = proj_data.copy() - proj_data_none_title["title"] = None - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_none_title, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "title" in response_json and response_json["title"].get("message") == "Title is required." - ) - - -def test_create_project_no_description(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_no_description = proj_data.copy() - proj_data_no_description.pop("description") - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_no_description, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "description" in response_json - and response_json["description"].get("message") == "A project description is required." - ) - - -def test_create_project_none_description(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_none_description = proj_data.copy() - proj_data_none_description["description"] = None - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_none_description, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "description" in response_json - and response_json["description"].get("message") == "A project description is required." - ) - - -def test_create_project_no_pi(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_no_pi = proj_data.copy() - proj_data_no_pi.pop("pi") - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_no_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "pi" in response_json - and response_json["pi"].get("message") == "A principal investigator is required." - ) - - -def test_create_project_none_pi(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_none_pi = proj_data.copy() - proj_data_none_pi["pi"] = None - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_none_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "pi" in response_json - and response_json["pi"].get("message") == "A principal investigator is required." - ) - - -def test_create_project_without_credentials(client): - """Create project without valid user credentials.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["researchuser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.FORBIDDEN - created_proj = models.Project.query.filter_by( - created_by="researchuser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert created_proj is None - - -def test_create_project_with_credentials(client, boto3_session): - """Create project with correct credentials.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - time_before_run = datetime.datetime.utcnow() - time.sleep(1) - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert ( - created_proj - and created_proj.date_created > time_before_run - and not created_proj.non_sensitive - ) - - -def test_create_project_no_title(client): - """Create project without a title specified.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json={"pi": "piName"}, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - pi=proj_data["pi"], - ).one_or_none() - assert created_proj is None - - -def test_create_project_title_too_short(client): - """Create a project with too short title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_short_title = proj_data.copy() - proj_data_short_title["title"] = "" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_short_title, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_short_title["title"], - pi=proj_data_short_title["pi"], - description=proj_data_short_title["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_with_malformed_json(client): - """Create a project with malformed project info.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json="", - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - created_proj = models.Project.query.filter_by( - created_by="unituser", - title="", - pi="", - description="", - ).one_or_none() - assert created_proj is None - - -def test_create_project_sensitive(client, boto3_session): - """Create a sensitive project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - p_data = proj_data - p_data["non_sensitive"] = False - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=p_data, - ) - assert response.status_code == http.HTTPStatus.OK - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert created_proj and not created_proj.non_sensitive - - -def test_create_project_description_too_short(client): - """Create a project with too short description.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_short_description = proj_data.copy() - proj_data_short_description["description"] = "" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_short_description, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_short_description["title"], - pi=proj_data_short_description["pi"], - description=proj_data_short_description["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_pi_too_short(client): - """Create a project with too short PI.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_short_pi = proj_data.copy() - proj_data_short_pi["pi"] = "" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_short_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_short_pi["title"], - pi=proj_data_short_pi["pi"], - description=proj_data_short_pi["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_pi_too_long(client): - """Create a project with too long PI.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_long_pi = proj_data.copy() - proj_data_long_pi["pi"] = "pi" * 128 - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_long_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_long_pi["title"], - pi=proj_data_long_pi["pi"], - description=proj_data_long_pi["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_wrong_status(client, boto3_session): - """Create a project with own status, should be overridden.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_wrong_status = proj_data.copy() - proj_data_wrong_status["status"] = "Incorrect Status" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_wrong_status, - ) - assert response.status_code == http.HTTPStatus.OK - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_wrong_status["title"], - pi=proj_data_wrong_status["pi"], - description=proj_data_wrong_status["description"], - ).one_or_none() - assert created_proj and created_proj.current_status == "In Progress" - - -def test_create_project_sensitive_not_boolean(client): - """Create project with incorrect non_sensitive format.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_sensitive_not_boolean = proj_data.copy() - proj_data_sensitive_not_boolean["non_sensitive"] = "test" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_sensitive_not_boolean, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_sensitive_not_boolean["title"], - pi=proj_data_sensitive_not_boolean["pi"], - description=proj_data_sensitive_not_boolean["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_date_created_overridden(client, boto3_session): - """Create project with own date_created, should be overridden.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_date_created_own = proj_data.copy() - proj_data_date_created_own["date_created"] = "test" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_date_created_own, - ) - assert response.status_code == http.HTTPStatus.OK - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_date_created_own["title"], - pi=proj_data_date_created_own["pi"], - description=proj_data_date_created_own["description"], - ).one_or_none() - assert created_proj and created_proj.date_created != proj_data_date_created_own["date_created"] - - -def test_create_project_with_users(client, boto3_session): - """Create project and add users to the project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_with_existing_users, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json and response.json.get("user_addition_statuses") - for x in response.json.get("user_addition_statuses"): - assert "given access to the Project" in x - - resp_json = response.json - created_proj = models.Project.query.filter_by(public_id=resp_json["project_id"]).one_or_none() - assert created_proj - users = models.ProjectUsers.query.filter_by(project_id=created_proj.id).all() - users_dict_from_db = [] - - for user in users: - users_dict_from_db.append({"username": user.user_id, "owner": user.owner}) - - users_dict_from_email = [] - for user in proj_data_with_existing_users["users_to_add"]: - email = models.Email.query.filter_by(email=user["email"]).one_or_none() - users_dict_from_email.append( - { - "username": email.user_id, - "owner": True if user.get("role") == "Project Owner" else False, - } - ) - - case = unittest.TestCase() - case.assertCountEqual(users_dict_from_db, users_dict_from_email) - - -def test_create_project_with_invited_users(client, boto3_session): - """Create project and invite users to the project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_with_nonexisting_users, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json and response.json.get("user_addition_statuses") - for x in response.json.get("user_addition_statuses"): - assert "Invitation sent" in x - - -def test_create_project_with_unsuitable_roles(client, boto3_session): - """Create project and add users with unsuitable roles to the project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_with_unsuitable_user_roles, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json and response.json.get("user_addition_statuses") - for x in response.json.get("user_addition_statuses"): - assert "User Role should be either 'Project Owner' or 'Researcher'" in x - - -def test_create_project_valid_characters(client, boto3_session): - """Create a project with no unicode.""" - # Project info with valid characters - proj_data_val_chars = proj_data.copy() - proj_data_val_chars["description"] = "A longer project description !#¤%&/()=?¡@£$€¥{[]}\\" - - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_val_chars, - ) - assert response.status_code == http.HTTPStatus.OK - - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data_val_chars["description"]) - .first() - ) - assert new_project - - -def test_create_project_invalid_characters(client, boto3_session): - """Create a project with unicode characters.""" - # Project info with invalid characters - proj_data_inval_chars = proj_data.copy() - proj_data_inval_chars["description"] = "A longer project description \U0001F300 \U0001F601" - - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_inval_chars, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - assert ( - response.json - and response.json.get("description") - and isinstance(response.json.get("description"), list) - ) - assert response.json["description"][0] == "This input is not allowed: \U0001F300\U0001F601" - - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data_inval_chars["description"]) - .first() - ) - assert not new_project - - -def test_create_project_sto2(client, boto3_session, capfd): - """Create a project in sto2.""" - # Make sure there are 3 unit admins - create_unit_admins(num_admins=2) - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Use sto2 -- all sto4 vars not set --------------------- - unit: models.Unit = models.Unit.query.filter_by(id=1).first() - assert unit - assert not all( - [ - unit.sto4_start_time, - unit.sto4_endpoint, - unit.sto4_name, - unit.sto4_access, - unit.sto4_secret, - ] - ) - - # Create project - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - - # Verify that new project is created - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data["description"]) - .first() - ) - assert new_project - - # Verify logging - _, err = capfd.readouterr() - assert f"Safespring location for project '{new_project.public_id}': sto2" in err - - -def test_create_project_sto4(client, boto3_session, capfd): - """Create a project in sto4.""" - # Make sure there are 3 unit admins - create_unit_admins(num_admins=2) - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Use sto4 - unit: models.Unit = models.Unit.query.filter_by(id=1).first() - assert unit - unit.sto4_start_time = current_time() - unit.sto4_endpoint = "endpoint" - unit.sto4_name = "name" - unit.sto4_access = "access" - unit.sto4_secret = "secret" - db.session.commit() - assert all( - [ - unit.sto4_start_time, - unit.sto4_endpoint, - unit.sto4_name, - unit.sto4_access, - unit.sto4_secret, - ] - ) - - # Create project - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - - # Verify that new project is created - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data["description"]) - .first() - ) - assert new_project - - # Verify logging - _, err = capfd.readouterr() - assert f"Safespring location for project '{new_project.public_id}': sto4" in err diff --git a/tests/test_user_remove_association.py b/tests/test_user_remove_association.py index 39c0ecbf5..61f6c4172 100644 --- a/tests/test_user_remove_association.py +++ b/tests/test_user_remove_association.py @@ -4,7 +4,7 @@ # Own import tests -from tests.test_project_creation import proj_data_with_existing_users, create_unit_admins +from tests.api.test_project import proj_data_with_existing_users, create_unit_admins from dds_web.database import models diff --git a/tests/test_utils.py b/tests/test_utils.py index bc6864db7..3d09a2dd5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -459,9 +459,9 @@ def test_verify_enough_unit_admins_less_than_3(client): assert num_admins == 1 # Create another unit admin - from tests import test_project_creation + from tests.api.test_project import create_unit_admins - test_project_creation.create_unit_admins(num_admins=1, unit_id=unit_id) + create_unit_admins(num_admins=1, unit_id=unit_id) # Get number of admins num_admins = db.session.query(models.UnitUser).filter_by(is_admin=True, unit_id=unit_id).count() @@ -486,9 +486,9 @@ def test_verify_enough_unit_admins_ok(client): assert num_admins == 1 # Create another unit admin - from tests import test_project_creation + from tests.api.test_project import create_unit_admins - test_project_creation.create_unit_admins(num_admins=2, unit_id=unit_id) + create_unit_admins(num_admins=2, unit_id=unit_id) # Get number of admins num_admins = db.session.query(models.UnitUser).filter_by(is_admin=True, unit_id=unit_id).count() From fe54ffdecdd31dc81538f0626b048a1ac46141a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 14:01:41 +0200 Subject: [PATCH 021/133] Revert "move tests and add new for sqlalchemyerror" This reverts commit 11b4716f29fffdada9ae2bea0618af255f76d4a7. --- tests/api/test_project.py | 891 +------------------------- tests/test_project_creation.py | 836 ++++++++++++++++++++++++ tests/test_user_remove_association.py | 2 +- tests/test_utils.py | 8 +- 4 files changed, 861 insertions(+), 876 deletions(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index f15d795dc..ae51b79b7 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -1,16 +1,13 @@ # IMPORTS ################################################################################ IMPORTS # # Standard library -import os import http from sqlite3 import OperationalError import pytest import datetime import time -import unittest import unittest.mock - # Installed import boto3 import flask_mail @@ -23,40 +20,13 @@ from dds_web.errors import BucketNotFoundError, DatabaseError, DeletionError import tests from tests.test_files_new import project_row, file_in_db, FIRST_NEW_FILE +from tests.test_project_creation import proj_data_with_existing_users, create_unit_admins from dds_web.database import models from dds_web.api.project import UserProjects -from dds_web.utils import current_time # CONFIG ################################################################################## CONFIG # -proj_data = { - "pi": "researchuser@mailtrap.io", - "title": "Test proj", - "description": "A longer project description", -} -proj_data_with_existing_users = { - **proj_data, - "users_to_add": [ - {"email": "researchuser@mailtrap.io", "role": "Project Owner"}, - {"email": "researchuser2@mailtrap.io", "role": "Researcher"}, - ], -} -proj_data_with_nonexisting_users = { - **proj_data, - "users_to_add": [ - {"email": "non_existing_user@mailtrap.io", "role": "Project Owner"}, - {"email": "non_existing_user2@mailtrap.io", "role": "Researcher"}, - ], -} -proj_data_with_unsuitable_user_roles = { - **proj_data, - "users_to_add": [ - {"email": "researchuser@mailtrap.io", "role": "Unit Admin"}, - {"email": "researchuser2@mailtrap.io", "role": "Unit Personnel"}, - ], -} - -proj_data_plus_users = { +proj_data = { "pi": "researchuser@mailtrap.io", "title": "Test proj", "description": "A longer project description", @@ -75,25 +45,6 @@ ] -def create_unit_admins(num_admins, unit_id=1): - new_admins = [] - for i in range(1, num_admins + 1): - new_admins.append( - models.UnitUser( - **{ - "username": "unit_admin_" + os.urandom(4).hex(), - "name": "Unit Admin " + str(i), - "password": "password", - "is_admin": True, - "unit_id": unit_id, - } - ) - ) - - db.session.add_all(new_admins) - db.session.commit() - - @pytest.fixture(scope="module") def test_project(module_client): """Create a shared test project""" @@ -101,7 +52,7 @@ def test_project(module_client): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) project_id = response.json.get("project_id") # add a file @@ -137,7 +88,7 @@ def test_projectstatus_get_status_without_args(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -174,7 +125,7 @@ def test_projectstatus_get_status_with_empty_args(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -202,7 +153,7 @@ def test_projectstatus_get_status_with_invalid_project(module_client, boto3_sess response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -339,7 +290,7 @@ def test_projectstatus_submit_request_with_invalid_args(module_client, boto3_ses response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -387,7 +338,7 @@ def test_projectstatus_post_operationalerror(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -418,7 +369,7 @@ def test_projectstatus_set_project_to_deleted_from_in_progress(module_client, bo response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -476,7 +427,7 @@ def test_projectstatus_archived_project(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -526,7 +477,7 @@ def test_projectstatus_aborted_project(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -582,7 +533,7 @@ def test_projectstatus_abort_from_in_progress_once_made_available(module_client, response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -673,7 +624,7 @@ def test_projectstatus_check_invalid_transitions_from_in_progress(module_client, response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK @@ -1074,7 +1025,7 @@ def test_projectstatus_post_invalid_deadline_release(module_client, boto3_sessio response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1102,7 +1053,7 @@ def test_projectstatus_post_invalid_deadline_expire(module_client, boto3_session response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1139,7 +1090,7 @@ def test_projectstatus_post_deletion_and_archivation_errors(module_client, boto3 response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1181,7 +1132,7 @@ def test_projectstatus_post_archiving_without_aborting(module_client, boto3_sess response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1232,7 +1183,7 @@ def test_projectstatus_post_deletion_and_archivation_errors(module_client, boto3 response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1287,7 +1238,7 @@ def test_getpublic_publickey_is_none(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1320,7 +1271,7 @@ def test_getpublic_publickey(module_client, boto3_session): response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(module_client), - json=proj_data_plus_users, + json=proj_data, ) assert response.status_code == http.HTTPStatus.OK project_id = response.json.get("project_id") @@ -1437,805 +1388,3 @@ def test_project_usage(module_client): # Call project_usage() for the project and check if cost is calculated correctly proj_bhours, proj_cost = UserProjects.project_usage(project=project_0) assert (proj_bhours / 1e9) * cost_gbhour == proj_cost - - -# CreateProject - - -def test_create_project_too_few_unit_admins(client): - """There needs to be at least 2 Unit Admins.""" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.FORBIDDEN - response_json = response.json - assert response_json - assert "Your unit does not have enough Unit Admins" in response_json.get("message") - - -def test_create_project_two_unit_admins(client): - """There needs to be at least 2 Unit Admins.""" - create_unit_admins(num_admins=1) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 2 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - response_json = response.json - assert response_json - assert "Your unit only has 2 Unit Admins" in response_json.get("warning") - - -def test_create_project_two_unit_admins_force(client): - """The force option (not in cli) can be used to create a project even if there are - less than 3 Unit Admins.""" - create_unit_admins(num_admins=1) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 2 - - # Use force - updated_proj_data = proj_data.copy() - updated_proj_data["force"] = True - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=updated_proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - created_proj = models.Project.query.filter_by( - created_by="unitadmin", - title=updated_proj_data["title"], - pi=updated_proj_data["pi"], - description=updated_proj_data["description"], - ).one_or_none() - assert created_proj - - -def test_create_project_two_unit_admins_force(client): - """The force option (not in cli) can be used to create a project even if there are - less than 3 Unit Admins.""" - create_unit_admins(num_admins=1) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 2 - - # Use force - updated_proj_data = proj_data.copy() - updated_proj_data["force"] = "not correct" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=updated_proj_data, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - created_proj = models.Project.query.filter_by( - created_by="unitadmin", - title=updated_proj_data["title"], - pi=updated_proj_data["pi"], - description=updated_proj_data["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_empty(client): - """Make empty request.""" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert response_json - assert "Required data missing from request" in response_json.get("message") - - -def test_create_project_unknown_field(client): - """Make request with unknown field passed.""" - # Make sure there's 3 unit admins for unit - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Attempt creating project - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json={"test": "test"}, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - response_json - and "title" in response_json - and response_json["title"].get("message") == "Title is required." - ) - - -def test_create_project_missing_title(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_no_title = proj_data.copy() - proj_data_no_title.pop("title") - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_no_title, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "title" in response_json and response_json["title"].get("message") == "Title is required." - ) - - -def test_create_project_none_title(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_none_title = proj_data.copy() - proj_data_none_title["title"] = None - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_none_title, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "title" in response_json and response_json["title"].get("message") == "Title is required." - ) - - -def test_create_project_no_description(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_no_description = proj_data.copy() - proj_data_no_description.pop("description") - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_no_description, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "description" in response_json - and response_json["description"].get("message") == "A project description is required." - ) - - -def test_create_project_none_description(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_none_description = proj_data.copy() - proj_data_none_description["description"] = None - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_none_description, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "description" in response_json - and response_json["description"].get("message") == "A project description is required." - ) - - -def test_create_project_no_pi(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_no_pi = proj_data.copy() - proj_data_no_pi.pop("pi") - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_no_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "pi" in response_json - and response_json["pi"].get("message") == "A principal investigator is required." - ) - - -def test_create_project_none_pi(client): - """Make request with missing title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_none_pi = proj_data.copy() - proj_data_none_pi["pi"] = None - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=proj_data_none_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - response_json = response.json - assert ( - "pi" in response_json - and response_json["pi"].get("message") == "A principal investigator is required." - ) - - -def test_create_project_without_credentials(client): - """Create project without valid user credentials.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["researchuser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.FORBIDDEN - created_proj = models.Project.query.filter_by( - created_by="researchuser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert created_proj is None - - -def test_create_project_with_credentials(client, boto3_session): - """Create project with correct credentials.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - time_before_run = datetime.datetime.utcnow() - time.sleep(1) - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert ( - created_proj - and created_proj.date_created > time_before_run - and not created_proj.non_sensitive - ) - - -def test_create_project_no_title(client): - """Create project without a title specified.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json={"pi": "piName"}, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - pi=proj_data["pi"], - ).one_or_none() - assert created_proj is None - - -def test_create_project_title_too_short(client): - """Create a project with too short title.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_short_title = proj_data.copy() - proj_data_short_title["title"] = "" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_short_title, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_short_title["title"], - pi=proj_data_short_title["pi"], - description=proj_data_short_title["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_with_malformed_json(client): - """Create a project with malformed project info.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json="", - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - created_proj = models.Project.query.filter_by( - created_by="unituser", - title="", - pi="", - description="", - ).one_or_none() - assert created_proj is None - - -def test_create_project_sensitive(client, boto3_session): - """Create a sensitive project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - p_data = proj_data - p_data["non_sensitive"] = False - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=p_data, - ) - assert response.status_code == http.HTTPStatus.OK - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert created_proj and not created_proj.non_sensitive - - -def test_create_project_description_too_short(client): - """Create a project with too short description.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_short_description = proj_data.copy() - proj_data_short_description["description"] = "" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_short_description, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_short_description["title"], - pi=proj_data_short_description["pi"], - description=proj_data_short_description["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_pi_too_short(client): - """Create a project with too short PI.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_short_pi = proj_data.copy() - proj_data_short_pi["pi"] = "" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_short_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_short_pi["title"], - pi=proj_data_short_pi["pi"], - description=proj_data_short_pi["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_pi_too_long(client): - """Create a project with too long PI.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_long_pi = proj_data.copy() - proj_data_long_pi["pi"] = "pi" * 128 - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_long_pi, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_long_pi["title"], - pi=proj_data_long_pi["pi"], - description=proj_data_long_pi["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_wrong_status(client, boto3_session): - """Create a project with own status, should be overridden.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_wrong_status = proj_data.copy() - proj_data_wrong_status["status"] = "Incorrect Status" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_wrong_status, - ) - assert response.status_code == http.HTTPStatus.OK - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_wrong_status["title"], - pi=proj_data_wrong_status["pi"], - description=proj_data_wrong_status["description"], - ).one_or_none() - assert created_proj and created_proj.current_status == "In Progress" - - -def test_create_project_sensitive_not_boolean(client): - """Create project with incorrect non_sensitive format.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_sensitive_not_boolean = proj_data.copy() - proj_data_sensitive_not_boolean["non_sensitive"] = "test" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_sensitive_not_boolean, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_sensitive_not_boolean["title"], - pi=proj_data_sensitive_not_boolean["pi"], - description=proj_data_sensitive_not_boolean["description"], - ).one_or_none() - assert not created_proj - - -def test_create_project_date_created_overridden(client, boto3_session): - """Create project with own date_created, should be overridden.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - proj_data_date_created_own = proj_data.copy() - proj_data_date_created_own["date_created"] = "test" - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_date_created_own, - ) - assert response.status_code == http.HTTPStatus.OK - - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data_date_created_own["title"], - pi=proj_data_date_created_own["pi"], - description=proj_data_date_created_own["description"], - ).one_or_none() - assert created_proj and created_proj.date_created != proj_data_date_created_own["date_created"] - - -def test_create_project_with_users(client, boto3_session): - """Create project and add users to the project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_with_existing_users, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json and response.json.get("user_addition_statuses") - for x in response.json.get("user_addition_statuses"): - assert "given access to the Project" in x - - resp_json = response.json - created_proj = models.Project.query.filter_by(public_id=resp_json["project_id"]).one_or_none() - assert created_proj - users = models.ProjectUsers.query.filter_by(project_id=created_proj.id).all() - users_dict_from_db = [] - - for user in users: - users_dict_from_db.append({"username": user.user_id, "owner": user.owner}) - - users_dict_from_email = [] - for user in proj_data_with_existing_users["users_to_add"]: - email = models.Email.query.filter_by(email=user["email"]).one_or_none() - users_dict_from_email.append( - { - "username": email.user_id, - "owner": True if user.get("role") == "Project Owner" else False, - } - ) - - case = unittest.TestCase() - case.assertCountEqual(users_dict_from_db, users_dict_from_email) - - -def test_create_project_with_invited_users(client, boto3_session): - """Create project and invite users to the project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_with_nonexisting_users, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json and response.json.get("user_addition_statuses") - for x in response.json.get("user_addition_statuses"): - assert "Invitation sent" in x - - -def test_create_project_with_unsuitable_roles(client, boto3_session): - """Create project and add users with unsuitable roles to the project.""" - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_with_unsuitable_user_roles, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json and response.json.get("user_addition_statuses") - for x in response.json.get("user_addition_statuses"): - assert "User Role should be either 'Project Owner' or 'Researcher'" in x - - -def test_create_project_valid_characters(client, boto3_session): - """Create a project with no unicode.""" - # Project info with valid characters - proj_data_val_chars = proj_data.copy() - proj_data_val_chars["description"] = "A longer project description !#¤%&/()=?¡@£$€¥{[]}\\" - - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_val_chars, - ) - assert response.status_code == http.HTTPStatus.OK - - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data_val_chars["description"]) - .first() - ) - assert new_project - - -def test_create_project_invalid_characters(client, boto3_session): - """Create a project with unicode characters.""" - # Project info with invalid characters - proj_data_inval_chars = proj_data.copy() - proj_data_inval_chars["description"] = "A longer project description \U0001F300 \U0001F601" - - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data_inval_chars, - ) - assert response.status_code == http.HTTPStatus.BAD_REQUEST - assert ( - response.json - and response.json.get("description") - and isinstance(response.json.get("description"), list) - ) - assert response.json["description"][0] == "This input is not allowed: \U0001F300\U0001F601" - - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data_inval_chars["description"]) - .first() - ) - assert not new_project - - -def test_create_project_sto2(client, boto3_session, capfd): - """Create a project in sto2.""" - # Make sure there are 3 unit admins - create_unit_admins(num_admins=2) - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Use sto2 -- all sto4 vars not set --------------------- - unit: models.Unit = models.Unit.query.filter_by(id=1).first() - assert unit - assert not all( - [ - unit.sto4_start_time, - unit.sto4_endpoint, - unit.sto4_name, - unit.sto4_access, - unit.sto4_secret, - ] - ) - - # Create project - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - - # Verify that new project is created - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data["description"]) - .first() - ) - assert new_project - - # Verify logging - _, err = capfd.readouterr() - assert f"Safespring location for project '{new_project.public_id}': sto2" in err - - -def test_create_project_sto4(client, boto3_session, capfd): - """Create a project in sto4.""" - # Make sure there are 3 unit admins - create_unit_admins(num_admins=2) - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Use sto4 - unit: models.Unit = models.Unit.query.filter_by(id=1).first() - assert unit - unit.sto4_start_time = current_time() - unit.sto4_endpoint = "endpoint" - unit.sto4_name = "name" - unit.sto4_access = "access" - unit.sto4_secret = "secret" - db.session.commit() - assert all( - [ - unit.sto4_start_time, - unit.sto4_endpoint, - unit.sto4_name, - unit.sto4_access, - unit.sto4_secret, - ] - ) - - # Create project - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK - - # Verify that new project is created - new_project = ( - db.session.query(models.Project) - .filter(models.Project.description == proj_data["description"]) - .first() - ) - assert new_project - - # Verify logging - _, err = capfd.readouterr() - assert f"Safespring location for project '{new_project.public_id}': sto4" in err - - -def test_create_project_sqlalchemyerror(client, boto3_session): - """Create project with correct credentials.""" - # Needs to have 3 unit admins - create_unit_admins(num_admins=2) - - current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() - assert current_unit_admins == 3 - - # Create project - time_before_run = datetime.datetime.utcnow() - time.sleep(1) - with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR - created_proj = models.Project.query.filter_by( - created_by="unituser", - title=proj_data["title"], - pi=proj_data["pi"], - description=proj_data["description"], - ).one_or_none() - assert not ( - created_proj - and created_proj.date_created > time_before_run - and not created_proj.non_sensitive - ) diff --git a/tests/test_project_creation.py b/tests/test_project_creation.py index e69de29bb..cef08f656 100644 --- a/tests/test_project_creation.py +++ b/tests/test_project_creation.py @@ -0,0 +1,836 @@ +# IMPORTS ################################################################################ IMPORTS # + +# Standard library +import http +import datetime +import unittest +import time +import os + +# Installed + +# Own +from dds_web import db +from dds_web.database import models +from dds_web.utils import current_time +import tests + + +# CONFIG ################################################################################## CONFIG # + +proj_data = { + "pi": "researchuser@mailtrap.io", + "title": "Test proj", + "description": "A longer project description", +} +proj_data_with_existing_users = { + **proj_data, + "users_to_add": [ + {"email": "researchuser@mailtrap.io", "role": "Project Owner"}, + {"email": "researchuser2@mailtrap.io", "role": "Researcher"}, + ], +} +proj_data_with_nonexisting_users = { + **proj_data, + "users_to_add": [ + {"email": "non_existing_user@mailtrap.io", "role": "Project Owner"}, + {"email": "non_existing_user2@mailtrap.io", "role": "Researcher"}, + ], +} +proj_data_with_unsuitable_user_roles = { + **proj_data, + "users_to_add": [ + {"email": "researchuser@mailtrap.io", "role": "Unit Admin"}, + {"email": "researchuser2@mailtrap.io", "role": "Unit Personnel"}, + ], +} + + +def create_unit_admins(num_admins, unit_id=1): + new_admins = [] + for i in range(1, num_admins + 1): + new_admins.append( + models.UnitUser( + **{ + "username": "unit_admin_" + os.urandom(4).hex(), + "name": "Unit Admin " + str(i), + "password": "password", + "is_admin": True, + "unit_id": unit_id, + } + ) + ) + + db.session.add_all(new_admins) + db.session.commit() + + +# TESTS #################################################################################### TESTS # + + +def test_create_project_too_few_unit_admins(client): + """There needs to be at least 2 Unit Admins.""" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.FORBIDDEN + response_json = response.json + assert response_json + assert "Your unit does not have enough Unit Admins" in response_json.get("message") + + +def test_create_project_two_unit_admins(client): + """There needs to be at least 2 Unit Admins.""" + create_unit_admins(num_admins=1) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 2 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + response_json = response.json + assert response_json + assert "Your unit only has 2 Unit Admins" in response_json.get("warning") + + +def test_create_project_two_unit_admins_force(client): + """The force option (not in cli) can be used to create a project even if there are + less than 3 Unit Admins.""" + create_unit_admins(num_admins=1) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 2 + + # Use force + updated_proj_data = proj_data.copy() + updated_proj_data["force"] = True + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=updated_proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + created_proj = models.Project.query.filter_by( + created_by="unitadmin", + title=updated_proj_data["title"], + pi=updated_proj_data["pi"], + description=updated_proj_data["description"], + ).one_or_none() + assert created_proj + + +def test_create_project_two_unit_admins_force(client): + """The force option (not in cli) can be used to create a project even if there are + less than 3 Unit Admins.""" + create_unit_admins(num_admins=1) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 2 + + # Use force + updated_proj_data = proj_data.copy() + updated_proj_data["force"] = "not correct" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=updated_proj_data, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + created_proj = models.Project.query.filter_by( + created_by="unitadmin", + title=updated_proj_data["title"], + pi=updated_proj_data["pi"], + description=updated_proj_data["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_empty(client): + """Make empty request.""" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert response_json + assert "Required data missing from request" in response_json.get("message") + + +def test_create_project_unknown_field(client): + """Make request with unknown field passed.""" + # Make sure there's 3 unit admins for unit + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Attempt creating project + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json={"test": "test"}, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + response_json + and "title" in response_json + and response_json["title"].get("message") == "Title is required." + ) + + +def test_create_project_missing_title(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_no_title = proj_data.copy() + proj_data_no_title.pop("title") + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_no_title, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "title" in response_json and response_json["title"].get("message") == "Title is required." + ) + + +def test_create_project_none_title(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_none_title = proj_data.copy() + proj_data_none_title["title"] = None + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_none_title, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "title" in response_json and response_json["title"].get("message") == "Title is required." + ) + + +def test_create_project_no_description(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_no_description = proj_data.copy() + proj_data_no_description.pop("description") + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_no_description, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "description" in response_json + and response_json["description"].get("message") == "A project description is required." + ) + + +def test_create_project_none_description(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_none_description = proj_data.copy() + proj_data_none_description["description"] = None + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_none_description, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "description" in response_json + and response_json["description"].get("message") == "A project description is required." + ) + + +def test_create_project_no_pi(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_no_pi = proj_data.copy() + proj_data_no_pi.pop("pi") + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_no_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "pi" in response_json + and response_json["pi"].get("message") == "A principal investigator is required." + ) + + +def test_create_project_none_pi(client): + """Make request with missing title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_none_pi = proj_data.copy() + proj_data_none_pi["pi"] = None + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=proj_data_none_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + response_json = response.json + assert ( + "pi" in response_json + and response_json["pi"].get("message") == "A principal investigator is required." + ) + + +def test_create_project_without_credentials(client): + """Create project without valid user credentials.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["researchuser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.FORBIDDEN + created_proj = models.Project.query.filter_by( + created_by="researchuser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert created_proj is None + + +def test_create_project_with_credentials(client, boto3_session): + """Create project with correct credentials.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + time_before_run = datetime.datetime.utcnow() + time.sleep(1) + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert ( + created_proj + and created_proj.date_created > time_before_run + and not created_proj.non_sensitive + ) + + +def test_create_project_no_title(client): + """Create project without a title specified.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json={"pi": "piName"}, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + pi=proj_data["pi"], + ).one_or_none() + assert created_proj is None + + +def test_create_project_title_too_short(client): + """Create a project with too short title.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_short_title = proj_data.copy() + proj_data_short_title["title"] = "" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_short_title, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_short_title["title"], + pi=proj_data_short_title["pi"], + description=proj_data_short_title["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_with_malformed_json(client): + """Create a project with malformed project info.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json="", + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + created_proj = models.Project.query.filter_by( + created_by="unituser", + title="", + pi="", + description="", + ).one_or_none() + assert created_proj is None + + +def test_create_project_sensitive(client, boto3_session): + """Create a sensitive project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + p_data = proj_data + p_data["non_sensitive"] = False + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=p_data, + ) + assert response.status_code == http.HTTPStatus.OK + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert created_proj and not created_proj.non_sensitive + + +def test_create_project_description_too_short(client): + """Create a project with too short description.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_short_description = proj_data.copy() + proj_data_short_description["description"] = "" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_short_description, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_short_description["title"], + pi=proj_data_short_description["pi"], + description=proj_data_short_description["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_pi_too_short(client): + """Create a project with too short PI.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_short_pi = proj_data.copy() + proj_data_short_pi["pi"] = "" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_short_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_short_pi["title"], + pi=proj_data_short_pi["pi"], + description=proj_data_short_pi["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_pi_too_long(client): + """Create a project with too long PI.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_long_pi = proj_data.copy() + proj_data_long_pi["pi"] = "pi" * 128 + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_long_pi, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_long_pi["title"], + pi=proj_data_long_pi["pi"], + description=proj_data_long_pi["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_wrong_status(client, boto3_session): + """Create a project with own status, should be overridden.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_wrong_status = proj_data.copy() + proj_data_wrong_status["status"] = "Incorrect Status" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_wrong_status, + ) + assert response.status_code == http.HTTPStatus.OK + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_wrong_status["title"], + pi=proj_data_wrong_status["pi"], + description=proj_data_wrong_status["description"], + ).one_or_none() + assert created_proj and created_proj.current_status == "In Progress" + + +def test_create_project_sensitive_not_boolean(client): + """Create project with incorrect non_sensitive format.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_sensitive_not_boolean = proj_data.copy() + proj_data_sensitive_not_boolean["non_sensitive"] = "test" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_sensitive_not_boolean, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_sensitive_not_boolean["title"], + pi=proj_data_sensitive_not_boolean["pi"], + description=proj_data_sensitive_not_boolean["description"], + ).one_or_none() + assert not created_proj + + +def test_create_project_date_created_overridden(client, boto3_session): + """Create project with own date_created, should be overridden.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + proj_data_date_created_own = proj_data.copy() + proj_data_date_created_own["date_created"] = "test" + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_date_created_own, + ) + assert response.status_code == http.HTTPStatus.OK + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data_date_created_own["title"], + pi=proj_data_date_created_own["pi"], + description=proj_data_date_created_own["description"], + ).one_or_none() + assert created_proj and created_proj.date_created != proj_data_date_created_own["date_created"] + + +def test_create_project_with_users(client, boto3_session): + """Create project and add users to the project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_with_existing_users, + ) + assert response.status_code == http.HTTPStatus.OK + assert response.json and response.json.get("user_addition_statuses") + for x in response.json.get("user_addition_statuses"): + assert "given access to the Project" in x + + resp_json = response.json + created_proj = models.Project.query.filter_by(public_id=resp_json["project_id"]).one_or_none() + assert created_proj + users = models.ProjectUsers.query.filter_by(project_id=created_proj.id).all() + users_dict_from_db = [] + + for user in users: + users_dict_from_db.append({"username": user.user_id, "owner": user.owner}) + + users_dict_from_email = [] + for user in proj_data_with_existing_users["users_to_add"]: + email = models.Email.query.filter_by(email=user["email"]).one_or_none() + users_dict_from_email.append( + { + "username": email.user_id, + "owner": True if user.get("role") == "Project Owner" else False, + } + ) + + case = unittest.TestCase() + case.assertCountEqual(users_dict_from_db, users_dict_from_email) + + +def test_create_project_with_invited_users(client, boto3_session): + """Create project and invite users to the project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_with_nonexisting_users, + ) + assert response.status_code == http.HTTPStatus.OK + assert response.json and response.json.get("user_addition_statuses") + for x in response.json.get("user_addition_statuses"): + assert "Invitation sent" in x + + +def test_create_project_with_unsuitable_roles(client, boto3_session): + """Create project and add users with unsuitable roles to the project.""" + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_with_unsuitable_user_roles, + ) + assert response.status_code == http.HTTPStatus.OK + assert response.json and response.json.get("user_addition_statuses") + for x in response.json.get("user_addition_statuses"): + assert "User Role should be either 'Project Owner' or 'Researcher'" in x + + +def test_create_project_valid_characters(client, boto3_session): + """Create a project with no unicode.""" + # Project info with valid characters + proj_data_val_chars = proj_data.copy() + proj_data_val_chars["description"] = "A longer project description !#¤%&/()=?¡@£$€¥{[]}\\" + + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_val_chars, + ) + assert response.status_code == http.HTTPStatus.OK + + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data_val_chars["description"]) + .first() + ) + assert new_project + + +def test_create_project_invalid_characters(client, boto3_session): + """Create a project with unicode characters.""" + # Project info with invalid characters + proj_data_inval_chars = proj_data.copy() + proj_data_inval_chars["description"] = "A longer project description \U0001F300 \U0001F601" + + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data_inval_chars, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + assert ( + response.json + and response.json.get("description") + and isinstance(response.json.get("description"), list) + ) + assert response.json["description"][0] == "This input is not allowed: \U0001F300\U0001F601" + + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data_inval_chars["description"]) + .first() + ) + assert not new_project + + +def test_create_project_sto2(client, boto3_session, capfd): + """Create a project in sto2.""" + # Make sure there are 3 unit admins + create_unit_admins(num_admins=2) + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Use sto2 -- all sto4 vars not set --------------------- + unit: models.Unit = models.Unit.query.filter_by(id=1).first() + assert unit + assert not all( + [ + unit.sto4_start_time, + unit.sto4_endpoint, + unit.sto4_name, + unit.sto4_access, + unit.sto4_secret, + ] + ) + + # Create project + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + + # Verify that new project is created + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data["description"]) + .first() + ) + assert new_project + + # Verify logging + _, err = capfd.readouterr() + assert f"Safespring location for project '{new_project.public_id}': sto2" in err + + +def test_create_project_sto4(client, boto3_session, capfd): + """Create a project in sto4.""" + # Make sure there are 3 unit admins + create_unit_admins(num_admins=2) + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + # Use sto4 + unit: models.Unit = models.Unit.query.filter_by(id=1).first() + assert unit + unit.sto4_start_time = current_time() + unit.sto4_endpoint = "endpoint" + unit.sto4_name = "name" + unit.sto4_access = "access" + unit.sto4_secret = "secret" + db.session.commit() + assert all( + [ + unit.sto4_start_time, + unit.sto4_endpoint, + unit.sto4_name, + unit.sto4_access, + unit.sto4_secret, + ] + ) + + # Create project + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + + # Verify that new project is created + new_project = ( + db.session.query(models.Project) + .filter(models.Project.description == proj_data["description"]) + .first() + ) + assert new_project + + # Verify logging + _, err = capfd.readouterr() + assert f"Safespring location for project '{new_project.public_id}': sto4" in err diff --git a/tests/test_user_remove_association.py b/tests/test_user_remove_association.py index 61f6c4172..39c0ecbf5 100644 --- a/tests/test_user_remove_association.py +++ b/tests/test_user_remove_association.py @@ -4,7 +4,7 @@ # Own import tests -from tests.api.test_project import proj_data_with_existing_users, create_unit_admins +from tests.test_project_creation import proj_data_with_existing_users, create_unit_admins from dds_web.database import models diff --git a/tests/test_utils.py b/tests/test_utils.py index 3d09a2dd5..bc6864db7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -459,9 +459,9 @@ def test_verify_enough_unit_admins_less_than_3(client): assert num_admins == 1 # Create another unit admin - from tests.api.test_project import create_unit_admins + from tests import test_project_creation - create_unit_admins(num_admins=1, unit_id=unit_id) + test_project_creation.create_unit_admins(num_admins=1, unit_id=unit_id) # Get number of admins num_admins = db.session.query(models.UnitUser).filter_by(is_admin=True, unit_id=unit_id).count() @@ -486,9 +486,9 @@ def test_verify_enough_unit_admins_ok(client): assert num_admins == 1 # Create another unit admin - from tests.api.test_project import create_unit_admins + from tests import test_project_creation - create_unit_admins(num_admins=2, unit_id=unit_id) + test_project_creation.create_unit_admins(num_admins=2, unit_id=unit_id) # Get number of admins num_admins = db.session.query(models.UnitUser).filter_by(is_admin=True, unit_id=unit_id).count() From df18e8ed14f59117bd63f2ef57764267835b38bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 14:13:07 +0200 Subject: [PATCH 022/133] new test for sqlalchemy --- tests/api/test_project.py | 2 +- tests/test_project_creation.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index ae51b79b7..a6f3da832 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -66,7 +66,7 @@ def test_project(module_client): return project_id -def mock_sqlalchemyerror(): +def mock_sqlalchemyerror(_: None): raise sqlalchemy.exc.SQLAlchemyError() diff --git a/tests/test_project_creation.py b/tests/test_project_creation.py index cef08f656..7bad52ee1 100644 --- a/tests/test_project_creation.py +++ b/tests/test_project_creation.py @@ -834,3 +834,37 @@ def test_create_project_sto4(client, boto3_session, capfd): # Verify logging _, err = capfd.readouterr() assert f"Safespring location for project '{new_project.public_id}': sto4" in err + + +def test_create_project_with_credentials(client, boto3_session): + """Create project with correct credentials.""" + # Create unit admins + create_unit_admins(num_admins=2) + + current_unit_admins = models.UnitUser.query.filter_by(unit_id=1, is_admin=True).count() + assert current_unit_admins == 3 + + time_before_run = datetime.datetime.utcnow() + time.sleep(1) + + # Create project + from tests.api.test_project import mock_sqlalchemyerror + with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): + response = client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR + + created_proj = models.Project.query.filter_by( + created_by="unituser", + title=proj_data["title"], + pi=proj_data["pi"], + description=proj_data["description"], + ).one_or_none() + assert not ( + created_proj + and created_proj.date_created > time_before_run + and not created_proj.non_sensitive + ) From 6ef4ea990ff17e2931be8d127ad5c628c59f33b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 14:14:52 +0200 Subject: [PATCH 023/133] argument not required --- tests/api/test_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index a6f3da832..593efbb08 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -66,7 +66,7 @@ def test_project(module_client): return project_id -def mock_sqlalchemyerror(_: None): +def mock_sqlalchemyerror(_ = None): raise sqlalchemy.exc.SQLAlchemyError() From 9a2aab208400b5201f82537867e7aaafd663cc82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 6 Sep 2023 14:15:57 +0200 Subject: [PATCH 024/133] black --- tests/api/test_project.py | 2 +- tests/test_project_creation.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/api/test_project.py b/tests/api/test_project.py index 593efbb08..cd1d7f740 100644 --- a/tests/api/test_project.py +++ b/tests/api/test_project.py @@ -66,7 +66,7 @@ def test_project(module_client): return project_id -def mock_sqlalchemyerror(_ = None): +def mock_sqlalchemyerror(_=None): raise sqlalchemy.exc.SQLAlchemyError() diff --git a/tests/test_project_creation.py b/tests/test_project_creation.py index 7bad52ee1..e44e99902 100644 --- a/tests/test_project_creation.py +++ b/tests/test_project_creation.py @@ -849,6 +849,7 @@ def test_create_project_with_credentials(client, boto3_session): # Create project from tests.api.test_project import mock_sqlalchemyerror + with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): response = client.post( tests.DDSEndpoint.PROJECT_CREATE, From 39d84dfa51d0c1e33985778863512f18f2158b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 7 Sep 2023 11:38:27 +0200 Subject: [PATCH 025/133] sto2 vars must be nullable --- dds_web/database/models.py | 8 +-- .../f27c5988d640_set_sto2_to_nullable.py | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/f27c5988d640_set_sto2_to_nullable.py diff --git a/dds_web/database/models.py b/dds_web/database/models.py index c568db36c..104fa932d 100644 --- a/dds_web/database/models.py +++ b/dds_web/database/models.py @@ -190,10 +190,10 @@ class Unit(db.Model): internal_ref = db.Column(db.String(50), unique=True, nullable=False) # Safespring storage - sto2_endpoint = db.Column(db.String(255), unique=False, nullable=False) # unique=True later - sto2_name = db.Column(db.String(255), unique=False, nullable=False) # unique=True later - sto2_access = db.Column(db.String(255), unique=False, nullable=False) # unique=True later - sto2_secret = db.Column(db.String(255), unique=False, nullable=False) # unique=True later + sto2_endpoint = db.Column(db.String(255), unique=False, nullable=True) # unique=True later + sto2_name = db.Column(db.String(255), unique=False, nullable=True) # unique=True later + sto2_access = db.Column(db.String(255), unique=False, nullable=True) # unique=True later + sto2_secret = db.Column(db.String(255), unique=False, nullable=True) # unique=True later # New safespring storage sto4_start_time = db.Column(db.DateTime(), nullable=True) diff --git a/migrations/versions/f27c5988d640_set_sto2_to_nullable.py b/migrations/versions/f27c5988d640_set_sto2_to_nullable.py new file mode 100644 index 000000000..7c7b37b60 --- /dev/null +++ b/migrations/versions/f27c5988d640_set_sto2_to_nullable.py @@ -0,0 +1,50 @@ +"""set-sto2-to-nullable + +Revision ID: f27c5988d640 +Revises: 1e56b6212479 +Create Date: 2023-09-07 09:36:25.289025 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'f27c5988d640' +down_revision = '1e56b6212479' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('units', 'sto2_endpoint', + existing_type=mysql.VARCHAR(length=255), + nullable=True) + op.alter_column('units', 'sto2_name', + existing_type=mysql.VARCHAR(length=255), + nullable=True) + op.alter_column('units', 'sto2_access', + existing_type=mysql.VARCHAR(length=255), + nullable=True) + op.alter_column('units', 'sto2_secret', + existing_type=mysql.VARCHAR(length=255), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('units', 'sto2_secret', + existing_type=mysql.VARCHAR(length=255), + nullable=False) + op.alter_column('units', 'sto2_access', + existing_type=mysql.VARCHAR(length=255), + nullable=False) + op.alter_column('units', 'sto2_name', + existing_type=mysql.VARCHAR(length=255), + nullable=False) + op.alter_column('units', 'sto2_endpoint', + existing_type=mysql.VARCHAR(length=255), + nullable=False) + # ### end Alembic commands ### From 2028431ebdb2cbb6f9199538f9a524e0465251fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 7 Sep 2023 13:37:09 +0200 Subject: [PATCH 026/133] fix tests that shouldn't have passed to begin with --- .../f27c5988d640_set_sto2_to_nullable.py | 40 ++--- tests/test_commands.py | 149 ++++++++++++------ 2 files changed, 116 insertions(+), 73 deletions(-) diff --git a/migrations/versions/f27c5988d640_set_sto2_to_nullable.py b/migrations/versions/f27c5988d640_set_sto2_to_nullable.py index 7c7b37b60..32885f5a4 100644 --- a/migrations/versions/f27c5988d640_set_sto2_to_nullable.py +++ b/migrations/versions/f27c5988d640_set_sto2_to_nullable.py @@ -10,41 +10,29 @@ from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. -revision = 'f27c5988d640' -down_revision = '1e56b6212479' +revision = "f27c5988d640" +down_revision = "1e56b6212479" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('units', 'sto2_endpoint', - existing_type=mysql.VARCHAR(length=255), - nullable=True) - op.alter_column('units', 'sto2_name', - existing_type=mysql.VARCHAR(length=255), - nullable=True) - op.alter_column('units', 'sto2_access', - existing_type=mysql.VARCHAR(length=255), - nullable=True) - op.alter_column('units', 'sto2_secret', - existing_type=mysql.VARCHAR(length=255), - nullable=True) + op.alter_column( + "units", "sto2_endpoint", existing_type=mysql.VARCHAR(length=255), nullable=True + ) + op.alter_column("units", "sto2_name", existing_type=mysql.VARCHAR(length=255), nullable=True) + op.alter_column("units", "sto2_access", existing_type=mysql.VARCHAR(length=255), nullable=True) + op.alter_column("units", "sto2_secret", existing_type=mysql.VARCHAR(length=255), nullable=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.alter_column('units', 'sto2_secret', - existing_type=mysql.VARCHAR(length=255), - nullable=False) - op.alter_column('units', 'sto2_access', - existing_type=mysql.VARCHAR(length=255), - nullable=False) - op.alter_column('units', 'sto2_name', - existing_type=mysql.VARCHAR(length=255), - nullable=False) - op.alter_column('units', 'sto2_endpoint', - existing_type=mysql.VARCHAR(length=255), - nullable=False) + op.alter_column("units", "sto2_secret", existing_type=mysql.VARCHAR(length=255), nullable=False) + op.alter_column("units", "sto2_access", existing_type=mysql.VARCHAR(length=255), nullable=False) + op.alter_column("units", "sto2_name", existing_type=mysql.VARCHAR(length=255), nullable=False) + op.alter_column( + "units", "sto2_endpoint", existing_type=mysql.VARCHAR(length=255), nullable=False + ) # ### end Alembic commands ### diff --git a/tests/test_commands.py b/tests/test_commands.py index 1ad7ea497..734d8b2f8 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -58,16 +58,16 @@ def mock_unit_size(): # fill_db_wrapper -def test_fill_db_wrapper_production(client, runner, capfd) -> None: +def test_fill_db_wrapper_production(client, runner, capfd: LogCaptureFixture) -> None: """Run init-db with the production argument.""" - result: click.testing.Result = runner.invoke(fill_db_wrapper, ["production"]) + _: click.testing.Result = runner.invoke(fill_db_wrapper, ["production"]) _, err = capfd.readouterr() assert "already exists, not creating user" in err -def test_fill_db_wrapper_devsmall(client, runner, capfd) -> None: +def test_fill_db_wrapper_devsmall(client, runner, capfd: LogCaptureFixture) -> None: """Run init-db with the dev-small argument.""" - result: click.testing.Result = runner.invoke(fill_db_wrapper, ["dev-small"]) + _: click.testing.Result = runner.invoke(fill_db_wrapper, ["dev-small"]) _, err = capfd.readouterr() assert "Initializing development db" in err assert "DB filled" not in err # DB already filled, duplicates. @@ -98,16 +98,17 @@ def create_command_options_from_dict(options: typing.Dict) -> typing.List: "external_display_name": "newexternaldisplay", "contact_email": "newcontact@mail.com", "internal_ref": "newinternalref", - "sto2_endpoint": "newsafespringendpoint", - "sto2_name": "newsafespringname", - "sto2_access": "newsafespringaccess", - "sto2_secret": "newsafespringsecret", + "safespring_endpoint": "newsafespringendpoint", + "safespring_name": "newsafespringname", + "safespring_access": "newsafespringaccess", + "safespring_secret": "newsafespringsecret", "days_in_available": 45, "days_in_expired": 15, + "quota": 80, } -def test_create_new_unit_public_id_too_long(client, runner) -> None: +def test_create_new_unit_public_id_too_long(client, runner, capfd: LogCaptureFixture) -> None: """Create new unit, public_id too long.""" # Change public_id incorrect_unit: typing.Dict = correct_unit.copy() @@ -117,14 +118,21 @@ def test_create_new_unit_public_id_too_long(client, runner) -> None: command_options = create_command_options_from_dict(options=incorrect_unit) # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert "The 'public_id' can be a maximum of 50 characters" in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + # Get log output + _, err = capfd.readouterr() + assert "The 'public_id' can be a maximum of 50 characters" in err + + # Verify that unit doesn't exist assert ( not db.session.query(models.Unit).filter(models.Unit.name == incorrect_unit["name"]).all() ) -def test_create_new_unit_public_id_incorrect_characters(client, runner) -> None: +def test_create_new_unit_public_id_incorrect_characters( + client, runner, capfd: LogCaptureFixture +) -> None: """Create new unit, public_id has invalid characters (here _).""" # Change public_id incorrect_unit: typing.Dict = correct_unit.copy() @@ -134,14 +142,21 @@ def test_create_new_unit_public_id_incorrect_characters(client, runner) -> None: command_options = create_command_options_from_dict(options=incorrect_unit) # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert "The 'public_id' can only contain letters, numbers, dots (.) and hyphens (-)." in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + # Get log output_ + _, err = capfd.readouterr() + assert "The 'public_id' can only contain letters, numbers, dots (.) and hyphens (-)." in err + + # Verify that unit doesn't exist assert ( not db.session.query(models.Unit).filter(models.Unit.name == incorrect_unit["name"]).all() ) -def test_create_new_unit_public_id_starts_with_dot(client, runner) -> None: +def test_create_new_unit_public_id_starts_with_dot( + client, runner, capfd: LogCaptureFixture +) -> None: """Create new unit, public_id starts with invalid character (. or -).""" # Change public_id incorrect_unit: typing.Dict = correct_unit.copy() @@ -151,8 +166,13 @@ def test_create_new_unit_public_id_starts_with_dot(client, runner) -> None: command_options = create_command_options_from_dict(options=incorrect_unit) # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert "The 'public_id' must begin with a letter or number." in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + # Get log output + _, err = capfd.readouterr() + assert "The 'public_id' must begin with a letter or number." in err + + # Verify that the unit doesn't exist assert ( not db.session.query(models.Unit).filter(models.Unit.name == incorrect_unit["name"]).all() ) @@ -164,14 +184,19 @@ def test_create_new_unit_public_id_starts_with_dot(client, runner) -> None: command_options = create_command_options_from_dict(options=incorrect_unit) # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert "The 'public_id' must begin with a letter or number." in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + # Get log output + _, err = capfd.readouterr() + assert "The 'public_id' must begin with a letter or number." in err + + # Verify that the unit doesn't exist assert ( not db.session.query(models.Unit).filter(models.Unit.name == incorrect_unit["name"]).all() ) -def test_create_new_unit_public_id_too_many_dots(client, runner) -> None: +def test_create_new_unit_public_id_too_many_dots(client, runner, capfd: LogCaptureFixture) -> None: """Create new unit, public_id has invalid number of dots.""" # Change public_id incorrect_unit: typing.Dict = correct_unit.copy() @@ -181,14 +206,19 @@ def test_create_new_unit_public_id_too_many_dots(client, runner) -> None: command_options = create_command_options_from_dict(options=incorrect_unit) # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert "The 'public_id' should not contain more than two dots." in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + # Get log output + _, err = capfd.readouterr() + assert "The 'public_id' should not contain more than two dots." in err + + # Verify that the unit doesn't exist assert ( not db.session.query(models.Unit).filter(models.Unit.name == incorrect_unit["name"]).all() ) -def test_create_new_unit_public_id_invalid_start(client, runner) -> None: +def test_create_new_unit_public_id_invalid_start(client, runner, capfd: LogCaptureFixture) -> None: """Create new unit, public_id starts with prefix.""" # Change public_id incorrect_unit: typing.Dict = correct_unit.copy() @@ -198,28 +228,35 @@ def test_create_new_unit_public_id_invalid_start(client, runner) -> None: command_options = create_command_options_from_dict(options=incorrect_unit) # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert "The 'public_id' cannot begin with the 'xn--' prefix." in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + # Get log output + _, err = capfd.readouterr() + assert "The 'public_id' cannot begin with the 'xn--' prefix." in err + + # Verify that the unit doesn't exist assert ( not db.session.query(models.Unit).filter(models.Unit.name == incorrect_unit["name"]).all() ) -def test_create_new_unit_success(client, runner) -> None: +def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> None: """Create new unit, public_id starts with prefix.""" # Get command options command_options = create_command_options_from_dict(options=correct_unit) with patch("dds_web.db.session.commit", mock_commit): # Run command - result: click.testing.Result = runner.invoke(create_new_unit, command_options) - # assert f"Unit '{correct_unit['name']}' created" in result.output + _: click.testing.Result = runner.invoke(create_new_unit, command_options) + + _, err = capfd.readouterr() + assert f"Unit '{correct_unit['name']}' created" in err # update_unit -def test_update_unit_no_such_unit(client, runner, capfd) -> None: +def test_update_unit_no_such_unit(client, runner, capfd: LogCaptureFixture) -> None: """Try to update a non existent unit -> Error.""" # Create command options command_options: typing.List = [ @@ -247,7 +284,9 @@ def test_update_unit_no_such_unit(client, runner, capfd) -> None: assert f"There is no unit with the public ID '{command_options[1]}'." in err -def test_update_unit_sto4_start_time_exists_mock_prompt_False(client, runner, capfd) -> None: +def test_update_unit_sto4_start_time_exists_mock_prompt_False( + client, runner, capfd: LogCaptureFixture +) -> None: """Start time already recorded. Answer no to prompt about update anyway. No changes should be made.""" # Get existing unit unit: models.Unit = models.Unit.query.first() @@ -310,7 +349,9 @@ def test_update_unit_sto4_start_time_exists_mock_prompt_False(client, runner, ca ] == sto4_info_original -def test_update_unit_sto4_start_time_exists_mock_prompt_True(client, runner, capfd) -> None: +def test_update_unit_sto4_start_time_exists_mock_prompt_True( + client, runner, capfd: LogCaptureFixture +) -> None: """Start time already recorded. Answer yes to prompt about update anyway. Changes should be made.""" # Get existing unit unit: models.Unit = models.Unit.query.first() @@ -382,7 +423,9 @@ def test_update_unit_sto4_start_time_exists_mock_prompt_True(client, runner, cap # update_uploaded_file_with_log -def test_update_uploaded_file_with_log_nonexisting_project(client, runner, capfd) -> None: +def test_update_uploaded_file_with_log_nonexisting_project( + client, runner, capfd: LogCaptureFixture +) -> None: """Add file info to non existing project.""" # Create command options command_options: typing.List = [ @@ -424,7 +467,7 @@ def test_update_uploaded_file_with_log_nonexisting_file(client, runner, fs: Fake # lost_files_s3_db -def test_lost_files_s3_db_no_command(client, cli_runner, capfd): +def test_lost_files_s3_db_no_command(client, cli_runner, capfd: LogCaptureFixture): """Test running the flask lost-files command without any subcommand.""" _: click.testing.Result = cli_runner.invoke(lost_files_s3_db) _, err = capfd.readouterr() @@ -434,7 +477,7 @@ def test_lost_files_s3_db_no_command(client, cli_runner, capfd): # lost_files_s3_db -- list_lost_files -def test_list_lost_files_no_such_project(client, cli_runner, capfd): +def test_list_lost_files_no_such_project(client, cli_runner, capfd: LogCaptureFixture): """flask lost-files ls: project specified, project doesnt exist.""" # Project ID -- doesn't exist project_id: str = "nonexistentproject" @@ -452,7 +495,9 @@ def test_list_lost_files_no_such_project(client, cli_runner, capfd): assert f"No such project: '{project_id}'" in err -def test_list_lost_files_no_lost_files_in_project(client, cli_runner, boto3_session, capfd): +def test_list_lost_files_no_lost_files_in_project( + client, cli_runner, boto3_session, capfd: LogCaptureFixture +): """flask lost-files ls: project specified, no lost files.""" # Get project project = models.Project.query.first() @@ -576,7 +621,9 @@ def test_list_lost_files_no_lost_files_in_project(client, cli_runner, boto3_sess # --------------------------------------------------------------------------------------- -def test_list_lost_files_missing_in_s3_in_project(client, cli_runner, boto3_session, capfd): +def test_list_lost_files_missing_in_s3_in_project( + client, cli_runner, boto3_session, capfd: LogCaptureFixture +): """flask lost-files ls: project specified, lost files in s3.""" # Get project project = models.Project.query.first() @@ -604,7 +651,9 @@ def test_list_lost_files_missing_in_s3_in_project(client, cli_runner, boto3_sess assert f"Lost files in project: {project.public_id}\t\tIn DB but not S3: {len(project.files)}\tIn S3 but not DB: 0\n" -def test_list_lost_files_no_lost_files_total(client, cli_runner, boto3_session, capfd): +def test_list_lost_files_no_lost_files_total( + client, cli_runner, boto3_session, capfd: LogCaptureFixture +): """flask lost-files ls: no project specified, no lost files.""" # Use sto2 -- no sto4_endpoint_added date --------------------------------------------- for u in models.Unit.query.all(): @@ -757,7 +806,9 @@ def test_list_lost_files_no_lost_files_total(client, cli_runner, boto3_session, # --------------------------------------------------------------------------------------- -def test_list_lost_files_missing_in_s3_in_project(client, cli_runner, boto3_session, capfd): +def test_list_lost_files_missing_in_s3_in_project( + client, cli_runner, boto3_session, capfd: LogCaptureFixture +): """flask lost-files ls: project specified, lost files in s3.""" # Run command result: click.testing.Result = cli_runner.invoke(lost_files_s3_db, ["ls"]) @@ -795,7 +846,7 @@ def test_add_missing_bucket_no_project(client, cli_runner): assert "Missing option '--project-id' / '-p'." in result.stdout -def test_add_missing_bucket_project_nonexistent(client, cli_runner, capfd): +def test_add_missing_bucket_project_nonexistent(client, cli_runner, capfd: LogCaptureFixture): """flask lost-files add-missing-bucket: no such project --> print out error.""" # Project -- doesn't exist project_id: str = "nonexistentproject" @@ -812,7 +863,7 @@ def test_add_missing_bucket_project_nonexistent(client, cli_runner, capfd): assert f"No such project: '{project_id}'" in err -def test_add_missing_bucket_project_inactive(client, cli_runner, capfd): +def test_add_missing_bucket_project_inactive(client, cli_runner, capfd: LogCaptureFixture): """flask lost-files add-missing-bucket: project specified, but inactive --> error message.""" # Get project project: models.Project = models.Project.query.first() @@ -834,7 +885,9 @@ def test_add_missing_bucket_project_inactive(client, cli_runner, capfd): assert f"Project '{project.public_id}' is not an active project." in err -def test_add_missing_bucket_not_missing(client, cli_runner, boto3_session, capfd): +def test_add_missing_bucket_not_missing( + client, cli_runner, boto3_session, capfd: LogCaptureFixture +): """flask lost-files add-missing-bucket: project specified, not missing --> ok.""" from tests.test_utils import mock_nosuchbucket @@ -958,7 +1011,7 @@ def test_delete_lost_files_no_project(client, cli_runner): assert "Missing option '--project-id' / '-p'." in result.stdout -def test_delete_lost_files_project_nonexistent(client, cli_runner, capfd): +def test_delete_lost_files_project_nonexistent(client, cli_runner, capfd: LogCaptureFixture): """flask lost-files delete: no such project --> print out error.""" # Project -- doesn't exist project_id: str = "nonexistentproject" @@ -975,7 +1028,7 @@ def test_delete_lost_files_project_nonexistent(client, cli_runner, capfd): assert f"No such project: '{project_id}'" in err -def test_delete_lost_files_deleted(client, cli_runner, boto3_session, capfd): +def test_delete_lost_files_deleted(client, cli_runner, boto3_session, capfd: LogCaptureFixture): """flask lost-files delete: project specified and exists --> deleted files ok.""" # Get project project: models.Project = models.Project.query.first() @@ -1077,7 +1130,9 @@ def test_delete_lost_files_deleted(client, cli_runner, boto3_session, capfd): # ------------------------------------------------------------------------ -def test_delete_lost_files_sqlalchemyerror(client, cli_runner, boto3_session, capfd): +def test_delete_lost_files_sqlalchemyerror( + client, cli_runner, boto3_session, capfd: LogCaptureFixture +): """flask lost-files delete: sqlalchemyerror during deletion.""" # Imports from tests.api.test_project import mock_sqlalchemyerror @@ -1104,7 +1159,7 @@ def test_delete_lost_files_sqlalchemyerror(client, cli_runner, boto3_session, ca # usage = 0 --> check log -def test_monitor_usage_no_usage(client, cli_runner, capfd): +def test_monitor_usage_no_usage(client, cli_runner, capfd: LogCaptureFixture): """If a unit has no uploaded data, there's no need to do the calculations or send email warning.""" # Mock the size property of the Unit table with patch("dds_web.database.models.Unit.size", new_callable=PropertyMock) as mock_size: @@ -1122,7 +1177,7 @@ def test_monitor_usage_no_usage(client, cli_runner, capfd): # percentage below warning level --> check log + no email -def test_monitor_usage_no_email(client, cli_runner, capfd): +def test_monitor_usage_no_email(client, cli_runner, capfd: LogCaptureFixture): """No email should be sent if the usage is below the warning level.""" # Define quota quota_in_test: int = 1e14 @@ -1152,7 +1207,7 @@ def test_monitor_usage_no_email(client, cli_runner, capfd): # percentage above warning level --> check log + email sent -def test_monitor_usage_warning_sent(client, cli_runner, capfd): +def test_monitor_usage_warning_sent(client, cli_runner, capfd: LogCaptureFixture): """An email should be sent if the usage is above the warning level.""" # Define quota quota_in_test: int = 1e14 From e0296293895d5c220f81faad8998729ce2e23352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 7 Sep 2023 13:39:42 +0200 Subject: [PATCH 027/133] sprintlog --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index cf5a1f9d2..44fac0bff 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -295,3 +295,4 @@ _Nothing merged in CLI during this sprint_ # 2023-09-4 - 2023-09-15 - Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) +- Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) From 0ab965c48a62ed6fd9460131668df0eedccda92b Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 7 Sep 2023 16:42:04 +0200 Subject: [PATCH 028/133] added test for listing projects of deleted users --- tests/test_project_listing.py | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index b04faa4d0..84294ff0f 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -29,6 +29,52 @@ def test_list_proj_no_token(client): assert "No token" in response_json.get("message") +def test_deleted_user_when_listing_projects + """ Deleted users that created a project should be listed as 'Former User' """ + + token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) + + # 1st Create project + response = module_client.post( + tests.DDSEndpoint.PROJECT_CREATE, + headers=token, + json=proj_data, + ) + assert response.status_code == http.HTTPStatus.OK + + # next, delete the user that created it + + email_to_delete = "unituser1@mailtrap.io" + test.create_delete_request(email_to_delete) + token_delete = test.get_deletion_token(email_to_delete) + + client = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) + + response = client.get( + tests.DDSEndpoint.USER_CONFIRM_DELETE + token_delete, + content_type="application/json", + headers=tests.DEFAULT_HEADER, + ) + + assert response.status_code == http.HTTPStatus.OK + + # list the project + response = client.get( + tests.DDSEndpoint.LIST_PROJ, + headers=token_unitadmin, + json={"usage": True}, + content_type="application/json", + ) + + assert response.status_code == http.HTTPStatus.OK + public_project = response.json.get("project_info")[0] + + # check that the name is Former User + assert "Former User" == public_project.get("Created by") + + + def test_list_proj_access_granted_ls(client): """Researcher should be able to list, "Created by" should be the Unit name""" @@ -142,6 +188,9 @@ def test_list_all_projects_unit_user(client): assert len(response.json.get("project_info")) == 5 + + + def test_proj_private_successful(client): """Successfully get the private key""" From cc1338c6415ca59de3b4d8ec537826705a19c6ab Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 09:09:50 +0200 Subject: [PATCH 029/133] fixed formatting --- tests/test_project_listing.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 84294ff0f..acebe6697 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -29,12 +29,12 @@ def test_list_proj_no_token(client): assert "No token" in response_json.get("message") -def test_deleted_user_when_listing_projects - """ Deleted users that created a project should be listed as 'Former User' """ +def test_deleted_user_when_listing_projects(client): + """Deleted users that created a project should be listed as 'Former User'""" token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) - + # 1st Create project response = module_client.post( tests.DDSEndpoint.PROJECT_CREATE, @@ -72,7 +72,6 @@ def test_deleted_user_when_listing_projects # check that the name is Former User assert "Former User" == public_project.get("Created by") - def test_list_proj_access_granted_ls(client): @@ -188,9 +187,6 @@ def test_list_all_projects_unit_user(client): assert len(response.json.get("project_info")) == 5 - - - def test_proj_private_successful(client): """Successfully get the private key""" From 9dccb8ee3842acb97d479e766810ad66d76cb602 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 10:28:15 +0200 Subject: [PATCH 030/133] client login added as a new object --- tests/test_project_listing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index acebe6697..0678b3df6 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -49,9 +49,9 @@ def test_deleted_user_when_listing_projects(client): test.create_delete_request(email_to_delete) token_delete = test.get_deletion_token(email_to_delete) - client = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) + client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) - response = client.get( + response = client_login.get( tests.DDSEndpoint.USER_CONFIRM_DELETE + token_delete, content_type="application/json", headers=tests.DEFAULT_HEADER, From eec2d586316cf6726f211156eeb6e32f0849d0ed Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 11:34:27 +0200 Subject: [PATCH 031/133] fixed typos --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 0678b3df6..54344c7de 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -36,7 +36,7 @@ def test_deleted_user_when_listing_projects(client): token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) # 1st Create project - response = module_client.post( + response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token, json=proj_data, From 748fc97e0fbc5693548380633930e0f2d180c596 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 13:40:37 +0200 Subject: [PATCH 032/133] fixed typos --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 54344c7de..93e824019 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -38,7 +38,7 @@ def test_deleted_user_when_listing_projects(client): # 1st Create project response = client.post( tests.DDSEndpoint.PROJECT_CREATE, - headers=token, + headers=token_unituser, json=proj_data, ) assert response.status_code == http.HTTPStatus.OK From 129365ebe15aa8661efbc1524b59ec9aabec27d0 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 14:20:34 +0200 Subject: [PATCH 033/133] added enough num admins to create project --- tests/test_project_listing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 93e824019..dba198862 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -36,6 +36,8 @@ def test_deleted_user_when_listing_projects(client): token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) # 1st Create project + # there has to be at least 3 unit admins + test.create_unit_admins(num_admins=3) response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token_unituser, From 4f3080cea854c26581cfaef5c42e0c082af0c445 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 14:48:22 +0200 Subject: [PATCH 034/133] added enough num admins to create project / fix typo --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index dba198862..644a5c3b9 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -37,7 +37,7 @@ def test_deleted_user_when_listing_projects(client): # 1st Create project # there has to be at least 3 unit admins - test.create_unit_admins(num_admins=3) + tests.create_unit_admins(num_admins=3) response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token_unituser, From b0d5b7e1b17d7574ebc5a44938b9ce2ffd9de870 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 15:18:25 +0200 Subject: [PATCH 035/133] not correct imports --- tests/test_project_listing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 644a5c3b9..756cecc28 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -8,8 +8,8 @@ from dds_web import db from dds_web.database import models import tests - - +import tests.test_project_creation +import tests.test_user_delete # CONFIG ################################################################################## CONFIG # proj_data = {"pi": "piName", "title": "Test proj", "description": "A longer project description"} @@ -37,7 +37,7 @@ def test_deleted_user_when_listing_projects(client): # 1st Create project # there has to be at least 3 unit admins - tests.create_unit_admins(num_admins=3) + tests.test_project_creation.create_unit_admins(num_admins=3) response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token_unituser, @@ -48,8 +48,8 @@ def test_deleted_user_when_listing_projects(client): # next, delete the user that created it email_to_delete = "unituser1@mailtrap.io" - test.create_delete_request(email_to_delete) - token_delete = test.get_deletion_token(email_to_delete) + tests.test_user_delete.create_delete_request(email_to_delete) + token_delete = tests.test_user_delete.get_deletion_token(email_to_delete) client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) From db0ed483567211ae178c0f5eb777f48da45092cf Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 15:20:31 +0200 Subject: [PATCH 036/133] not correct imports --- tests/test_project_listing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 756cecc28..abba08c2d 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -10,6 +10,7 @@ import tests import tests.test_project_creation import tests.test_user_delete + # CONFIG ################################################################################## CONFIG # proj_data = {"pi": "piName", "title": "Test proj", "description": "A longer project description"} From 21a574f0975b9526be45eb8df6689f2bc437cc7e Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 8 Sep 2023 15:53:12 +0200 Subject: [PATCH 037/133] not correct imports --- tests/test_project_listing.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index abba08c2d..4baaf1d77 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -13,7 +13,11 @@ # CONFIG ################################################################################## CONFIG # -proj_data = {"pi": "piName", "title": "Test proj", "description": "A longer project description"} +proj_data = { + "pi": "researchuser@mailtrap.io", + "title": "Test proj", + "description": "A longer project description", +} proj_query = {"project": "public_project_id"} proj_query_restricted = {"project": "restricted_project_id"} From 21728237283bbf38e16c0cf994e2a605d978abab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Fri, 8 Sep 2023 15:56:22 +0200 Subject: [PATCH 038/133] add new projectusers row --- dds_web/api/project.py | 14 ++++++++++++++ dds_web/security/project_user_keys.py | 1 + 2 files changed, 15 insertions(+) diff --git a/dds_web/api/project.py b/dds_web/api/project.py index da18a9340..2dc29fc17 100644 --- a/dds_web/api/project.py +++ b/dds_web/api/project.py @@ -911,6 +911,20 @@ def give_project_access(project_list, current_user, user): project_id=proj.id, user_id=user.username ).one_or_none() if not project_keys_row: + # Make sure that Researchers are also listed in project users + if ( + user.role == "Researcher" + and not models.ProjectUsers.query.filter_by( + project_id=proj.id, user_id=user.username + ).one_or_none() + ): + # New row in association table + new_projectuser_row = models.ProjectUsers( + project_id=proj.id, user_id=user.username + ) + # Append association -- only one required, not both ways + proj.researchusers.append(new_projectuser_row) + share_project_private_key( from_user=current_user, to_another=user, diff --git a/dds_web/security/project_user_keys.py b/dds_web/security/project_user_keys.py index ea2eb7f55..240bdcf29 100644 --- a/dds_web/security/project_user_keys.py +++ b/dds_web/security/project_user_keys.py @@ -127,6 +127,7 @@ def share_project_private_key( def __init_and_append_project_user_key(user, project, project_private_key): + """Create a new row in ProjectUserKeys for specific user and project.""" project_user_key = models.ProjectUserKeys( project_id=project.id, user_id=user.username, From 5200f4efcad7a5363338e2b1d8f845b960ea2ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Mon, 11 Sep 2023 08:19:47 +0200 Subject: [PATCH 039/133] sprintlog --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 44fac0bff..0b5e92db8 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -296,3 +296,4 @@ _Nothing merged in CLI during this sprint_ - Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) +- Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) From dbd1431160751204dbe5bf13b5572079ae4a3152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Mon, 11 Sep 2023 09:13:52 +0200 Subject: [PATCH 040/133] add tests for project user key --- tests/test_project_access.py | 81 ++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 6028825a0..de72e5ac8 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -405,3 +405,84 @@ def test_fix_access_unitadmin_valid_email_unituser(client): project_id=project.id, user_id="unituser" ).first() assert user_project_key_row + + +def test_fix_access_unitadmin_valid_email_unituser_no_project(client): + """Unit Admin giving access to unituser - ok. No project.""" + # Get project from database + project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() + assert project + + # Delete projectuserkey and verify that it's deleted for user + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id="unituser" + ).first() + if user_project_key_row: + db.session.delete(user_project_key_row) + db.session.commit() + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id="unituser" + ).first() + assert not user_project_key_row + + # Fix access for user with no project specified + token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) + response = client.post( + tests.DDSEndpoint.PROJECT_ACCESS, + headers=token, + json={"email": "unituser1@mailtrap.io"}, + ) + assert response.status_code == http.HTTPStatus.OK + + # Verify that the projectuserkey row is fixed + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id="unituser" + ).first() + assert user_project_key_row + + +def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): + """Unit Admin giving access to researcher where""" + # Get project from database + project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() + assert project + + # Delete projectuserkey and verify that it's deleted for user + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id="researchuser" + ).first() + if user_project_key_row: + db.session.delete(user_project_key_row) + db.session.commit() + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id="researchuser" + ).first() + assert not user_project_key_row + + # Delete projectuser row and verify that it's deleted for user + project_users_row = models.ProjectUsers.query.filter_by( + project_id=project.id, user_id="researchuser" + ).first() + if project_users_row: + db.session.delete(project_users_row) + db.session.commit() + project_users_row = models.ProjectUsers.query.filter_by( + project_id=project.id, user_id="researchuser" + ).first() + assert not project_users_row + + # Fix access for user with no project specified + token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) + response = client.post( + tests.DDSEndpoint.PROJECT_ACCESS, + headers=token, + query_string={"project": project.public_id}, + json={"email": "researchuser@mailtrap.io"}, + ) + assert response.status_code == http.HTTPStatus.OK + + # Verify that the projectuserkey row is fixed + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id="unituser" + ).first() + assert user_project_key_row From ee2f934ef15a844af97fd244c4d267074bc004f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Mon, 11 Sep 2023 09:19:52 +0200 Subject: [PATCH 041/133] typo --- tests/test_project_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index de72e5ac8..0a35ce07f 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -483,6 +483,6 @@ def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): # Verify that the projectuserkey row is fixed user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id="unituser" + project_id=project.id, user_id="researchuser" ).first() assert user_project_key_row From 7395db19a9d8871a22e3648776d86ffedd05578f Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 11:49:21 +0200 Subject: [PATCH 042/133] added boto3 sesion for sucesfull project creation --- tests/test_project_listing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 4baaf1d77..942271334 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -8,8 +8,8 @@ from dds_web import db from dds_web.database import models import tests -import tests.test_project_creation -import tests.test_user_delete +from tests.test_project_creation import create_unit_admins +from tests.test_user_delete import create_delete_request,get_deletion_token # CONFIG ################################################################################## CONFIG # @@ -34,7 +34,7 @@ def test_list_proj_no_token(client): assert "No token" in response_json.get("message") -def test_deleted_user_when_listing_projects(client): +def test_deleted_user_when_listing_projects(client,boto3_session): """Deleted users that created a project should be listed as 'Former User'""" token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) @@ -42,7 +42,7 @@ def test_deleted_user_when_listing_projects(client): # 1st Create project # there has to be at least 3 unit admins - tests.test_project_creation.create_unit_admins(num_admins=3) + create_unit_admins(num_admins=3) response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token_unituser, @@ -53,8 +53,8 @@ def test_deleted_user_when_listing_projects(client): # next, delete the user that created it email_to_delete = "unituser1@mailtrap.io" - tests.test_user_delete.create_delete_request(email_to_delete) - token_delete = tests.test_user_delete.get_deletion_token(email_to_delete) + create_delete_request(email_to_delete) + get_deletion_token(email_to_delete) client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) From 7096ca47490db3902421241111743f0b8c59194e Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 11:57:14 +0200 Subject: [PATCH 043/133] added boto3 sesion for sucesfull project creation --- tests/test_project_listing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 942271334..5105ff7a3 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -9,7 +9,7 @@ from dds_web.database import models import tests from tests.test_project_creation import create_unit_admins -from tests.test_user_delete import create_delete_request,get_deletion_token +from tests.test_user_delete import create_delete_request, get_deletion_token # CONFIG ################################################################################## CONFIG # @@ -34,7 +34,7 @@ def test_list_proj_no_token(client): assert "No token" in response_json.get("message") -def test_deleted_user_when_listing_projects(client,boto3_session): +def test_deleted_user_when_listing_projects(client, boto3_session): """Deleted users that created a project should be listed as 'Former User'""" token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) From 551df6138e108d28704b674ff05b8768cc6e9b29 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 13:52:39 +0200 Subject: [PATCH 044/133] fixed user to delete --- tests/test_project_listing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 5105ff7a3..794689f6a 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -37,12 +37,13 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client, boto3_session): """Deleted users that created a project should be listed as 'Former User'""" - token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) # 1st Create project # there has to be at least 3 unit admins create_unit_admins(num_admins=3) + response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token_unituser, @@ -52,11 +53,11 @@ def test_deleted_user_when_listing_projects(client, boto3_session): # next, delete the user that created it - email_to_delete = "unituser1@mailtrap.io" + email_to_delete = "unituser2@mailtrap.io" create_delete_request(email_to_delete) - get_deletion_token(email_to_delete) + token_delete = get_deletion_token(email_to_delete) - client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) + client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).fake_web_login(client) response = client_login.get( tests.DDSEndpoint.USER_CONFIRM_DELETE + token_delete, From 096ce93e9057e9717989cc377838ec98a11e7e19 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 14:23:33 +0200 Subject: [PATCH 045/133] unituser 2 not initialized --- tests/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/__init__.py b/tests/__init__.py index eb27869bf..71aefebfd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -34,6 +34,7 @@ "researchuser2": "researchuser2:password", "projectowner": "projectowner:password", "unituser": "unituser:password", + "unituser2": "unituser2:password" "unitadmin": "unitadmin:password", "superadmin": "superadmin:password", "delete_me_researcher": "delete_me_researcher:password", From fcc9168115e6fb0b8f29975ddaf92acc6e6e87f9 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 14:24:00 +0200 Subject: [PATCH 046/133] unituser 2 not initialized --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 71aefebfd..9f45f2447 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -34,7 +34,7 @@ "researchuser2": "researchuser2:password", "projectowner": "projectowner:password", "unituser": "unituser:password", - "unituser2": "unituser2:password" + "unituser2": "unituser2:password", "unitadmin": "unitadmin:password", "superadmin": "superadmin:password", "delete_me_researcher": "delete_me_researcher:password", From 1226a390c892829f1b63f3a919a2ae81ce69f45d Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 17:41:00 +0200 Subject: [PATCH 047/133] list projects error --- tests/test_project_listing.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 794689f6a..180f35e8f 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -37,13 +37,11 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client, boto3_session): """Deleted users that created a project should be listed as 'Former User'""" - token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) - token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) - # 1st Create project # there has to be at least 3 unit admins create_unit_admins(num_admins=3) + token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) response = client.post( tests.DDSEndpoint.PROJECT_CREATE, headers=token_unituser, @@ -67,19 +65,18 @@ def test_deleted_user_when_listing_projects(client, boto3_session): assert response.status_code == http.HTTPStatus.OK + token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) # list the project response = client.get( tests.DDSEndpoint.LIST_PROJ, headers=token_unitadmin, - json={"usage": True}, - content_type="application/json", ) assert response.status_code == http.HTTPStatus.OK - public_project = response.json.get("project_info")[0] + project = response.json.get("project_info")[-1] # check that the name is Former User - assert "Former User" == public_project.get("Created by") + assert "Former User" == project.get("Created by") def test_list_proj_access_granted_ls(client): From ac7aeb38c2c55c0884432fb4b4be36c1efa74225 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 18:09:07 +0200 Subject: [PATCH 048/133] try listing with user 1 --- tests/test_project_listing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 180f35e8f..18cbc89d3 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -36,6 +36,7 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client, boto3_session): """Deleted users that created a project should be listed as 'Former User'""" + #token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) # 1st Create project # there has to be at least 3 unit admins @@ -65,11 +66,11 @@ def test_deleted_user_when_listing_projects(client, boto3_session): assert response.status_code == http.HTTPStatus.OK - token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) + token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) # list the project response = client.get( tests.DDSEndpoint.LIST_PROJ, - headers=token_unitadmin, + headers=token_unituser, ) assert response.status_code == http.HTTPStatus.OK From 292e9771c474747d42ce70ca32c304a85094541d Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 11 Sep 2023 18:22:37 +0200 Subject: [PATCH 049/133] try listing with user 1 --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 18cbc89d3..f8120986b 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -36,7 +36,7 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client, boto3_session): """Deleted users that created a project should be listed as 'Former User'""" - #token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) + # token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) # 1st Create project # there has to be at least 3 unit admins From aea34ce409ee3b9e5c3a782237a1270d5a8c2a79 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 09:18:04 +0200 Subject: [PATCH 050/133] delete already existing project instead --- tests/test_project_listing.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index f8120986b..269617f79 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -34,21 +34,22 @@ def test_list_proj_no_token(client): assert "No token" in response_json.get("message") -def test_deleted_user_when_listing_projects(client, boto3_session): +def test_deleted_user_when_listing_projects(client): """Deleted users that created a project should be listed as 'Former User'""" # token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) + token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) # 1st Create project # there has to be at least 3 unit admins - create_unit_admins(num_admins=3) + #create_unit_admins(num_admins=3) - token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) - response = client.post( - tests.DDSEndpoint.PROJECT_CREATE, - headers=token_unituser, - json=proj_data, - ) - assert response.status_code == http.HTTPStatus.OK + token_unituser2 = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) + #response = client.post( + # tests.DDSEndpoint.PROJECT_CREATE, + # headers=token_unituser, + # json=proj_data, + #) + #assert response.status_code == http.HTTPStatus.OK # next, delete the user that created it @@ -66,15 +67,15 @@ def test_deleted_user_when_listing_projects(client, boto3_session): assert response.status_code == http.HTTPStatus.OK - token_unituser = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + #token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) # list the project response = client.get( tests.DDSEndpoint.LIST_PROJ, - headers=token_unituser, + headers=token_unituser1, ) assert response.status_code == http.HTTPStatus.OK - project = response.json.get("project_info")[-1] + project = response.json.get("project_info")[0] # check that the name is Former User assert "Former User" == project.get("Created by") From 8ceb417f4e8f7a70e35a9ccfecb6d6c92e37e486 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 09:18:23 +0200 Subject: [PATCH 051/133] delete already existing project instead --- tests/test_project_listing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 269617f79..92aa57852 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -41,15 +41,15 @@ def test_deleted_user_when_listing_projects(client): # 1st Create project # there has to be at least 3 unit admins - #create_unit_admins(num_admins=3) + # create_unit_admins(num_admins=3) token_unituser2 = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) - #response = client.post( + # response = client.post( # tests.DDSEndpoint.PROJECT_CREATE, # headers=token_unituser, # json=proj_data, - #) - #assert response.status_code == http.HTTPStatus.OK + # ) + # assert response.status_code == http.HTTPStatus.OK # next, delete the user that created it @@ -67,7 +67,7 @@ def test_deleted_user_when_listing_projects(client): assert response.status_code == http.HTTPStatus.OK - #token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + # token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) # list the project response = client.get( tests.DDSEndpoint.LIST_PROJ, From c271deadfd14771d628f06e547f4f275e53555b9 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 09:20:14 +0200 Subject: [PATCH 052/133] delete already existing project instead --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 92aa57852..b0ec80ae3 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -75,7 +75,7 @@ def test_deleted_user_when_listing_projects(client): ) assert response.status_code == http.HTTPStatus.OK - project = response.json.get("project_info")[0] + project = response.json.get("project_info")[1] # check that the name is Former User assert "Former User" == project.get("Created by") From 4146cbbe856499f3f02bb91213fbebb99a4a96d5 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 10:23:21 +0200 Subject: [PATCH 053/133] deep copy of client before deletion --- tests/test_project_listing.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index b0ec80ae3..5f65acde4 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -3,6 +3,7 @@ # Standard library import http import unittest +import copy # Own from dds_web import db @@ -37,13 +38,13 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client): """Deleted users that created a project should be listed as 'Former User'""" # token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) - token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + # token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) # 1st Create project # there has to be at least 3 unit admins # create_unit_admins(num_admins=3) - token_unituser2 = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) + # token_unituser2 = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) # response = client.post( # tests.DDSEndpoint.PROJECT_CREATE, # headers=token_unituser, @@ -57,7 +58,8 @@ def test_deleted_user_when_listing_projects(client): create_delete_request(email_to_delete) token_delete = get_deletion_token(email_to_delete) - client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).fake_web_login(client) + client_login = copy.deepcopy(client) + client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).fake_web_login(client_login) response = client_login.get( tests.DDSEndpoint.USER_CONFIRM_DELETE + token_delete, @@ -67,7 +69,7 @@ def test_deleted_user_when_listing_projects(client): assert response.status_code == http.HTTPStatus.OK - # token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) # list the project response = client.get( tests.DDSEndpoint.LIST_PROJ, From 6f0be79fdc8622ff039011a52e55a1c06bffa936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Tue, 12 Sep 2023 11:33:12 +0200 Subject: [PATCH 054/133] update comment --- tests/test_project_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 0a35ce07f..85d698d93 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -442,7 +442,7 @@ def test_fix_access_unitadmin_valid_email_unituser_no_project(client): def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): - """Unit Admin giving access to researcher where""" + """Unit Admin giving access to researcher where there is no row in ProjectUsers table.""" # Get project from database project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() assert project From f0b1ffef3c555831a044307616fe9638513f8a86 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 11:49:24 +0200 Subject: [PATCH 055/133] fixed token issued with client --- tests/test_project_listing.py | 39 ++++++++++++++--------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 5f65acde4..b80b8e1bb 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -37,31 +37,15 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client): """Deleted users that created a project should be listed as 'Former User'""" - # token_unitadmin = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) - # token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) - - # 1st Create project - # there has to be at least 3 unit admins - # create_unit_admins(num_admins=3) - - # token_unituser2 = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).token(client) - # response = client.post( - # tests.DDSEndpoint.PROJECT_CREATE, - # headers=token_unituser, - # json=proj_data, - # ) - # assert response.status_code == http.HTTPStatus.OK - - # next, delete the user that created it + # delete unituser 2 email_to_delete = "unituser2@mailtrap.io" create_delete_request(email_to_delete) token_delete = get_deletion_token(email_to_delete) - client_login = copy.deepcopy(client) - client_login = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).fake_web_login(client_login) + client = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).fake_web_login(client) - response = client_login.get( + response = client.get( tests.DDSEndpoint.USER_CONFIRM_DELETE + token_delete, content_type="application/json", headers=tests.DEFAULT_HEADER, @@ -69,18 +53,27 @@ def test_deleted_user_when_listing_projects(client): assert response.status_code == http.HTTPStatus.OK + # login again with unituser1 to list the projects + client = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) + + # requests to list projects token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) - # list the project response = client.get( tests.DDSEndpoint.LIST_PROJ, headers=token_unituser1, ) assert response.status_code == http.HTTPStatus.OK - project = response.json.get("project_info")[1] - # check that the name is Former User - assert "Former User" == project.get("Created by") + # in Conftests.py it is specified the list of projects + # unituser 2 created "unused_project_id" and "second_public_project_id" + projects = response.json.get("project_info") + for project in projects: + p_id = project.get("Project ID") + if p_id == "unused_project_id" or p_id == "second_public_project_id": + # check that the name is Former User + assert "Former User" == project.get("Created by") + def test_list_proj_access_granted_ls(client): From d8354057329db0466a972bf5269ade6c84b28806 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 11:49:36 +0200 Subject: [PATCH 056/133] fixed token issued with client --- tests/test_project_listing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index b80b8e1bb..5688fb9d5 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -65,7 +65,7 @@ def test_deleted_user_when_listing_projects(client): assert response.status_code == http.HTTPStatus.OK - # in Conftests.py it is specified the list of projects + # in Conftests.py it is specified the list of projects # unituser 2 created "unused_project_id" and "second_public_project_id" projects = response.json.get("project_info") for project in projects: @@ -73,7 +73,6 @@ def test_deleted_user_when_listing_projects(client): if p_id == "unused_project_id" or p_id == "second_public_project_id": # check that the name is Former User assert "Former User" == project.get("Created by") - def test_list_proj_access_granted_ls(client): From 9856bd29612946363afb292baaacb650700a710a Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 11:52:12 +0200 Subject: [PATCH 057/133] reformating --- tests/test_project_listing.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 5688fb9d5..930ae179c 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -3,7 +3,6 @@ # Standard library import http import unittest -import copy # Own from dds_web import db @@ -14,11 +13,7 @@ # CONFIG ################################################################################## CONFIG # -proj_data = { - "pi": "researchuser@mailtrap.io", - "title": "Test proj", - "description": "A longer project description", -} +proj_data = {"pi": "piName", "title": "Test proj", "description": "A longer project description"} proj_query = {"project": "public_project_id"} proj_query_restricted = {"project": "restricted_project_id"} From 4acd8bd31e3e0d3d7a0e794683dd57239d7247a6 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 17:27:15 +0200 Subject: [PATCH 058/133] improved to avoid hardcoding --- tests/test_project_listing.py | 38 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 930ae179c..a617c81c3 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -9,7 +9,7 @@ from dds_web.database import models import tests from tests.test_project_creation import create_unit_admins -from tests.test_user_delete import create_delete_request, get_deletion_token +from tests.test_user_delete import create_delete_request, get_deletion_token, user_from_email # CONFIG ################################################################################## CONFIG # @@ -17,6 +17,15 @@ proj_query = {"project": "public_project_id"} proj_query_restricted = {"project": "restricted_project_id"} + +#################################### UTILITY FUNCTIONS ################################## + +def get_projects_user_is_creator(user): + """get the projects where that user was the creator""" + + projects = models.Project.query.filter_by(created_by=user).all() + return [ project.__dict__.get("public_id") for project in projects ] + # TESTS #################################################################################### TESTS # @@ -33,39 +42,38 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client): """Deleted users that created a project should be listed as 'Former User'""" - # delete unituser 2 + #user to delete email_to_delete = "unituser2@mailtrap.io" - create_delete_request(email_to_delete) - token_delete = get_deletion_token(email_to_delete) + user_to_delete = user_from_email(email_to_delete).__dict__['username'] - client = tests.UserAuth(tests.USER_CREDENTIALS["unituser2"]).fake_web_login(client) + # get the list of projects user was involved + was_creator = get_projects_user_is_creator(user=user_to_delete) + # create the delete request for said user + create_delete_request(email_to_delete) + token_delete = get_deletion_token(email_to_delete) + client = tests.UserAuth(tests.USER_CREDENTIALS[user_to_delete]).fake_web_login(client) response = client.get( tests.DDSEndpoint.USER_CONFIRM_DELETE + token_delete, content_type="application/json", headers=tests.DEFAULT_HEADER, ) - assert response.status_code == http.HTTPStatus.OK - # login again with unituser1 to list the projects + # login again with a different user from the same unit to see the projects client = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).fake_web_login(client) - # requests to list projects - token_unituser1 = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client) + # requests to list all projects response = client.get( tests.DDSEndpoint.LIST_PROJ, - headers=token_unituser1, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client), ) - assert response.status_code == http.HTTPStatus.OK - # in Conftests.py it is specified the list of projects - # unituser 2 created "unused_project_id" and "second_public_project_id" + # Compare the new list of projects and see if the creators name is different projects = response.json.get("project_info") for project in projects: - p_id = project.get("Project ID") - if p_id == "unused_project_id" or p_id == "second_public_project_id": + if project.get("Project ID") in was_creator: # check that the name is Former User assert "Former User" == project.get("Created by") From 3b87b92fd8c2efd8187f35a764f6134823462266 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 12 Sep 2023 17:33:59 +0200 Subject: [PATCH 059/133] black lint --- tests/test_project_listing.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index a617c81c3..d6a3b4745 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -20,11 +20,13 @@ #################################### UTILITY FUNCTIONS ################################## + def get_projects_user_is_creator(user): """get the projects where that user was the creator""" projects = models.Project.query.filter_by(created_by=user).all() - return [ project.__dict__.get("public_id") for project in projects ] + return [project.__dict__.get("public_id") for project in projects] + # TESTS #################################################################################### TESTS # @@ -42,9 +44,9 @@ def test_list_proj_no_token(client): def test_deleted_user_when_listing_projects(client): """Deleted users that created a project should be listed as 'Former User'""" - #user to delete + # user to delete email_to_delete = "unituser2@mailtrap.io" - user_to_delete = user_from_email(email_to_delete).__dict__['username'] + user_to_delete = user_from_email(email_to_delete).__dict__["username"] # get the list of projects user was involved was_creator = get_projects_user_is_creator(user=user_to_delete) From 32b43bff3a7b15e9b1527692d74f5f365d1f0834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 09:04:20 +0200 Subject: [PATCH 060/133] fix comment from review --- tests/test_project_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 85d698d93..49ea1b3fc 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -471,7 +471,7 @@ def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): ).first() assert not project_users_row - # Fix access for user with no project specified + # Fix access for user, project specified token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) response = client.post( tests.DDSEndpoint.PROJECT_ACCESS, From 8fc9950599ff006787dfedb78dd432ffc9492541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Wed, 13 Sep 2023 09:21:15 +0200 Subject: [PATCH 061/133] refactoring code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index d6a3b4745..470eb837e 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -25,7 +25,7 @@ def get_projects_user_is_creator(user): """get the projects where that user was the creator""" projects = models.Project.query.filter_by(created_by=user).all() - return [project.__dict__.get("public_id") for project in projects] + return [project.public_id for project in projects] # TESTS #################################################################################### TESTS # From b019b9910b4a544844f3dde221beb57d430ace96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Wed, 13 Sep 2023 09:21:31 +0200 Subject: [PATCH 062/133] refactoring code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/test_project_listing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_project_listing.py b/tests/test_project_listing.py index 470eb837e..8773e7798 100644 --- a/tests/test_project_listing.py +++ b/tests/test_project_listing.py @@ -46,7 +46,7 @@ def test_deleted_user_when_listing_projects(client): # user to delete email_to_delete = "unituser2@mailtrap.io" - user_to_delete = user_from_email(email_to_delete).__dict__["username"] + user_to_delete = user_from_email(email_to_delete).username # get the list of projects user was involved was_creator = get_projects_user_is_creator(user=user_to_delete) From 53fbeee6ad44b1dd6af8e40e082e67a7a2138507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 09:27:52 +0200 Subject: [PATCH 063/133] review suggestion --- tests/test_project_access.py | 56 +++++++++++++++++------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 49ea1b3fc..0b3d8bc33 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -15,6 +15,28 @@ proj_query = {"project": "public_project_id"} # proj_query_restricted = {"project": "restricted_project_id"} + +# UTILITY FUNCTIONS ############################################################ UTILITY FUNCTIONS # + +def delete_project_user(project_id,user_id): + # Get project from database + project = models.Project.query.filter_by(public_id=project_id).one_or_none() + assert project + + # Delete projectuserkey and verify that it's deleted for user + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id=user_id + ).first() + if user_project_key_row: + db.session.delete(user_project_key_row) + db.session.commit() + user_project_key_row = models.ProjectUserKeys.query.filter_by( + project_id=project.id, user_id=user_id + ).first() + assert not user_project_key_row + + return project + # TESTS #################################################################################### TESTS # @@ -409,21 +431,8 @@ def test_fix_access_unitadmin_valid_email_unituser(client): def test_fix_access_unitadmin_valid_email_unituser_no_project(client): """Unit Admin giving access to unituser - ok. No project.""" - # Get project from database - project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() - assert project - - # Delete projectuserkey and verify that it's deleted for user - user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id="unituser" - ).first() - if user_project_key_row: - db.session.delete(user_project_key_row) - db.session.commit() - user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id="unituser" - ).first() - assert not user_project_key_row + # Remove ProjectUserKeys row for specific project and user + project: models.Project = delete_project_user(project_id="public_project_id", user_id="unituser") # Fix access for user with no project specified token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) @@ -443,21 +452,8 @@ def test_fix_access_unitadmin_valid_email_unituser_no_project(client): def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): """Unit Admin giving access to researcher where there is no row in ProjectUsers table.""" - # Get project from database - project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() - assert project - - # Delete projectuserkey and verify that it's deleted for user - user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id="researchuser" - ).first() - if user_project_key_row: - db.session.delete(user_project_key_row) - db.session.commit() - user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id="researchuser" - ).first() - assert not user_project_key_row + # Remove ProjectUserKeys row for specific project and user + project: models.Project = delete_project_user(project_id="public_project_id", user_id="researchuser") # Delete projectuser row and verify that it's deleted for user project_users_row = models.ProjectUsers.query.filter_by( From 9e865c2e1ec6575017fc2232b5fe1487bc25bd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 09:28:04 +0200 Subject: [PATCH 064/133] black --- tests/test_project_access.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 0b3d8bc33..7d758c9d3 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -18,7 +18,8 @@ # UTILITY FUNCTIONS ############################################################ UTILITY FUNCTIONS # -def delete_project_user(project_id,user_id): + +def delete_project_user(project_id, user_id): # Get project from database project = models.Project.query.filter_by(public_id=project_id).one_or_none() assert project @@ -37,6 +38,7 @@ def delete_project_user(project_id,user_id): return project + # TESTS #################################################################################### TESTS # @@ -432,7 +434,9 @@ def test_fix_access_unitadmin_valid_email_unituser(client): def test_fix_access_unitadmin_valid_email_unituser_no_project(client): """Unit Admin giving access to unituser - ok. No project.""" # Remove ProjectUserKeys row for specific project and user - project: models.Project = delete_project_user(project_id="public_project_id", user_id="unituser") + project: models.Project = delete_project_user( + project_id="public_project_id", user_id="unituser" + ) # Fix access for user with no project specified token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) @@ -453,7 +457,9 @@ def test_fix_access_unitadmin_valid_email_unituser_no_project(client): def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): """Unit Admin giving access to researcher where there is no row in ProjectUsers table.""" # Remove ProjectUserKeys row for specific project and user - project: models.Project = delete_project_user(project_id="public_project_id", user_id="researchuser") + project: models.Project = delete_project_user( + project_id="public_project_id", user_id="researchuser" + ) # Delete projectuser row and verify that it's deleted for user project_users_row = models.ProjectUsers.query.filter_by( From 9706d9f876d5396531108b3bb940458e851cc4e9 Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Wed, 13 Sep 2023 15:36:59 +0200 Subject: [PATCH 065/133] Bump MariaBD to version 10.11.5 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index dd7a6939f..6b0739cc2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ version: "3.9" services: db: container_name: dds_database - image: mariadb:10.7.8 + image: mariadb:10.11.5 environment: - MYSQL_ROOT_PASSWORD=${DDS_MYSQL_ROOT_PASS} - MYSQL_USER=${DDS_MYSQL_USER} From bf16a3437b19b22981eec7d9a609b8a6596378a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 16:40:47 +0200 Subject: [PATCH 066/133] specify which table to use --- tests/test_project_access.py | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 7d758c9d3..dfe6ed9cf 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -19,22 +19,19 @@ # UTILITY FUNCTIONS ############################################################ UTILITY FUNCTIONS # -def delete_project_user(project_id, user_id): +def delete_project_user(project_id, user_id, table_to_use): + """Delete row in either ProjectUsers or ProjectUserKeys.""" # Get project from database project = models.Project.query.filter_by(public_id=project_id).one_or_none() assert project # Delete projectuserkey and verify that it's deleted for user - user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id=user_id - ).first() - if user_project_key_row: - db.session.delete(user_project_key_row) + user_project_row = table_to_use.query.filter_by(project_id=project.id, user_id=user_id).first() + if user_project_row: + db.session.delete(user_project_row) db.session.commit() - user_project_key_row = models.ProjectUserKeys.query.filter_by( - project_id=project.id, user_id=user_id - ).first() - assert not user_project_key_row + user_project_row = table_to_use.query.filter_by(project_id=project.id, user_id=user_id).first() + assert not user_project_row return project @@ -435,7 +432,7 @@ def test_fix_access_unitadmin_valid_email_unituser_no_project(client): """Unit Admin giving access to unituser - ok. No project.""" # Remove ProjectUserKeys row for specific project and user project: models.Project = delete_project_user( - project_id="public_project_id", user_id="unituser" + project_id="public_project_id", user_id="unituser", table_to_use=models.ProjectUserKeys ) # Fix access for user with no project specified @@ -458,20 +455,23 @@ def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): """Unit Admin giving access to researcher where there is no row in ProjectUsers table.""" # Remove ProjectUserKeys row for specific project and user project: models.Project = delete_project_user( - project_id="public_project_id", user_id="researchuser" + project_id="public_project_id", user_id="researchuser", table_to_use=models.ProjectUserKeys ) # Delete projectuser row and verify that it's deleted for user - project_users_row = models.ProjectUsers.query.filter_by( - project_id=project.id, user_id="researchuser" - ).first() - if project_users_row: - db.session.delete(project_users_row) - db.session.commit() - project_users_row = models.ProjectUsers.query.filter_by( - project_id=project.id, user_id="researchuser" - ).first() - assert not project_users_row + _ = delete_project_user( + project="public_project_id", user_id="researcher", table_to_use=models.ProjectUsers + ) + # project_users_row = models.ProjectUsers.query.filter_by( + # project_id=project.id, user_id="researchuser" + # ).first() + # if project_users_row: + # db.session.delete(project_users_row) + # db.session.commit() + # project_users_row = models.ProjectUsers.query.filter_by( + # project_id=project.id, user_id="researchuser" + # ).first() + # assert not project_users_row # Fix access for user, project specified token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) From fab36aaa1bed199c30d2bd10f7ac0f420e73b56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 17:01:56 +0200 Subject: [PATCH 067/133] fixed typo --- dds_web/__init__.py | 3 +-- tests/test_project_access.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dds_web/__init__.py b/dds_web/__init__.py index 318ff4c2f..b793e4d86 100644 --- a/dds_web/__init__.py +++ b/dds_web/__init__.py @@ -96,8 +96,7 @@ def action_wrapper(_, __, event_dict): "class": "logging.handlers.RotatingFileHandler", "maxBytes": app.config.get("LOG_MAX_SIZE"), "backupCount": app.config.get("LOG_BACKUP_COUNT"), - "filename": pathlib.Path(app.config.get("LOGS_DIR")) - / pathlib.Path("actions.log"), + "filename": pathlib.Path(app.config.get("LOGS_DIR")) / pathlib.Path("actions.log"), "formatter": "default", }, "console": { diff --git a/tests/test_project_access.py b/tests/test_project_access.py index dfe6ed9cf..539298a25 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -460,7 +460,7 @@ def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): # Delete projectuser row and verify that it's deleted for user _ = delete_project_user( - project="public_project_id", user_id="researcher", table_to_use=models.ProjectUsers + project_id="public_project_id", user_id="researcher", table_to_use=models.ProjectUsers ) # project_users_row = models.ProjectUsers.query.filter_by( # project_id=project.id, user_id="researchuser" From aa4b4914c29cb6c706b8fd6ceec2f92502b00980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 17:03:33 +0200 Subject: [PATCH 068/133] black --- dds_web/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dds_web/__init__.py b/dds_web/__init__.py index b793e4d86..318ff4c2f 100644 --- a/dds_web/__init__.py +++ b/dds_web/__init__.py @@ -96,7 +96,8 @@ def action_wrapper(_, __, event_dict): "class": "logging.handlers.RotatingFileHandler", "maxBytes": app.config.get("LOG_MAX_SIZE"), "backupCount": app.config.get("LOG_BACKUP_COUNT"), - "filename": pathlib.Path(app.config.get("LOGS_DIR")) / pathlib.Path("actions.log"), + "filename": pathlib.Path(app.config.get("LOGS_DIR")) + / pathlib.Path("actions.log"), "formatter": "default", }, "console": { From b49c89aa4a1d5993191100de8befb6830f805bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Wed, 13 Sep 2023 17:04:15 +0200 Subject: [PATCH 069/133] remove commented rows --- tests/test_project_access.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_project_access.py b/tests/test_project_access.py index 539298a25..3b423af38 100644 --- a/tests/test_project_access.py +++ b/tests/test_project_access.py @@ -462,16 +462,6 @@ def test_fix_access_unitadmin_valid_email_researcher_no_projectuser_row(client): _ = delete_project_user( project_id="public_project_id", user_id="researcher", table_to_use=models.ProjectUsers ) - # project_users_row = models.ProjectUsers.query.filter_by( - # project_id=project.id, user_id="researchuser" - # ).first() - # if project_users_row: - # db.session.delete(project_users_row) - # db.session.commit() - # project_users_row = models.ProjectUsers.query.filter_by( - # project_id=project.id, user_id="researchuser" - # ).first() - # assert not project_users_row # Fix access for user, project specified token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client) From 2c4a80c4f28c813e99e209f30c3b815338c85d02 Mon Sep 17 00:00:00 2001 From: valyo <582646+valyo@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:52:18 +0200 Subject: [PATCH 070/133] sprintlog --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 44fac0bff..097a7c0eb 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -296,3 +296,4 @@ _Nothing merged in CLI during this sprint_ - Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) +- Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) From d43e2a64e38e61e26bacfbb45f2b0e73d9fb29d7 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 14 Sep 2023 12:06:03 +0200 Subject: [PATCH 071/133] added test and functionality --- dds_web/api/user.py | 10 ++++++++++ tests/api/test_user.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/dds_web/api/user.py b/dds_web/api/user.py index aaedce3b9..d0f152a51 100644 --- a/dds_web/api/user.py +++ b/dds_web/api/user.py @@ -86,6 +86,16 @@ def post(self): except sqlalchemy.exc.OperationalError as err: raise ddserr.DatabaseError(message=str(err), alt_message="Unexpected database error.") + # check if the user invite should have expired and be deleted + if unanswered_invite: + # If actual time minus 168 hours (7 days) is greater than the time in the invite -> is expired, delete it + if ( + datetime.datetime.utcnow() - datetime.timedelta(hours=168) + ) > unanswered_invite.created_at: + db.session.delete(unanswered_invite) + db.session.commit() + unanswered_invite = None + if existing_user or unanswered_invite: if not project: raise ddserr.DDSArgumentError( diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 770fc1f0c..ec6f467fa 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -272,6 +272,37 @@ def test_add_unitadmin_user_with_unitpersonnel_permission_denied(client): assert invited_user is None +def test_invite_user_expired_not_deleted(client): + """The invite token expires passed 168 hours (7 days) and every night at midnight a cronjob deletes the row in the db + However, if the token is expired and the cronjob hasn't exec yet (the invit is still in the DB), users should be able to send a new invite + That replaces the old one + """ + + # invite a new user + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK + + # Set the expiration date in the DB to +7 days for now + invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user.created_at -= timedelta(hours=168) + db.session.commit() + + # Send the invite again and confirm it works + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK + + invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + assert invited_user + + # -- Add existing users to projects ################################# Add existing users to projects # def test_add_existing_user_without_project(client): """Project required if inviting user to project.""" From b634b40293d1ffd19fa1fb5fd4c8ca4e91d91f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 13:05:20 +0200 Subject: [PATCH 072/133] description of change types --- .github/pull_request_template.md | 43 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1bc390f37..babc97cca 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,32 +1,33 @@ -## Before submitting this PR +## 1. Description / Summary -1. **Description:** _Add a summary of the changes in this PR and the related issue._ -2. **Jira task / GitHub issue:** _Link to the github issue or add the Jira task ID here._ -3. **How to test:** _Add information on how someone could manually test this functionality. As detailed as possible._ -4. **Type of change:** [_Check the relevant boxes in the section below_](#what-type-of-changes-does-the-pr-contain) -5. **Add docstrings and comments to code**, _even if_ you personally think it's obvious. +_Add a summary of the changes in this PR and the related issue._ + +## 2. Jira task / GitHub issue + +_Link to the github issue or add the Jira task ID here._ + +## 3. Type of change + +What _type of change(s)_ does the PR contain? + +**Check the relevant boxes below. For an explanation of the different sections, enter edit mode of this PR description template.** + +- [ ] New feature + - [ ] Breaking: _Why / How? Add info here._ + - [ ] Non-breaking +- [ ] Database change: _Remember the to include a new migration version, **or** explain here why it's not needed._ +- [ ] Bug fix +- [ ] Security Alert fix +- [ ] Documentation +- [ ] Workflow +- [ ] Tests **only** -## What _type of change(s)_ does the PR contain? -- [ ] New feature - - [ ] Breaking: _Please describe the reason for the break and how we can fix it._ - - [ ] Non-breaking -- [ ] Database change - - [ ] Migration _included in PR_ - - [ ] Migration _not needed_ -- [ ] Bug fix - - [ ] Breaking: _Please describe the reason for the break and how we can fix it._ - - [ ] Non-breaking -- [ ] Security Alert fix -- [ ] Documentation -- [ ] Tests **(only)** -- [ ] Workflow - ## Checklist - [Sprintlog](../SPRINTLOG.md) From 1b9c53ffe776e044ad680e0af4c9f757d28d4cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 13:18:39 +0200 Subject: [PATCH 073/133] additional information --- .github/pull_request_template.md | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index babc97cca..b7e5857d1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,30 +19,16 @@ What _type of change(s)_ does the PR contain? - [ ] Bug fix - [ ] Security Alert fix - [ ] Documentation -- [ ] Workflow +- [ ] Workflow - [ ] Tests **only** +## 4. Additional information - - -## Checklist - -- [Sprintlog](../SPRINTLOG.md) - - [ ] Added - - [ ] Not needed (E.g. PR contains _only_ tests) -- Rebase / Update / Merge _from_ base branch (the branch from which the current is forked) - - [ ] Done - - [ ] Not needed -- Blocking PRs - - [ ] Merged - - [ ] No blocking PRs -- PR to `master` branch - - [ ] Yes: Read [the release instructions](https://github.com/ScilifelabDataCentre/dds_web/blob/master/doc/procedures/new_release.md) - - [ ] I have followed steps 1-8. - - [ ] No +- [ ] [Sprintlog](../SPRINTLOG.md): +- [ ] Blocking PRs + - [ ] Merged +- [ ] PR to `master` branch: _If checked, read [the release instructions](../doc/procedures/new_release.md) + - [ ] I have followed steps 1-8. ## Actions / Scans From db72261b475ddce6947bf0fc8a0b8b90543d2ddc Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 14 Sep 2023 13:31:56 +0200 Subject: [PATCH 074/133] sprintolog and clarifications --- SPRINTLOG.md | 1 + tests/api/test_user.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 097a7c0eb..3d6fac718 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -297,3 +297,4 @@ _Nothing merged in CLI during this sprint_ - Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) +- AddUser checks for expired unanswered invites and updated db ([#1466])(https://github.com/ScilifelabDataCentre/dds_web/pull/1466) \ No newline at end of file diff --git a/tests/api/test_user.py b/tests/api/test_user.py index ec6f467fa..56f57ba9e 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -274,7 +274,7 @@ def test_add_unitadmin_user_with_unitpersonnel_permission_denied(client): def test_invite_user_expired_not_deleted(client): """The invite token expires passed 168 hours (7 days) and every night at midnight a cronjob deletes the row in the db - However, if the token is expired and the cronjob hasn't exec yet (the invit is still in the DB), users should be able to send a new invite + However, if the token is expired and the cronjob hasn't exec yet (i.e the invit is still in the DB), users should be able to send a new invite That replaces the old one """ @@ -286,9 +286,10 @@ def test_invite_user_expired_not_deleted(client): ) assert response.status_code == http.HTTPStatus.OK - # Set the expiration date in the DB to +7 days for now + # Set the creation date in the DB to -7 days for now invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() invited_user.created_at -= timedelta(hours=168) + old_time = invited_user.created_at db.session.commit() # Send the invite again and confirm it works @@ -302,6 +303,9 @@ def test_invite_user_expired_not_deleted(client): invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() assert invited_user + # check that the date has been updated + assert not old_time == invited_user.created_at + # -- Add existing users to projects ################################# Add existing users to projects # def test_add_existing_user_without_project(client): From 6bb98bab1ad7f8417237bcc0fa2fd0ccfc18933c Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 14 Sep 2023 13:42:57 +0200 Subject: [PATCH 075/133] prettier --- SPRINTLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 3d6fac718..5585a0092 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -297,4 +297,4 @@ _Nothing merged in CLI during this sprint_ - Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) -- AddUser checks for expired unanswered invites and updated db ([#1466])(https://github.com/ScilifelabDataCentre/dds_web/pull/1466) \ No newline at end of file +- AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) \ No newline at end of file From 4b54cfdc112b1c50ea900d0f866d72e05cc8a7e3 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 14 Sep 2023 13:47:51 +0200 Subject: [PATCH 076/133] prettier --- SPRINTLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 5585a0092..135e14c94 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -297,4 +297,4 @@ _Nothing merged in CLI during this sprint_ - Only return date (not time) from `Statistics` endpoint ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1456)) - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) -- AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) \ No newline at end of file +- AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) From 4f890eba589b1201f2bba4873058dea46d2c9c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 13:49:44 +0200 Subject: [PATCH 077/133] actions and scans --- .github/pull_request_template.md | 73 ++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b7e5857d1..12ffe9898 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,34 +30,45 @@ What _type of change(s)_ does the PR contain? - [ ] PR to `master` branch: _If checked, read [the release instructions](../doc/procedures/new_release.md) - [ ] I have followed steps 1-8. -## Actions / Scans - - - -- **Black**: Python code formatter. Does not execute. Only tests. - Run `black .` locally to execute formatting. - - [ ] Passed -- **Prettier**: General code formatter. Our use case: MD and yaml mainly. - Run `npx prettier --write .` locally to execute formatting. - - [ ] Passed -- **Yamllint**: Linting of yaml files. - - [ ] Passed -- **Tests**: Pytest to verify that functionality works as expected. - - [ ] New tests added - - [ ] No new tests - - [ ] Passed -- **CodeQL**: Scan for security vulnerabilities, bugs, errors - - [ ] New alerts: _Go through them and either fix, dismiss och ignore. Add reasoning in items below._ - - [ ] Alerts fixed: _What?_ - - [ ] Alerts ignored / dismissed: _Why?_ - - [ ] Passed -- **Trivy**: Security scanner - - [ ] New alerts: _Go through them and either fix, dismiss och ignore. Add reasoning in items below._ - - [ ] Alerts fixed: _What?_ - - [ ] Alerts ignored / dismissed: _Why?_ - - [ ] Passed -- **Snyk**: Security scanner - - [ ] New alerts: _Go through them and either fix, dismiss och ignore. Add reasoning in items below._ - - [ ] Alerts fixed: _What?_ - - [ ] Alerts ignored / dismissed: _Why?_ - - [ ] Passed +## 5. Actions / Scans + +_Check the boxes when the specified checks have passed._ + +**For information on what the different checks do and how to fix it if they're failing, enter edit mode of this description or go to the [PR template](../.github/pull_request_template.md).** + +- [ ] **Black** + +- [ ] **Prettier** + +- [ ] **Yamllint** + +- [ ] **Tests** + +- [ ] **CodeQL** + +- [ ] **Trivy** + +- [ ] **Snyk** + From 9ab9e7302939473cadbac2d7bad58d37a6966df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 14:01:29 +0200 Subject: [PATCH 078/133] read this first and prettier --- .github/pull_request_template.md | 68 ++++++++++++++++------------- dds_web/static/js/dds.js | 2 +- dds_web/static/scss/_variables.scss | 15 +------ 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 12ffe9898..456748d93 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,12 @@ -## 1. Description / Summary +## Read this before submitting the PR + +- When creating a new PR, make it a _Draft PR_. +- Go through sections 1-5 below, fill them in and check all the boxes +- **When all information is filled in and all boxes are checked**, mark the PR as _Ready for Review_ and tag reviewers (in the "Reviewers" section in the top right). +- If you're stuck, or any checks fail (e.g. tests) and you need help, you can tag reviewers and mark as ready for review. Inform the reviewers about needing input about the specific issue. +- Once there is a submitted review, implement the suggestions (if reasonable, otherwise discuss) and then request a new review (unless approved) + +## 1. Description / Summary _Add a summary of the changes in this PR and the related issue._ @@ -27,48 +35,46 @@ What _type of change(s)_ does the PR contain? - [ ] [Sprintlog](../SPRINTLOG.md): - [ ] Blocking PRs - [ ] Merged -- [ ] PR to `master` branch: _If checked, read [the release instructions](../doc/procedures/new_release.md) - - [ ] I have followed steps 1-8. +- [ ] PR to `master` branch: \_If checked, read [the release instructions](../doc/procedures/new_release.md) + - [ ] I have followed steps 1-8. ## 5. Actions / Scans -_Check the boxes when the specified checks have passed._ +_Check the boxes when the specified checks have passed._ **For information on what the different checks do and how to fix it if they're failing, enter edit mode of this description or go to the [PR template](../.github/pull_request_template.md).** - [ ] **Black** - -- [ ] **Prettier** +- [ ] **Prettier** + - [ ] **Yamllint** - + - [ ] **Tests** - + - [ ] **CodeQL** + - What: Scan for security vulnerabilities, bugs, errors + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> - [ ] **Trivy** - + - [ ] **Snyk** - + diff --git a/dds_web/static/js/dds.js b/dds_web/static/js/dds.js index 861827669..a9643ca49 100644 --- a/dds_web/static/js/dds.js +++ b/dds_web/static/js/dds.js @@ -18,7 +18,7 @@ } form.classList.add("was-validated"); }, - false, + false ); }); })(); diff --git a/dds_web/static/scss/_variables.scss b/dds_web/static/scss/_variables.scss index 79e4121a8..ac7189afe 100644 --- a/dds_web/static/scss/_variables.scss +++ b/dds_web/static/scss/_variables.scss @@ -28,19 +28,8 @@ $font-family-sans-serif: "Lato", // Everything else is the default Bootstrap 5 sans-serif font stack.. system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - "Helvetica Neue", - Arial, - "Noto Sans", - "Liberation Sans", - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji" !default; + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", + sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; // // Override Bootstrap defaults From 2f427a2b60b257fdecf374333ab3d75af3a350d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 14:09:00 +0200 Subject: [PATCH 079/133] additional point --- .github/pull_request_template.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 456748d93..7f85ef675 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,7 +2,9 @@ - When creating a new PR, make it a _Draft PR_. - Go through sections 1-5 below, fill them in and check all the boxes -- **When all information is filled in and all boxes are checked**, mark the PR as _Ready for Review_ and tag reviewers (in the "Reviewers" section in the top right). + - If there is a field which you are unsure about, enter the edit mode of this description or go to the [PR template](../.github/pull_request_template.md); There are invisible comments providing descriptions which may be of help. +- If you see the "Update Branch" button at the bottom of the PR, you will need to rebase or update (merge the head branch into the current branch) +- **When all information is filled in, all boxes are checked and the branch is updated with the changes from the master/dev branch (depending on which one this branch is forked from**, mark the PR as _Ready for Review_ and tag reviewers (in the "Reviewers" section in the top right). - If you're stuck, or any checks fail (e.g. tests) and you need help, you can tag reviewers and mark as ready for review. Inform the reviewers about needing input about the specific issue. - Once there is a submitted review, implement the suggestions (if reasonable, otherwise discuss) and then request a new review (unless approved) From d63546df39293c4b138e7d901dab7d98694361c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 14:09:18 +0200 Subject: [PATCH 080/133] prettier --- .github/pull_request_template.md | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7f85ef675..29c931e77 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,8 +2,8 @@ - When creating a new PR, make it a _Draft PR_. - Go through sections 1-5 below, fill them in and check all the boxes - - If there is a field which you are unsure about, enter the edit mode of this description or go to the [PR template](../.github/pull_request_template.md); There are invisible comments providing descriptions which may be of help. -- If you see the "Update Branch" button at the bottom of the PR, you will need to rebase or update (merge the head branch into the current branch) + - If there is a field which you are unsure about, enter the edit mode of this description or go to the [PR template](../.github/pull_request_template.md); There are invisible comments providing descriptions which may be of help. +- If you see the "Update Branch" button at the bottom of the PR, you will need to rebase or update (merge the head branch into the current branch) - **When all information is filled in, all boxes are checked and the branch is updated with the changes from the master/dev branch (depending on which one this branch is forked from**, mark the PR as _Ready for Review_ and tag reviewers (in the "Reviewers" section in the top right). - If you're stuck, or any checks fail (e.g. tests) and you need help, you can tag reviewers and mark as ready for review. Inform the reviewers about needing input about the specific issue. - Once there is a submitted review, implement the suggestions (if reasonable, otherwise discuss) and then request a new review (unless approved) @@ -48,35 +48,35 @@ _Check the boxes when the specified checks have passed._ - [ ] **Black** + - What: Python code formatter. + - How to fix: Run `black .` locally to execute formatting. + --> - [ ] **Prettier** - [ ] **Yamllint** + - What: Linting of yaml files. + - How to fix: Manually fix any errors locally. + --> - [ ] **Tests** + - What: Pytest to verify that functionality works as expected. + - How to fix: Manually fix any errors locally. Follow the instructions in the "Run tests" section of the README.md to run the tests locally. + - Additional info: The PR should ALWAYS include new tests or fixed tests when there are code changes. When pytest action has finished, it will post a codecov report; Look at this report and verify the files you have changed are listed. "90% <100.00%> (+0.8%)" means "Tests cover 90% of the changed file, <100 % of this PR's code changes are tested>, and (the code changes and added tests increased the overall test coverage with 0.8%) + --> - [ ] **CodeQL** + - What: Scan for security vulnerabilities, bugs, errors + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> - [ ] **Trivy** + - What: Security scanner + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> - [ ] **Snyk** + - What: Security scanner + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> From 8ea0d808e6e953e3dd06897b1eea757b9ab09ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 14:12:09 +0200 Subject: [PATCH 081/133] prettier --- .github/pull_request_template.md | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 29c931e77..140543058 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -48,35 +48,35 @@ _Check the boxes when the specified checks have passed._ - [ ] **Black** + - What: Python code formatter. + - How to fix: Run `black .` locally to execute formatting. + --> - [ ] **Prettier** - [ ] **Yamllint** + - What: Linting of yaml files. + - How to fix: Manually fix any errors locally. + --> - [ ] **Tests** + - What: Pytest to verify that functionality works as expected. + - How to fix: Manually fix any errors locally. Follow the instructions in the "Run tests" section of the README.md to run the tests locally. + - Additional info: The PR should ALWAYS include new tests or fixed tests when there are code changes. When pytest action has finished, it will post a codecov report; Look at this report and verify the files you have changed are listed. "90% <100.00%> (+0.8%)" means "Tests cover 90% of the changed file, <100 % of this PR's code changes are tested>, and (the code changes and added tests increased the overall test coverage with 0.8%) + --> - [ ] **CodeQL** + - What: Scan for security vulnerabilities, bugs, errors + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> - [ ] **Trivy** + - What: Security scanner + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> - [ ] **Snyk** + - What: Security scanner + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> From cb60d5ccecb4ce94695ee60885282b21a427d59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 14:17:41 +0200 Subject: [PATCH 082/133] reformatting --- .github/pull_request_template.md | 56 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 140543058..f4580918f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -47,36 +47,52 @@ _Check the boxes when the specified checks have passed._ **For information on what the different checks do and how to fix it if they're failing, enter edit mode of this description or go to the [PR template](../.github/pull_request_template.md).** - [ ] **Black** + + - What: Python code formatter. + - How to fix: Run `black .` locally to execute formatting. + --> + - [ ] **Prettier** - + - [ ] **Yamllint** + + - What: Linting of yaml files. + - How to fix: Manually fix any errors locally. + --> + - [ ] **Tests** + + - What: Pytest to verify that functionality works as expected. + - How to fix: Manually fix any errors locally. Follow the instructions in the "Run tests" section of the README.md to run the tests locally. + - Additional info: The PR should ALWAYS include new tests or fixed tests when there are code changes. When pytest action has finished, it will post a codecov report; Look at this report and verify the files you have changed are listed. "90% <100.00%> (+0.8%)" means "Tests cover 90% of the changed file, <100 % of this PR's code changes are tested>, and (the code changes and added tests increased the overall test coverage with 0.8%) + --> + - [ ] **CodeQL** + + - What: Scan for security vulnerabilities, bugs, errors + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> + - [ ] **Trivy** + + - What: Security scanner + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> + - [ ] **Snyk** + + - What: Security scanner + - How to fix: Go through the alerts and either manually fix, dismiss or ignore. Add info on ignored or dismissed alerts. + --> + From 2fc29f91d48e704e30ccb09ed337aa57653d2748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 14:20:56 +0200 Subject: [PATCH 083/133] reformatting --- .github/pull_request_template.md | 57 ++++---------------------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f4580918f..16ea6f39a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -46,53 +46,10 @@ _Check the boxes when the specified checks have passed._ **For information on what the different checks do and how to fix it if they're failing, enter edit mode of this description or go to the [PR template](../.github/pull_request_template.md).** -- [ ] **Black** - - - -- [ ] **Prettier** - - - -- [ ] **Yamllint** - - - -- [ ] **Tests** - - - -- [ ] **CodeQL** - - - -- [ ] **Trivy** - - - -- [ ] **Snyk** - - - +- [ ] **Black** +- [ ] **Prettier** +- [ ] **Yamllint** +- [ ] **Tests** +- [ ] **CodeQL** +- [ ] **Trivy** +- [ ] **Snyk** From 6ee563de2e52c2c5743ebdc73b1d78312109f86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= <35953392+i-oden@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:31:51 +0200 Subject: [PATCH 084/133] Apply suggestions from code review --- dds_web/static/js/dds.js | 2 +- dds_web/static/scss/_variables.scss | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/dds_web/static/js/dds.js b/dds_web/static/js/dds.js index a9643ca49..861827669 100644 --- a/dds_web/static/js/dds.js +++ b/dds_web/static/js/dds.js @@ -18,7 +18,7 @@ } form.classList.add("was-validated"); }, - false + false, ); }); })(); diff --git a/dds_web/static/scss/_variables.scss b/dds_web/static/scss/_variables.scss index ac7189afe..f9ca942fe 100644 --- a/dds_web/static/scss/_variables.scss +++ b/dds_web/static/scss/_variables.scss @@ -28,8 +28,19 @@ $font-family-sans-serif: "Lato", // Everything else is the default Bootstrap 5 sans-serif font stack.. system-ui, - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", - sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", + "Liberation Sans", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji" !default; // // Override Bootstrap defaults From ae08d2cb7d94b3590384afb974244af1b04b1455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= <35953392+i-oden@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:33:28 +0200 Subject: [PATCH 085/133] Apply suggestions from code review --- dds_web/static/scss/_variables.scss | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dds_web/static/scss/_variables.scss b/dds_web/static/scss/_variables.scss index f9ca942fe..79e4121a8 100644 --- a/dds_web/static/scss/_variables.scss +++ b/dds_web/static/scss/_variables.scss @@ -29,17 +29,17 @@ $font-family-sans-serif: // Everything else is the default Bootstrap 5 sans-serif font stack.. system-ui, -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - "Helvetica Neue", - Arial, - "Noto Sans", + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + Arial, + "Noto Sans", "Liberation Sans", - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji" !default; // From 9aa7e9f25c9be984815035d90e646a5e06d12d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 15:05:35 +0200 Subject: [PATCH 086/133] prettier --- .github/pull_request_template.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 16ea6f39a..ff1c76771 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -46,10 +46,19 @@ _Check the boxes when the specified checks have passed._ **For information on what the different checks do and how to fix it if they're failing, enter edit mode of this description or go to the [PR template](../.github/pull_request_template.md).** -- [ ] **Black** -- [ ] **Prettier** -- [ ] **Yamllint** -- [ ] **Tests** -- [ ] **CodeQL** -- [ ] **Trivy** +- [ ] **Black** + +- [ ] **Prettier** + +- [ ] **Yamllint** + +- [ ] **Tests** + +- [ ] **CodeQL** + +- [ ] **Trivy** + - [ ] **Snyk** From de97ca3ab542abbe28bd8d5f32c90b20db53595b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 15:09:08 +0200 Subject: [PATCH 087/133] prettier --- .github/pull_request_template.md | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ff1c76771..7b81f1c25 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -52,13 +52,33 @@ _Check the boxes when the specified checks have passed._ How to fix: Run `black .` locally to execute formatting. --> - [ ] **Prettier** - + - [ ] **Yamllint** - + - [ ] **Tests** - + - [ ] **CodeQL** - + - [ ] **Trivy** - -- [ ] **Snyk** + +- [ ] **Snyk** + From d1915abd962869606fe214f1b29c659d83b0efc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 15:10:09 +0200 Subject: [PATCH 088/133] sprintlog --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 5c48b0561..77700d4e5 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -299,3 +299,4 @@ _Nothing merged in CLI during this sprint_ - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) - Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) +- Workflow: Update PR template and clarify sections ([#1467](https://github.com/ScilifelabDataCentre/dds_web/pull/1467)) From 072be2a6937a36679e177acc8c21979f487653b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 15:12:08 +0200 Subject: [PATCH 089/133] hide before --- .github/pull_request_template.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7b81f1c25..d3dc329b8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,4 @@ + ## 1. Description / Summary _Add a summary of the changes in this PR and the related issue._ From 696378eec468a103089452bcc45c07c8159a1892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 15:18:57 +0200 Subject: [PATCH 090/133] change start --- .github/pull_request_template.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d3dc329b8..f7104f7e8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,14 +1,13 @@ - +1. Always create a Draft PR first +2. Go through sections 1-5 below, fill them in and check all the boxes +3. Make sure that the branch is updated; if there's an "Update branch" button at the bottom of the PR, rebase or update branch. +4. When all boxes are checked, information is filled in, and the branch is updated: mark as Ready For Review and tag reviewers (top right) +5. Once there is a submitted review, implement the suggestions (if reasonable, otherwise discuss) and request an new review. + +If there is a field which you are unsure about, enter the edit mode of this description or go to the [PR template](../.github/pull_request_template.md); There are invisible comments providing descriptions which may be of help. + ## 1. Description / Summary _Add a summary of the changes in this PR and the related issue._ From 67da525a0f6d33b7e01f655efa282f7741fdd9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Thu, 14 Sep 2023 15:20:56 +0200 Subject: [PATCH 091/133] remove : --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f7104f7e8..27db39cc9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -34,7 +34,7 @@ What _type of change(s)_ does the PR contain? ## 4. Additional information -- [ ] [Sprintlog](../SPRINTLOG.md): +- [ ] [Sprintlog](../SPRINTLOG.md) - [ ] Blocking PRs - [ ] Merged - [ ] PR to `master` branch: \_If checked, read [the release instructions](../doc/procedures/new_release.md) From 42c72adb828947713101326c77fa263a91703ac9 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 14 Sep 2023 15:22:43 +0200 Subject: [PATCH 092/133] prettier --- SPRINTLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 344135455..820f587cc 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -299,4 +299,4 @@ _Nothing merged in CLI during this sprint_ - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) - Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) -- AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) \ No newline at end of file +- AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) From 0ca4d8f17aa89cdd9282151c6e006b614b55f2b0 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 15 Sep 2023 11:30:54 +0200 Subject: [PATCH 093/133] moved logic function --- dds_web/api/schemas/user_schemas.py | 13 +++++++++++++ dds_web/api/user.py | 10 ---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/dds_web/api/schemas/user_schemas.py b/dds_web/api/schemas/user_schemas.py index f1f25fc52..47f2ad6d5 100644 --- a/dds_web/api/schemas/user_schemas.py +++ b/dds_web/api/schemas/user_schemas.py @@ -4,6 +4,10 @@ # IMPORTS ################################################################################ IMPORTS # #################################################################################################### +# standar library +import datetime +from datetime import timedelta + # Installed import flask import marshmallow @@ -72,6 +76,15 @@ def return_invite(self, data, **kwargs): # double check if there is no existing user with this email userexists = utils.email_in_db(email=data.get("email")) + # check if the user invite should have expired and be deleted + if (invite): + if ( + datetime.datetime.utcnow() - datetime.timedelta(hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"]) + ) > invite.created_at: + db.session.delete(invite) + db.session.commit() + invite = None + if userexists and invite: raise ddserr.DatabaseError(message="Email exists for user and invite at the same time") diff --git a/dds_web/api/user.py b/dds_web/api/user.py index d0f152a51..aaedce3b9 100644 --- a/dds_web/api/user.py +++ b/dds_web/api/user.py @@ -86,16 +86,6 @@ def post(self): except sqlalchemy.exc.OperationalError as err: raise ddserr.DatabaseError(message=str(err), alt_message="Unexpected database error.") - # check if the user invite should have expired and be deleted - if unanswered_invite: - # If actual time minus 168 hours (7 days) is greater than the time in the invite -> is expired, delete it - if ( - datetime.datetime.utcnow() - datetime.timedelta(hours=168) - ) > unanswered_invite.created_at: - db.session.delete(unanswered_invite) - db.session.commit() - unanswered_invite = None - if existing_user or unanswered_invite: if not project: raise ddserr.DDSArgumentError( From 0a000fabad75d9c696bdd08c13c19cf7714784a5 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 15 Sep 2023 11:32:22 +0200 Subject: [PATCH 094/133] moved logic function --- dds_web/api/schemas/user_schemas.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dds_web/api/schemas/user_schemas.py b/dds_web/api/schemas/user_schemas.py index 47f2ad6d5..60b0fd964 100644 --- a/dds_web/api/schemas/user_schemas.py +++ b/dds_web/api/schemas/user_schemas.py @@ -77,13 +77,16 @@ def return_invite(self, data, **kwargs): userexists = utils.email_in_db(email=data.get("email")) # check if the user invite should have expired and be deleted - if (invite): - if ( - datetime.datetime.utcnow() - datetime.timedelta(hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"]) - ) > invite.created_at: - db.session.delete(invite) - db.session.commit() - invite = None + if invite and ( + ( + datetime.datetime.utcnow() + - datetime.timedelta(hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"]) + ) + > invite.created_at + ): + db.session.delete(invite) + db.session.commit() + invite = None if userexists and invite: raise ddserr.DatabaseError(message="Email exists for user and invite at the same time") From 650ea67083e63cfab577f5beba1fb6eaa2ea7a0c Mon Sep 17 00:00:00 2001 From: rv0lt Date: Fri, 15 Sep 2023 11:35:16 +0200 Subject: [PATCH 095/133] new catching exception --- dds_web/api/user.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dds_web/api/user.py b/dds_web/api/user.py index aaedce3b9..b4dc6868d 100644 --- a/dds_web/api/user.py +++ b/dds_web/api/user.py @@ -83,8 +83,12 @@ def post(self): try: existing_user = user_schemas.UserSchema().load({"email": email}) unanswered_invite = user_schemas.UnansweredInvite().load({"email": email}) - except sqlalchemy.exc.OperationalError as err: - raise ddserr.DatabaseError(message=str(err), alt_message="Unexpected database error.") + except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: + raise ddserr.DatabaseError( + message=str(err), + alt_message="Something happened while checking for existig account / active invite.", + pass_message=False, + ) if existing_user or unanswered_invite: if not project: From 3e45f40b4911d507b238ecd30dd0ccc576689110 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 12:03:54 +0200 Subject: [PATCH 096/133] added test for adding user to existing project --- tests/api/test_user.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 56f57ba9e..f97d778f5 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -307,6 +307,53 @@ def test_invite_user_expired_not_deleted(client): assert not old_time == invited_user.created_at +def test_invite_user_existing_project_invite_expired(client): + """Same test as above but user is invited to existing project""" + project_id = "public_project_id" + + # invite a new user + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + query_string={"project": project_id}, + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK + + # Set the creation date in the DB to -7 days for now + invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user.created_at -= timedelta(hours=168) + old_time = invited_user.created_at + db.session.commit() + + # check row was added to project invite keys table + project_invite_keys = models.ProjectInviteKeys.query.filter_by( + invite_id=invited_user.id, project_id=project_id + ).one_or_none() + assert project_invite_keys + + # Send the invite again and confirm it works + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + query_string={"project": project_id}, + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK + + invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + assert invited_user + + # check that the date has been updated + assert not old_time == invited_user.created_at + + # check that the project invite keys as a new row + project_invite_keys_new = models.ProjectInviteKeys.query.filter_by( + invite_id=invited_user.id, project_id=project_id + ).one_or_none() + assert not project_invite_keys == project_invite_keys_new + + # -- Add existing users to projects ################################# Add existing users to projects # def test_add_existing_user_without_project(client): """Project required if inviting user to project.""" From 5428955240c0d5afa618fe76946aaf44e5e9a34f Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 12:10:10 +0200 Subject: [PATCH 097/133] prettier --- SPRINTLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 80b272871..e8bf51863 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -300,4 +300,4 @@ _Nothing merged in CLI during this sprint_ - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) - Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) - AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) -- Workflow: Update PR template and clarify sections ([#1467](https://github.com/ScilifelabDataCentre/dds_web/pull/1467)) \ No newline at end of file +- Workflow: Update PR template and clarify sections ([#1467](https://github.com/ScilifelabDataCentre/dds_web/pull/1467)) From 248c2e9f710d0a49153e6c607c64ac5002afdbe1 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 12:50:49 +0200 Subject: [PATCH 098/133] fix order --- tests/api/test_user.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index f97d778f5..5af3c5903 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -309,6 +309,7 @@ def test_invite_user_expired_not_deleted(client): def test_invite_user_existing_project_invite_expired(client): """Same test as above but user is invited to existing project""" + project_id = "public_project_id" # invite a new user @@ -320,11 +321,9 @@ def test_invite_user_existing_project_invite_expired(client): ) assert response.status_code == http.HTTPStatus.OK - # Set the creation date in the DB to -7 days for now + invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() - invited_user.created_at -= timedelta(hours=168) - old_time = invited_user.created_at - db.session.commit() + assert invited_user # check row was added to project invite keys table project_invite_keys = models.ProjectInviteKeys.query.filter_by( @@ -332,6 +331,11 @@ def test_invite_user_existing_project_invite_expired(client): ).one_or_none() assert project_invite_keys + # Set the creation date in the DB to -7 days for now + invited_user.created_at -= timedelta(hours=168) + old_time = invited_user.created_at + db.session.commit() + # Send the invite again and confirm it works response = client.post( tests.DDSEndpoint.USER_ADD, From 483d16311494188b55fe1af76e81d9982de3d936 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 12:51:08 +0200 Subject: [PATCH 099/133] fix order --- tests/api/test_user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 5af3c5903..b7a30d8b6 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -309,7 +309,7 @@ def test_invite_user_expired_not_deleted(client): def test_invite_user_existing_project_invite_expired(client): """Same test as above but user is invited to existing project""" - + project_id = "public_project_id" # invite a new user @@ -321,7 +321,6 @@ def test_invite_user_existing_project_invite_expired(client): ) assert response.status_code == http.HTTPStatus.OK - invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() assert invited_user From 81981a9e6a6d679ef02a667aeab32245eeadd2fe Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 13:20:32 +0200 Subject: [PATCH 100/133] fixed test --- tests/api/test_user.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index b7a30d8b6..edcb08c4a 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -310,13 +310,13 @@ def test_invite_user_expired_not_deleted(client): def test_invite_user_existing_project_invite_expired(client): """Same test as above but user is invited to existing project""" - project_id = "public_project_id" + project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() # invite a new user response = client.post( tests.DDSEndpoint.USER_ADD, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - query_string={"project": project_id}, + query_string={"project": project.public_id}, json=first_new_user, ) assert response.status_code == http.HTTPStatus.OK @@ -326,7 +326,7 @@ def test_invite_user_existing_project_invite_expired(client): # check row was added to project invite keys table project_invite_keys = models.ProjectInviteKeys.query.filter_by( - invite_id=invited_user.id, project_id=project_id + invite_id=invited_user.id, project_id=project.id ).one_or_none() assert project_invite_keys @@ -339,7 +339,7 @@ def test_invite_user_existing_project_invite_expired(client): response = client.post( tests.DDSEndpoint.USER_ADD, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - query_string={"project": project_id}, + query_string={"project": project.public_id}, json=first_new_user, ) assert response.status_code == http.HTTPStatus.OK @@ -352,7 +352,7 @@ def test_invite_user_existing_project_invite_expired(client): # check that the project invite keys as a new row project_invite_keys_new = models.ProjectInviteKeys.query.filter_by( - invite_id=invited_user.id, project_id=project_id + invite_id=invited_user.id, project_id=project.id ).one_or_none() assert not project_invite_keys == project_invite_keys_new From 3dab9bbb0b6da3cf1f4ee5c9d68744c30404260b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:52:11 +0200 Subject: [PATCH 101/133] Update dds_web/api/schemas/user_schemas.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- dds_web/api/schemas/user_schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dds_web/api/schemas/user_schemas.py b/dds_web/api/schemas/user_schemas.py index 60b0fd964..b5a1b0cbc 100644 --- a/dds_web/api/schemas/user_schemas.py +++ b/dds_web/api/schemas/user_schemas.py @@ -6,7 +6,6 @@ # standar library import datetime -from datetime import timedelta # Installed import flask From 9bd1c2e8ae0e45f1c2b9c2489c420910bc8ef29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:52:56 +0200 Subject: [PATCH 102/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index edcb08c4a..06514b5a3 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -287,7 +287,7 @@ def test_invite_user_expired_not_deleted(client): assert response.status_code == http.HTTPStatus.OK # Set the creation date in the DB to -7 days for now - invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() invited_user.created_at -= timedelta(hours=168) old_time = invited_user.created_at db.session.commit() From ebb026518edd3c76167af44d49008578f43a084d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:53:14 +0200 Subject: [PATCH 103/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 06514b5a3..c0201d723 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -321,7 +321,7 @@ def test_invite_user_existing_project_invite_expired(client): ) assert response.status_code == http.HTTPStatus.OK - invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() assert invited_user # check row was added to project invite keys table From cbef679c3d8f14e77bb618e22281fb4de0ab04a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:53:23 +0200 Subject: [PATCH 104/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index c0201d723..2f654d5b6 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -344,7 +344,7 @@ def test_invite_user_existing_project_invite_expired(client): ) assert response.status_code == http.HTTPStatus.OK - invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() assert invited_user # check that the date has been updated From 4f33547936955233f461a2691b233e9f575be86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:53:36 +0200 Subject: [PATCH 105/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 2f654d5b6..0538671aa 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -354,7 +354,7 @@ def test_invite_user_existing_project_invite_expired(client): project_invite_keys_new = models.ProjectInviteKeys.query.filter_by( invite_id=invited_user.id, project_id=project.id ).one_or_none() - assert not project_invite_keys == project_invite_keys_new + assert project_invite_keys_new != project_invite_keys # -- Add existing users to projects ################################# Add existing users to projects # From 56073f073cf48c29c937720efc93f718c8cde2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:53:47 +0200 Subject: [PATCH 106/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 0538671aa..e1698979b 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -304,7 +304,7 @@ def test_invite_user_expired_not_deleted(client): assert invited_user # check that the date has been updated - assert not old_time == invited_user.created_at + assert invited_user.created_at != old_time def test_invite_user_existing_project_invite_expired(client): From 2a22c084b5f82fba86f2462052182c9eec26bac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:53:57 +0200 Subject: [PATCH 107/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index e1698979b..c10d84621 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -348,7 +348,7 @@ def test_invite_user_existing_project_invite_expired(client): assert invited_user # check that the date has been updated - assert not old_time == invited_user.created_at + assert invited_user.created_at != old_time # check that the project invite keys as a new row project_invite_keys_new = models.ProjectInviteKeys.query.filter_by( From effcf046191e705a4972ccb152690f541bbdc0ab Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 15:08:16 +0200 Subject: [PATCH 108/133] added comments for the tests --- tests/api/test_user.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index c10d84621..d2c8ce800 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -273,10 +273,7 @@ def test_add_unitadmin_user_with_unitpersonnel_permission_denied(client): def test_invite_user_expired_not_deleted(client): - """The invite token expires passed 168 hours (7 days) and every night at midnight a cronjob deletes the row in the db - However, if the token is expired and the cronjob hasn't exec yet (i.e the invit is still in the DB), users should be able to send a new invite - That replaces the old one - """ + """If an invite has expired and hasn't been removed to the database the invite should be replaced""" # invite a new user response = client.post( @@ -293,12 +290,15 @@ def test_invite_user_expired_not_deleted(client): db.session.commit() # Send the invite again and confirm it works - response = client.post( - tests.DDSEndpoint.USER_ADD, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=first_new_user, - ) - assert response.status_code == http.HTTPStatus.OK + from tests.api.test_project import mock_sqlalchemyerror + + with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() assert invited_user @@ -308,7 +308,7 @@ def test_invite_user_expired_not_deleted(client): def test_invite_user_existing_project_invite_expired(client): - """Same test as above but user is invited to existing project""" + """If an invite to a project has expired and hasn't been removed. A new invite should replace the old one""" project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() @@ -336,13 +336,16 @@ def test_invite_user_existing_project_invite_expired(client): db.session.commit() # Send the invite again and confirm it works - response = client.post( - tests.DDSEndpoint.USER_ADD, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - query_string={"project": project.public_id}, - json=first_new_user, - ) - assert response.status_code == http.HTTPStatus.OK + from tests.api.test_project import mock_sqlalchemyerror + + with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + query_string={"project": project.public_id}, + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() assert invited_user @@ -350,7 +353,7 @@ def test_invite_user_existing_project_invite_expired(client): # check that the date has been updated assert invited_user.created_at != old_time - # check that the project invite keys as a new row + # check that the project invite keys has a new row project_invite_keys_new = models.ProjectInviteKeys.query.filter_by( invite_id=invited_user.id, project_id=project.id ).one_or_none() From 5a2c27b90b206c95b6149d918c5b4439af062757 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Mon, 18 Sep 2023 15:46:53 +0200 Subject: [PATCH 109/133] modified assertions --- tests/api/test_user.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index d2c8ce800..e2ac5f125 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -292,13 +292,21 @@ def test_invite_user_expired_not_deleted(client): # Send the invite again and confirm it works from tests.api.test_project import mock_sqlalchemyerror + # simulate database error with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): response = client.post( tests.DDSEndpoint.USER_ADD, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), json=first_new_user, ) - assert response.status_code == http.HTTPStatus.OK + assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR + + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() assert invited_user @@ -338,6 +346,7 @@ def test_invite_user_existing_project_invite_expired(client): # Send the invite again and confirm it works from tests.api.test_project import mock_sqlalchemyerror + # simulate database error with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): response = client.post( tests.DDSEndpoint.USER_ADD, @@ -345,7 +354,16 @@ def test_invite_user_existing_project_invite_expired(client): query_string={"project": project.public_id}, json=first_new_user, ) - assert response.status_code == http.HTTPStatus.OK + assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR + + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + query_string={"project": project.public_id}, + json=first_new_user, + ) + + assert response.status_code == http.HTTPStatus.OK invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() assert invited_user From 8731bffc755f31e84dab96cf3791691030fe2d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:56:11 +0200 Subject: [PATCH 110/133] Update dds_web/api/user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- dds_web/api/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dds_web/api/user.py b/dds_web/api/user.py index b4dc6868d..b0800c20b 100644 --- a/dds_web/api/user.py +++ b/dds_web/api/user.py @@ -84,6 +84,7 @@ def post(self): existing_user = user_schemas.UserSchema().load({"email": email}) unanswered_invite = user_schemas.UnansweredInvite().load({"email": email}) except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: + db.session.rollback() raise ddserr.DatabaseError( message=str(err), alt_message="Something happened while checking for existig account / active invite.", From 8f463f6f132a2eeb81254e964b24411acb63f3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:56:34 +0200 Subject: [PATCH 111/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index e2ac5f125..81e07d709 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -346,15 +346,6 @@ def test_invite_user_existing_project_invite_expired(client): # Send the invite again and confirm it works from tests.api.test_project import mock_sqlalchemyerror - # simulate database error - with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): - response = client.post( - tests.DDSEndpoint.USER_ADD, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - query_string={"project": project.public_id}, - json=first_new_user, - ) - assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR response = client.post( tests.DDSEndpoint.USER_ADD, From d77ea8d6297b199287adf0226f27ddff31cdffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:56:45 +0200 Subject: [PATCH 112/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 81e07d709..9ccbfa380 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -292,14 +292,6 @@ def test_invite_user_expired_not_deleted(client): # Send the invite again and confirm it works from tests.api.test_project import mock_sqlalchemyerror - # simulate database error - with unittest.mock.patch("dds_web.db.session.add", mock_sqlalchemyerror): - response = client.post( - tests.DDSEndpoint.USER_ADD, - headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), - json=first_new_user, - ) - assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR response = client.post( tests.DDSEndpoint.USER_ADD, From ce928cd5b16d9d948bbdb9dc7644c47a16250425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:57:14 +0200 Subject: [PATCH 113/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 9ccbfa380..e8354f76d 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -360,7 +360,43 @@ def test_invite_user_existing_project_invite_expired(client): ).one_or_none() assert project_invite_keys_new != project_invite_keys +def test_invite_user_expired_sqlalchemyerror(client): + """Error message should be returned if sqlalchemyerror occurs during deletion of unanswered invite.""" + # Invite a new user + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.OK + + # Set the creation date in the DB to -7 days for now + invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() + invited_user.created_at -= timedelta(hours=168) + old_time = invited_user.created_at + old_id = invited_user.id + db.session.commit() + + from tests.api.test_project import mock_sqlalchemyerror + + # Simulate database error while trying to send new invite + with unittest.mock.patch("dds_web.db.session.delete", mock_sqlalchemyerror): + response = client.post( + tests.DDSEndpoint.USER_ADD, + headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), + json=first_new_user, + ) + assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR + assert response.json.get("message") == "Something happened while checking for existig account / active invite." + + # Get invite again + invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + assert invited_user + + # The invite should be the same + assert invited_user.created_at == old_time + assert invited_user.id == old_id # -- Add existing users to projects ################################# Add existing users to projects # def test_add_existing_user_without_project(client): """Project required if inviting user to project.""" From b881853dbae0c827d51886fde4ed49bb2c2bff25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:57:24 +0200 Subject: [PATCH 114/133] Update SPRINTLOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- SPRINTLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index e8bf51863..1e60d6a82 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -299,5 +299,5 @@ _Nothing merged in CLI during this sprint_ - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) - Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) -- AddUser checks for expired unanswered invites and updated db ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) +- Replace expired invites when there's a new invitation attempt ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) - Workflow: Update PR template and clarify sections ([#1467](https://github.com/ScilifelabDataCentre/dds_web/pull/1467)) From ca973c08d511a7df0ca389506a4be5118d492b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:00:03 +0200 Subject: [PATCH 115/133] Update test_user.py --- tests/api/test_user.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index e8354f76d..f17e1ae28 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -290,9 +290,6 @@ def test_invite_user_expired_not_deleted(client): db.session.commit() # Send the invite again and confirm it works - from tests.api.test_project import mock_sqlalchemyerror - - response = client.post( tests.DDSEndpoint.USER_ADD, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), @@ -336,9 +333,6 @@ def test_invite_user_existing_project_invite_expired(client): db.session.commit() # Send the invite again and confirm it works - from tests.api.test_project import mock_sqlalchemyerror - - response = client.post( tests.DDSEndpoint.USER_ADD, headers=tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client), @@ -397,6 +391,7 @@ def test_invite_user_expired_sqlalchemyerror(client): # The invite should be the same assert invited_user.created_at == old_time assert invited_user.id == old_id + # -- Add existing users to projects ################################# Add existing users to projects # def test_add_existing_user_without_project(client): """Project required if inviting user to project.""" From 3d220b4a1511a70187678e28349d4a21a746d4ae Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 19 Sep 2023 10:01:08 +0200 Subject: [PATCH 116/133] black --- tests/api/test_user.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index f17e1ae28..3dc83b5fa 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -354,6 +354,7 @@ def test_invite_user_existing_project_invite_expired(client): ).one_or_none() assert project_invite_keys_new != project_invite_keys + def test_invite_user_expired_sqlalchemyerror(client): """Error message should be returned if sqlalchemyerror occurs during deletion of unanswered invite.""" @@ -374,7 +375,7 @@ def test_invite_user_expired_sqlalchemyerror(client): from tests.api.test_project import mock_sqlalchemyerror - # Simulate database error while trying to send new invite + # Simulate database error while trying to send new invite with unittest.mock.patch("dds_web.db.session.delete", mock_sqlalchemyerror): response = client.post( tests.DDSEndpoint.USER_ADD, @@ -382,7 +383,10 @@ def test_invite_user_expired_sqlalchemyerror(client): json=first_new_user, ) assert response.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR - assert response.json.get("message") == "Something happened while checking for existig account / active invite." + assert ( + response.json.get("message") + == "Something happened while checking for existig account / active invite." + ) # Get invite again invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() @@ -391,7 +395,8 @@ def test_invite_user_expired_sqlalchemyerror(client): # The invite should be the same assert invited_user.created_at == old_time assert invited_user.id == old_id - + + # -- Add existing users to projects ################################# Add existing users to projects # def test_add_existing_user_without_project(client): """Project required if inviting user to project.""" From 9f4c7f6fd64d04e7e4d3ea4e9ef99b5f3809d63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:46:26 +0200 Subject: [PATCH 117/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 3dc83b5fa..5d210eac1 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -297,7 +297,7 @@ def test_invite_user_expired_not_deleted(client): ) assert response.status_code == http.HTTPStatus.OK - invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() assert invited_user # check that the date has been updated From e373b6154f9d1452ab06fb4a449b573d61457147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:46:34 +0200 Subject: [PATCH 118/133] Update tests/api/test_user.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ina Odén Österbo <35953392+i-oden@users.noreply.github.com> --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 5d210eac1..7da585d46 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -389,7 +389,7 @@ def test_invite_user_expired_sqlalchemyerror(client): ) # Get invite again - invited_user = models.Invite.query.filter_by(email=first_new_email["email"]).one_or_none() + invited_user = models.Invite.query.filter_by(email=first_new_user["email"]).one_or_none() assert invited_user # The invite should be the same From 10fe025782a36f9f45218da44472b239ebe10032 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 19 Sep 2023 14:00:48 +0200 Subject: [PATCH 119/133] added start time --- dds_web/commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dds_web/commands.py b/dds_web/commands.py index a69c45e2b..6fff6efa3 100644 --- a/dds_web/commands.py +++ b/dds_web/commands.py @@ -106,7 +106,8 @@ def create_new_unit( https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html """ from dds_web.database import models - + from dds_web.utils import current_time + error_message = "" if len(public_id) > 50: error_message = "The 'public_id' can be a maximum of 50 characters" @@ -139,6 +140,7 @@ def create_new_unit( days_in_expired=days_in_expired, quota=quota, warning_level=warn_at, + sto4_start_time = current_time() ) db.session.add(new_unit) db.session.commit() From dffb18aa0e23eef0ceb113ec6d6d6596ac6f080a Mon Sep 17 00:00:00 2001 From: rv0lt Date: Tue, 19 Sep 2023 14:05:55 +0200 Subject: [PATCH 120/133] sprintlog --- SPRINTLOG.md | 1 + dds_web/commands.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 77700d4e5..0189c2dbe 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -300,3 +300,4 @@ _Nothing merged in CLI during this sprint_ - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) - Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) - Workflow: Update PR template and clarify sections ([#1467](https://github.com/ScilifelabDataCentre/dds_web/pull/1467)) +- Column `sto4_start_time` is automatically set when the create-unit command is run ([#1668])(https://scilifelab.atlassian.net/jira/software/projects/DDS/boards/13?selectedIssue=DDS-1668) diff --git a/dds_web/commands.py b/dds_web/commands.py index 6fff6efa3..4a1e34ba4 100644 --- a/dds_web/commands.py +++ b/dds_web/commands.py @@ -107,7 +107,7 @@ def create_new_unit( """ from dds_web.database import models from dds_web.utils import current_time - + error_message = "" if len(public_id) > 50: error_message = "The 'public_id' can be a maximum of 50 characters" @@ -140,7 +140,7 @@ def create_new_unit( days_in_expired=days_in_expired, quota=quota, warning_level=warn_at, - sto4_start_time = current_time() + sto4_start_time=current_time(), ) db.session.add(new_unit) db.session.commit() From 377b954887bbc441f1b8bf3ca70bb55cf8eeb87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:27:51 +0200 Subject: [PATCH 121/133] Update tests/api/test_user.py Co-authored-by: Valentin Georgiev --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 7da585d46..9942b9267 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -273,7 +273,7 @@ def test_add_unitadmin_user_with_unitpersonnel_permission_denied(client): def test_invite_user_expired_not_deleted(client): - """If an invite has expired and hasn't been removed to the database the invite should be replaced""" + """If an invite has expired and hasn't been removed from the database, the invite should be replaced""" # invite a new user response = client.post( From 753ead325ec02fbf79d4921d7dcc822df13a6646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Revuelta?= <46089290+rv0lt@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:28:01 +0200 Subject: [PATCH 122/133] Update tests/api/test_user.py Co-authored-by: Valentin Georgiev --- tests/api/test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 9942b9267..3473a96eb 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -305,7 +305,7 @@ def test_invite_user_expired_not_deleted(client): def test_invite_user_existing_project_invite_expired(client): - """If an invite to a project has expired and hasn't been removed. A new invite should replace the old one""" + """If an invite to a project has expired and hasn't been removed, a new invite should replace the old one""" project = models.Project.query.filter_by(public_id="public_project_id").one_or_none() From cd81f8aa17f2a4a5fab6ac02b88cc46434f0df58 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 21 Sep 2023 13:58:29 +0200 Subject: [PATCH 123/133] feedback --- dds_web/commands.py | 2 +- tests/test_commands.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dds_web/commands.py b/dds_web/commands.py index 4a1e34ba4..08bc8f84b 100644 --- a/dds_web/commands.py +++ b/dds_web/commands.py @@ -132,6 +132,7 @@ def create_new_unit( external_display_name=external_display_name, contact_email=contact_email, internal_ref=internal_ref or public_id, + sto4_start_time=current_time(), sto4_endpoint=safespring_endpoint, sto4_name=safespring_name, sto4_access=safespring_access, @@ -140,7 +141,6 @@ def create_new_unit( days_in_expired=days_in_expired, quota=quota, warning_level=warn_at, - sto4_start_time=current_time(), ) db.session.add(new_unit) db.session.commit() diff --git a/tests/test_commands.py b/tests/test_commands.py index 734d8b2f8..4b479b760 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -252,6 +252,9 @@ def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> No _, err = capfd.readouterr() assert f"Unit '{correct_unit['name']}' created" in err + new_unit = db.session.query(models.Unit).filter(models.Unit.name == correct_unit["name"]).one_or_none() + + assert new_unit.sto4_start_time # update_unit From 2f652c5f8c5337a6a9e640d27d6b26f1c1223c99 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 21 Sep 2023 14:01:13 +0200 Subject: [PATCH 124/133] black --- dds_web/commands.py | 2 +- tests/test_commands.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dds_web/commands.py b/dds_web/commands.py index 08bc8f84b..c9d925cb3 100644 --- a/dds_web/commands.py +++ b/dds_web/commands.py @@ -132,7 +132,7 @@ def create_new_unit( external_display_name=external_display_name, contact_email=contact_email, internal_ref=internal_ref or public_id, - sto4_start_time=current_time(), + sto4_start_time=current_time(), sto4_endpoint=safespring_endpoint, sto4_name=safespring_name, sto4_access=safespring_access, diff --git a/tests/test_commands.py b/tests/test_commands.py index 4b479b760..e44b57e68 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -252,10 +252,13 @@ def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> No _, err = capfd.readouterr() assert f"Unit '{correct_unit['name']}' created" in err - new_unit = db.session.query(models.Unit).filter(models.Unit.name == correct_unit["name"]).one_or_none() + new_unit = ( + db.session.query(models.Unit).filter(models.Unit.name == correct_unit["name"]).one_or_none() + ) assert new_unit.sto4_start_time + # update_unit From da5f521ebee1b0e0d6ae2eabf48ed883f377341c Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 21 Sep 2023 14:43:07 +0200 Subject: [PATCH 125/133] more test conditions --- tests/test_commands.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index e44b57e68..4f82dd869 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -256,7 +256,21 @@ def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> No db.session.query(models.Unit).filter(models.Unit.name == correct_unit["name"]).one_or_none() ) + # Check that the different attributes have been set up + + assert new_unit.public_id == correct_unit["public_id"] + assert new_unit.external_display_name == correct_unit["external_display_name"] + assert new_unit.contact_email == correct_unit["contact_email"] + assert new_unit.internal_ref assert new_unit.sto4_start_time + assert new_unit.sto4_endpoint == correct_unit["safespring_endpoint"] + assert new_unit.sto4_name == correct_unit["safespring_name"] + assert new_unit.sto4_access == correct_unit["safespring_access"] + assert new_unit.sto4_secret == correct_unit["safespring_secret"] + assert new_unit.days_in_available + assert new_unit.days_in_expired + assert new_unit.quota == correct_unit["quota"] + assert new_unit.warning_level # update_unit From 1bc6d10d90e1d6d268e86f34d92605ddc6f62b0d Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 21 Sep 2023 15:19:52 +0200 Subject: [PATCH 126/133] sto2 endpoints --- tests/test_commands.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index 4f82dd869..1cd5310f1 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -272,6 +272,11 @@ def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> No assert new_unit.quota == correct_unit["quota"] assert new_unit.warning_level + # check for the atributes that should not be setted up + assert not new_unit.sto2_endpoint + assert not new_unit.sto2_name + assert not new_unit.sto2_access + assert not new_unit.sto2_secret # update_unit From d1fa7f25e4f33af2a60f72d40d398b4cfaff2b4f Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 21 Sep 2023 15:23:34 +0200 Subject: [PATCH 127/133] black --- tests/test_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index 1cd5310f1..4985b5041 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -278,6 +278,7 @@ def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> No assert not new_unit.sto2_access assert not new_unit.sto2_secret + # update_unit From ed04fabd37a354207d5e0b5993e545213daa36f7 Mon Sep 17 00:00:00 2001 From: rv0lt Date: Thu, 21 Sep 2023 15:27:36 +0200 Subject: [PATCH 128/133] removed unncesary checks --- tests/test_commands.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 4985b5041..4f82dd869 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -272,12 +272,6 @@ def test_create_new_unit_success(client, runner, capfd: LogCaptureFixture) -> No assert new_unit.quota == correct_unit["quota"] assert new_unit.warning_level - # check for the atributes that should not be setted up - assert not new_unit.sto2_endpoint - assert not new_unit.sto2_name - assert not new_unit.sto2_access - assert not new_unit.sto2_secret - # update_unit From 01a07e6da88fbe8995b50d3575c6adfe00baa32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Fri, 22 Sep 2023 11:56:39 +0200 Subject: [PATCH 129/133] changelog --- CHANGELOG.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1561b1288..adb37fe23 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,24 @@ Changelog ========== +.. _2.5.1: + +2.5.1 - 2023-09-27 +~~~~~~~~~~~~~~~~~~~ + +- Super Admins only: + - New endpoint `MaintenanceMode.get`: Super Admins can get info on whether or not the DDS maintenance mode is on or off. + - Statistics endpoint returns date of generated statistics, not time. +- Bugs fixed: + - Errors when attempting to create a project after it has failed due to a database error should now not happen; Database rollback added to project creation endpoint. + - Researchers should now always appear in the list of project users after running `dds project access fix --project `; Missing database update added. + - Expired invites are deleted automatically when invite is sent to user again; Deleting invite with `dds user delete --is-invite` is no longer necessary prior to a new `dds user add`. +- Dependencies: + - `MariaDB` from EOL `10.7.8`` to LTS `10.11.5` + .. _2.5.0: -version 2.5.0 - 2023-08-30 +2.5.0 - 2023-08-30 ~~~~~~~~~~~~~~~~~~~~~~~~ - Dependencies: From ecc7204b64bd8c6877f255964cd8a7bd8f2ae4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Fri, 22 Sep 2023 11:57:57 +0200 Subject: [PATCH 130/133] version --- dds_web/version.py | 2 +- tests/test_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dds_web/version.py b/dds_web/version.py index 50062f87c..7a2056f56 100644 --- a/dds_web/version.py +++ b/dds_web/version.py @@ -1 +1 @@ -__version__ = "2.5.0" +__version__ = "2.5.1" diff --git a/tests/test_version.py b/tests/test_version.py index 6e9338b1b..02a5bc16a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,4 +2,4 @@ def test_version(): - assert version.__version__ == "2.5.0" + assert version.__version__ == "2.5.1" From 48b510a2dfa4a84face4448bb0d24153ba74bb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Fri, 22 Sep 2023 12:08:18 +0200 Subject: [PATCH 131/133] update sprintlog --- SPRINTLOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index d19f88744..50f43113c 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -299,6 +299,9 @@ _Nothing merged in CLI during this sprint_ - Set `sto2*` columns in `Unit` table to nullable ([#1456](https://github.com/ScilifelabDataCentre/dds_web/pull/1462)) - Dependency: Bump `MariaDB` to LTS version 10.11.5 ([#1465](https://github.com/ScilifelabDataCentre/dds_web/pull/1465)) - Bug fixed: Row in `ProjectUsers` should also be added if it doesn't exist when giving Researcher access to a specific project ([#1464](https://github.com/ScilifelabDataCentre/dds_web/pull/1464)) -- Replace expired invites when there's a new invitation attempt ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) - Workflow: Update PR template and clarify sections ([#1467](https://github.com/ScilifelabDataCentre/dds_web/pull/1467)) + +# 2023-09-18 - 2023-09-29 + - Column `sto4_start_time` is automatically set when the create-unit command is run ([#1668])(https://scilifelab.atlassian.net/jira/software/projects/DDS/boards/13?selectedIssue=DDS-1668) +- Replace expired invites when there's a new invitation attempt ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) From 767f65aa9c75306ea5c3d0f64e684b31ae395623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= Date: Fri, 22 Sep 2023 12:09:12 +0200 Subject: [PATCH 132/133] sprintlog --- SPRINTLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 50f43113c..0844d56b3 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -305,3 +305,4 @@ _Nothing merged in CLI during this sprint_ - Column `sto4_start_time` is automatically set when the create-unit command is run ([#1668])(https://scilifelab.atlassian.net/jira/software/projects/DDS/boards/13?selectedIssue=DDS-1668) - Replace expired invites when there's a new invitation attempt ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466)) +- New version: 2.5.1 ([#1471](https://github.com/ScilifelabDataCentre/dds_web/pull/1471)) From 5a8f956fa9d067a3e42bedc90fe6deecc851b05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ina=20Od=C3=A9n=20=C3=96sterbo?= <35953392+i-oden@users.noreply.github.com> Date: Mon, 25 Sep 2023 10:36:10 +0200 Subject: [PATCH 133/133] Update CHANGELOG.rst --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index adb37fe23..d9b86cf5f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ Changelog - Researchers should now always appear in the list of project users after running `dds project access fix --project `; Missing database update added. - Expired invites are deleted automatically when invite is sent to user again; Deleting invite with `dds user delete --is-invite` is no longer necessary prior to a new `dds user add`. - Dependencies: - - `MariaDB` from EOL `10.7.8`` to LTS `10.11.5` + - `MariaDB` from EOL `10.7.8` to LTS `10.11.5` .. _2.5.0: @@ -77,4 +77,4 @@ Changelog Earlier versions ~~~~~~~~~~~~~~~~~ -Please see `the release page on GitHub `_ for detailed information about the changes in each release. \ No newline at end of file +Please see `the release page on GitHub `_ for detailed information about the changes in each release.