diff --git a/.github/workflows/publish_and_trivyscan.yml b/.github/workflows/publish_and_trivyscan.yml index 7bd44a6c8..f250b7fba 100644 --- a/.github/workflows/publish_and_trivyscan.yml +++ b/.github/workflows/publish_and_trivyscan.yml @@ -96,12 +96,12 @@ jobs: uses: actions/download-artifact@v4 with: name: technical-overview-pdf - path: dds_web/static/dds-technical-overview.pdf + path: dds_web/static - name: Download troubleshooting PDF uses: actions/download-artifact@v4 with: name: troubleshooting-pdf - path: dds_web/static/dds-troubleshooting.pdf + path: dds_web/static - name: Docker metadata id: meta uses: docker/metadata-action@v5 @@ -111,14 +111,16 @@ jobs: - name: Ensure lowercase name run: echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - name: Build for scan - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: file: Dockerfiles/backend.Dockerfile context: . push: false tags: ghcr.io/${{ env.IMAGE_REPOSITORY }}:sha-${{ github.sha }} - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.7.1 + uses: aquasecurity/trivy-action@0.26.0 + env: + TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db with: image-ref: "ghcr.io/${{ env.IMAGE_REPOSITORY }}:sha-${{ github.sha }}" format: "sarif" @@ -130,7 +132,7 @@ jobs: sarif_file: "trivy-results.sarif" category: trivy-build - name: Publish image - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: file: Dockerfiles/backend.Dockerfile context: . diff --git a/.github/workflows/trivy-scheduled-dev.yml b/.github/workflows/trivy-scheduled-dev.yml index 1399be061..7bd34cf9d 100644 --- a/.github/workflows/trivy-scheduled-dev.yml +++ b/.github/workflows/trivy-scheduled-dev.yml @@ -23,7 +23,9 @@ jobs: run: echo REPOSITORY_OWNER=$(echo ${{ github.repository_owner }} | tr "[:upper:]" "[:lower:]") >> $GITHUB_ENV - name: Run Trivy on latest dev image - uses: aquasecurity/trivy-action@0.24.0 + uses: aquasecurity/trivy-action@0.26.0 + env: + TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db with: image-ref: "ghcr.io/${{ env.REPOSITORY_OWNER }}/dds-backend:dev" format: "sarif" diff --git a/.github/workflows/trivy-scheduled-master.yml b/.github/workflows/trivy-scheduled-master.yml index 4ef9fa58b..e2ef46b86 100644 --- a/.github/workflows/trivy-scheduled-master.yml +++ b/.github/workflows/trivy-scheduled-master.yml @@ -25,7 +25,9 @@ jobs: run: echo REPOSITORY_OWNER=$(echo ${{ github.repository_owner }} | tr "[:upper:]" "[:lower:]") >> $GITHUB_ENV - name: Run Trivy on latest release image - uses: aquasecurity/trivy-action@0.24.0 + uses: aquasecurity/trivy-action@0.26.0 + env: + TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db with: image-ref: "ghcr.io/${{ env.REPOSITORY_OWNER }}/dds-backend:latest" format: "sarif" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d635628a..62c932f5c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,17 @@ Changelog ========== +.. _2.8.1: + +2.8.1 - 2024-10-23 +~~~~~~~~~~~~~~~~~~~~~~~ + +- New features: + - warning_level option when a unit is created defaults to 0.8. + - Add option to MOTD endpoint to send an email to unit users only. + - Modify the invoicing commands to send the instance name in the emails. +- Documentation: Update readme to indicate that the backend image is published to GGCR, not DockerHub. + .. _2.8.0: 2.8.0 - 2024-09-24 diff --git a/README.md b/README.md index 0bb2111a0..f5be819d3 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ Equally, if you want to tear down you need to run pytest _twice_ without it, as ## Production Instance -The production version of the backend image is published at [Dockerhub](https://hub.docker.com/repository/docker/scilifelabdatacentre/dds-backend). It can also be built by running: +The production version of the backend image is published at the [GitHub Container Registry (GHCR)](ghcr.io/scilifelabdatacentre/dds-backend). It can also be built by running: ```bash docker build --target production -f Dockerfiles/backend.Dockerfile . diff --git a/SPRINTLOG.md b/SPRINTLOG.md index c8c7c7d62..18638a870 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -430,3 +430,17 @@ _Nothing merged during this sprint_ - Flask command to update unit quotas ([#1551](https://github.com/ScilifelabDataCentre/dds_web/pull/1551)) - Bump python base image to 3.12 and related libraries in both web and client([#1548](https://github.com/ScilifelabDataCentre/dds_web/pull/1548)) +- Warning_level option defaults to 0.8([#1557](https://github.com/ScilifelabDataCentre/dds_web/pull/1557)) + +# 2024-09-24 - 2024-10-04 + +- Add option to motd command for sending to unit users only([#1552](https://github.com/ScilifelabDataCentre/dds_web/pull/1552)) + +# 2024-10-07 - 2024-10-18 + +- Update readme: backend image is published to GHCR, not DockerHub ([#1558](https://github.com/ScilifelabDataCentre/dds_web/pull/1558)) +- Workflow bug fixed: PDFs (Technical Overview and Troubleshooting) were downloaded to incorrect directory([#1559](https://github.com/ScilifelabDataCentre/dds_web/pull/1559)) +- Update trivy action and add a second mirror repository to reduce TOO MANY REQUEST issue([#1560](https://github.com/ScilifelabDataCentre/dds_web/pull/1560)) +- Modify the invoicing commands to send the instance name in the emails([#1561](https://github.com/ScilifelabDataCentre/dds_web/pull/1561)) +- Fix the MOTD endpoint according to post merge review([#1564](https://github.com/ScilifelabDataCentre/dds_web/pull/1564)) +- New version & changelog([#1565](https://github.com/ScilifelabDataCentre/dds_web/pull/1565)) diff --git a/dds_web/__init__.py b/dds_web/__init__.py index 5afd64728..fd87c63ca 100644 --- a/dds_web/__init__.py +++ b/dds_web/__init__.py @@ -171,6 +171,10 @@ def create_app(testing=False, database_uri=None): # Initiate app object app = flask.Flask(__name__, instance_relative_config=False) + # All variables in the env that start with FLASK_* will be loaded into the app config + # 'FLASK_' will be dropped, e.g. FLASK_TESTVAR will be loaded as TESTVAR + app.config.from_prefixed_env() + # Default development config app.config.from_object("dds_web.config.Config") diff --git a/dds_web/api/superadmin_only.py b/dds_web/api/superadmin_only.py index 6afc9b7e1..3cc435079 100644 --- a/dds_web/api/superadmin_only.py +++ b/dds_web/api/superadmin_only.py @@ -163,6 +163,15 @@ def post(self): if not motd_obj or not motd_obj.active: raise ddserr.DDSArgumentError(message=f"There is no active MOTD with ID '{motd_id}'.") + # check if sent to unit users only or all users + unit_only: bool = request_json.get("unit_only", False) + if not isinstance(unit_only, bool): + raise ddserr.DDSArgumentError(message="The 'unit_only' argument must be a boolean.") + if unit_only: + users_to_send = db.session.query(models.UnitUser) + else: + users_to_send = db.session.query(models.User) + # Create email content # put motd_obj.message etc in there etc subject: str = "Important Information: Data Delivery System" @@ -172,7 +181,7 @@ def post(self): # Setup email connection with mail.connect() as conn: # Email users - for user in utils.page_query(db.session.query(models.User)): + for user in utils.page_query(users_to_send): primary_email = user.primary_email if not primary_email: flask.current_app.logger.warning( @@ -197,7 +206,13 @@ def post(self): # Send email utils.send_email_with_retry(msg=msg, obj=conn) - return {"message": f"MOTD '{motd_id}' has been sent to the users."} + return_msg = f"MOTD '{motd_id}' has been " + if unit_only: + return_msg += "sent to unit personnel only." + else: + return_msg += "sent to all users." + + return {"message": return_msg} class FindUser(flask_restful.Resource): diff --git a/dds_web/commands.py b/dds_web/commands.py index 7cd3d8777..6550a8679 100644 --- a/dds_web/commands.py +++ b/dds_web/commands.py @@ -85,7 +85,7 @@ def fill_db_wrapper(db_type): @click.option("--days_in_available", "-da", type=int, required=False, default=90) @click.option("--days_in_expired", "-de", type=int, required=False, default=30) @click.option("--quota", "-q", type=int, required=True) -@click.option("--warn-at", "-w", type=int, required=False, default=80) +@click.option("--warn-at", "-w", type=click.FloatRange(0.0, 1.0), required=False, default=0.8) @flask.cli.with_appcontext def create_new_unit( name, @@ -841,16 +841,23 @@ def monthly_usage(): send_email_with_retry, ) + # Get the instance name (DEVELOPMENT, PRODUCTION, etc.) + instance_name = flask.current_app.config.get("INSTANCE_NAME") + # Email settings email_recipient: str = flask.current_app.config.get("MAIL_DDS") # -- Success email_subject: str = "[INVOICING CRONJOB]" + if instance_name: # instance name can be none, so check if it is set and add it to the subject + email_subject += f" ({instance_name})" + email_body: str = ( "The calculation of the monthly usage succeeded; The byte hours " "for all active projects have been saved to the database." ) # -- Failure error_subject: str = f"{email_subject} Error in monthly-usage cronjob" + error_body: str = ( "There was an error in the cronjob 'monthly-usage', used for calculating the" " byte hours for every active project in the last month.\n\n" @@ -972,13 +979,20 @@ def send_usage(months): from dds_web.database import models from dds_web.utils import current_time, page_query, send_email_with_retry + # Get the instance name (DEVELOPMENT, PRODUCTION, etc.) + instance_name = flask.current_app.config.get("INSTANCE_NAME") + # Email settings email_recipient: str = flask.current_app.config.get("MAIL_DDS") # -- Success email_subject: str = "[SEND-USAGE CRONJOB]" + if instance_name: # instance name can be none, so check if it is set and add it to the subject + email_subject += f" ({instance_name})" + email_body: str = f"Here is the usage for the last {months} months.\n" # -- Failure error_subject: str = f"{email_subject} Error in send-usage cronjob" + error_body: str = ( "There was an error in the cronjob 'send-usage', used for sending" " information about the storage usage for each SciLifeLab unit. \n\n" @@ -1133,7 +1147,15 @@ def collect_stats(): # Get email address recipient: str = flask.current_app.config.get("MAIL_DDS") - error_subject: str = "[CRONJOB] Error during collection of DDS unit- and user statistics." + + # Get the instance name (DEVELOPMENT, PRODUCTION, etc.) + instance_name = flask.current_app.config.get("INSTANCE_NAME") + + error_subject: str = "[CRONJOB]" + if instance_name: # instance name can be none, so check if it is set and add it to the subject + error_subject += f" ({instance_name})" + error_subject += " Error during collection of DDS unit and user statistics." + error_body: str = ( f"The cronjob 'reporting' experienced issues. Please see logs. Time: {current_time}." ) @@ -1256,7 +1278,7 @@ def monitor_usage(): # Get info from database quota: int = unit.quota - warn_after: int = unit.warning_level + warn_after: float = unit.warning_level current_usage: int = unit.size # Check if 0 and then skip the next steps @@ -1273,7 +1295,7 @@ def monitor_usage(): # Information to log and potentially send info_string: str = ( f"- Quota:{quota} bytes\n" - f"- Warning level: {warn_after*quota} bytes ({warn_after*100}%)\n" + f"- Warning level: {int(warn_after*quota)} bytes ({int(warn_after*100)}%)\n" f"- Current usage: {current_usage} bytes ({perc_used}%)\n" ) flask.current_app.logger.debug( diff --git a/dds_web/database/models.py b/dds_web/database/models.py index 104b413af..eb992339f 100644 --- a/dds_web/database/models.py +++ b/dds_web/database/models.py @@ -11,6 +11,7 @@ # Installed import sqlalchemy +from sqlalchemy.orm import validates import flask import argon2 import flask_login @@ -225,6 +226,12 @@ def size(self): return sum([p.size for p in self.projects]) + @validates("warning_level") + def validate_level(self, key, value): + if not (0.0 <= value <= 1.0): + raise ValueError("Warning level must be a float between 0 and 1") + return value + class Project(db.Model): """ diff --git a/dds_web/static/swagger.yaml b/dds_web/static/swagger.yaml index 5b5cb8686..a780320a8 100644 --- a/dds_web/static/swagger.yaml +++ b/dds_web/static/swagger.yaml @@ -1447,6 +1447,9 @@ paths: motd_id: type: integer example: 1 + unit_only: + type: boolean + example: false /user/find: get: tags: diff --git a/dds_web/static/swaggerv3.yaml b/dds_web/static/swaggerv3.yaml index 7417afb91..8b157279b 100644 --- a/dds_web/static/swaggerv3.yaml +++ b/dds_web/static/swaggerv3.yaml @@ -1409,6 +1409,9 @@ paths: motd_id: type: integer example: 1 + unit_only: + type: boolean + example: false /user/find: get: tags: diff --git a/dds_web/version.py b/dds_web/version.py index 91ebb8d13..dcd4a7562 100644 --- a/dds_web/version.py +++ b/dds_web/version.py @@ -1,3 +1,3 @@ # Do not do major version upgrade during 2024. # If mid or minor version reaches 9, continue with 10, 11 etc etc. -__version__ = "2.8.0" +__version__ = "2.8.1" diff --git a/docker-compose.yml b/docker-compose.yml index 98c287f93..7cda25b4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,8 @@ services: - DDS_APP_CONFIG=/code/dds_web/sensitive/dds_app.cfg - FLASK_DEBUG=true - FLASK_APP=dds_web + - FLASK_INSTANCE_NAME=LOCAL_DEVELOPMENT + - DB_TYPE=${DDS_DB_TYPE} # - RATELIMIT_STORAGE_URI=redis://dds_redis depends_on: diff --git a/tests/api/test_superadmin_only.py b/tests/api/test_superadmin_only.py index ca1d5d428..487a3efad 100644 --- a/tests/api/test_superadmin_only.py +++ b/tests/api/test_superadmin_only.py @@ -588,7 +588,34 @@ def test_send_motd_no_primary_email(client: flask.testing.FlaskClient) -> None: assert "incorrect subject" not in outbox[-1].subject -def test_send_motd_ok(client: flask.testing.FlaskClient) -> None: +def test_send_motd_incorrect_type_unit_user_only(client: flask.testing.FlaskClient) -> None: + """The parameter unit_only should be a boolean""" + # Authenticate + token: typing.Dict = get_token(username=users["Super Admin"], client=client) + + # Create a motd + message: str = "This is a message that should become a MOTD and then be sent to all the users." + new_motd: models.MOTD = models.MOTD(message=message) + db.session.add(new_motd) + db.session.commit() + + # Make sure the motd is created + created_motd: models.MOTD = models.MOTD.query.filter_by(message=message).one_or_none() + assert created_motd + + # Attempt request + with unittest.mock.patch.object(flask_mail.Connection, "send") as mock_mail_send: + response: werkzeug.test.WrapperTestResponse = client.post( + tests.DDSEndpoint.MOTD_SEND, + headers=token, + json={"motd_id": created_motd.id, "unit_only": "some_string"}, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + assert "The 'unit_only' argument must be a boolean." in response.json.get("message") + assert mock_mail_send.call_count == 0 + + +def test_send_motd_ok_all(client: flask.testing.FlaskClient) -> None: """Send a motd to all users.""" # Authenticate token: typing.Dict = get_token(username=users["Super Admin"], client=client) @@ -609,7 +636,39 @@ def test_send_motd_ok(client: flask.testing.FlaskClient) -> None: # Attempt request and catch email with mail.record_messages() as outbox: response: werkzeug.test.WrapperTestResponse = client.post( - tests.DDSEndpoint.MOTD_SEND, headers=token, json={"motd_id": created_motd.id} + tests.DDSEndpoint.MOTD_SEND, + headers=token, + json={"motd_id": created_motd.id, "unit_only": False}, + ) + assert response.status_code == http.HTTPStatus.OK + assert len(outbox) == num_users + assert "Important Information: Data Delivery System" in outbox[-1].subject + + +def test_send_motd_ok_unitusers(client: flask.testing.FlaskClient) -> None: + """Send a motd to all unitusers users.""" + # Authenticate + token: typing.Dict = get_token(username=users["Super Admin"], client=client) + + # Create a motd + message: str = "This is a message that should become a MOTD and then be sent to all the users." + new_motd: models.MOTD = models.MOTD(message=message) + db.session.add(new_motd) + db.session.commit() + + # Make sure the motd is created + created_motd: models.MOTD = models.MOTD.query.filter_by(message=message).one_or_none() + assert created_motd + + # Get number of users + num_users = models.UnitUser.query.count() + + # Attempt request and catch email + with mail.record_messages() as outbox: + response: werkzeug.test.WrapperTestResponse = client.post( + tests.DDSEndpoint.MOTD_SEND, + headers=token, + json={"motd_id": created_motd.id, "unit_only": True}, ) assert response.status_code == http.HTTPStatus.OK assert len(outbox) == num_users diff --git a/tests/test_commands.py b/tests/test_commands.py index cfb4de2b0..a92ad376e 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -134,6 +134,25 @@ def test_create_new_unit_public_id_too_long(client, runner, capfd: LogCaptureFix ) +def test_create_new_unit_incorrect_warning_level(client, runner, capfd: LogCaptureFixture) -> None: + """Create new unit, warning level is not a float between 0.0 and 1.0""" + # Change public_id + incorrect_unit: typing.Dict = correct_unit.copy() + incorrect_unit["warn-at"] = 30 + + # Get command options + command_options = create_command_options_from_dict(options=incorrect_unit) + + # Run command + result: click.testing.Result = runner.invoke(create_new_unit, command_options) + + assert result.exit_code != 0 # No sucess + # 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, capfd: LogCaptureFixture ) -> None: @@ -1636,7 +1655,10 @@ def create_file_versions(project: models.Project): # Error email should be sent assert len(outbox1) == 1 - assert "[INVOICING CRONJOB] Error in monthly-usage cronjob" in outbox1[-1].subject + assert ( + "[INVOICING CRONJOB] (LOCAL_DEVELOPMENT) Error in monthly-usage cronjob" + == outbox1[-1].subject + ) assert "What to do:" in outbox1[-1].body # No usage rows should have been saved @@ -1669,7 +1691,10 @@ def create_file_versions(project: models.Project): # Error email should have been sent assert len(outbox2) == 1 - assert "[INVOICING CRONJOB] Error in monthly-usage cronjob" in outbox2[-1].subject + assert ( + "[INVOICING CRONJOB] (LOCAL_DEVELOPMENT) Error in monthly-usage cronjob" + == outbox2[-1].subject + ) assert "What to do:" in outbox2[-1].body # Project versions should not be altered @@ -1702,7 +1727,10 @@ def create_file_versions(project: models.Project): # Email should be sent assert len(outbox3) == 1 - assert "[INVOICING CRONJOB] Usage records available for collection" in outbox3[-1].subject + assert ( + "[INVOICING CRONJOB] (LOCAL_DEVELOPMENT) Usage records available for collection" + == outbox3[-1].subject + ) assert ( "The calculation of the monthly usage succeeded; The byte hours for all active projects have been saved to the database." in outbox3[-1].body @@ -1719,7 +1747,25 @@ def create_file_versions(project: models.Project): assert usage_row_2 -# reporting units and users +def test_monthly_usage_no_instance_name(client, cli_runner, capfd: LogCaptureFixture): + """Test that the command do not send an email with the name if it is not set.""" + + import flask + + assert flask.current_app.config.get("INSTANCE_NAME") == "LOCAL_DEVELOPMENT" + # Set the instance name to none + flask.current_app.config["INSTANCE_NAME"] = None + + with mail.record_messages() as outbox: + cli_runner.invoke(monthly_usage) + + # Email should be sent + assert len(outbox) == 1 + assert "[INVOICING CRONJOB] Usage records available for collection" == outbox[-1].subject + assert ( + "The calculation of the monthly usage succeeded; The byte hours for all active projects have been saved to the database." + == outbox[-1].body + ) def test_collect_stats(client, cli_runner, fs: FakeFilesystem): @@ -1899,7 +1945,7 @@ def run_command_and_check_output(months_to_test, start_time): # Verify output and sent email assert len(outbox) == 1 assert ( - "[SEND-USAGE CRONJOB] Usage records attached in the present mail" + "[SEND-USAGE CRONJOB] (LOCAL_DEVELOPMENT) Usage records attached in the present mail" in outbox[-1].subject ) assert f"Here is the usage for the last {months_to_test} months." in outbox[-1].body @@ -2036,5 +2082,8 @@ def test_send_usage_error_csv(client, cli_runner, capfd: LogCaptureFixture): # Verify error email :- At least one email was sent assert len(outbox) == 1 - assert "[SEND-USAGE CRONJOB] Error in send-usage cronjob" in outbox[-1].subject + assert ( + "[SEND-USAGE CRONJOB] (LOCAL_DEVELOPMENT) Error in send-usage cronjob" + == outbox[-1].subject + ) assert "There was an error in the cronjob 'send-usage'" in outbox[-1].body diff --git a/tests/test_models.py b/tests/test_models.py index 6dc559dc3..316611b24 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -109,6 +109,32 @@ def test_delete_unit_row(client): assert invites == [] +def test_create_unit_wrong_warning_level(client): + """Test try to create a unit which has an invalid value for the warning level""" + from dds_web.utils import current_time + + unit = models.Unit.query.filter_by(name="Unit 1").first() + + with pytest.raises(ValueError) as err: + new_unit = models.Unit( + name="test", + public_id="public_id", + external_display_name="external_display_name", + contact_email=unit.contact_email, + internal_ref="public_id", + sto4_start_time=current_time(), + sto4_endpoint=unit.sto4_endpoint, + sto4_name=unit.sto4_name, + sto4_access=unit.sto4_access, + sto4_secret=unit.sto4_secret, + days_in_available=unit.days_in_available, + days_in_expired=unit.days_in_expired, + quota=unit.quota, + warning_level=20.0, + ) + assert "Warning level must be a float between 0 and 1" in str(err.value) + + # Project #################################################################################### Project # def __setup_project(): """ diff --git a/tests/test_version.py b/tests/test_version.py index ff0e1562e..2ef1533b8 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,4 +2,4 @@ def test_version(): - assert version.__version__ == "2.8.0" + assert version.__version__ == "2.8.1" diff --git a/tests/tests_v3/api/test_superadmin_only.py b/tests/tests_v3/api/test_superadmin_only.py index b70658819..1cf168359 100644 --- a/tests/tests_v3/api/test_superadmin_only.py +++ b/tests/tests_v3/api/test_superadmin_only.py @@ -575,7 +575,34 @@ def test_send_motd_no_primary_email(client: flask.testing.FlaskClient) -> None: assert "incorrect subject" not in outbox[-1].subject -def test_send_motd_ok(client: flask.testing.FlaskClient) -> None: +def test_send_motd_incorrect_type_unit_only(client: flask.testing.FlaskClient) -> None: + """The parameter unit_only should be a boolean""" + # Authenticate + token: typing.Dict = get_token(username=users["Super Admin"], client=client) + + # Create a motd + message: str = "This is a message that should become a MOTD and then be sent to all the users." + new_motd: models.MOTD = models.MOTD(message=message) + db.session.add(new_motd) + db.session.commit() + + # Make sure the motd is created + created_motd: models.MOTD = models.MOTD.query.filter_by(message=message).one_or_none() + assert created_motd + + # Attempt request + with unittest.mock.patch.object(flask_mail.Connection, "send") as mock_mail_send: + response: werkzeug.test.WrapperTestResponse = client.post( + tests.DDSEndpoint.MOTD_SEND, + headers=token, + json={"motd_id": created_motd.id, "unit_only": "some_string"}, + ) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + assert "The 'unit_only' argument must be a boolean." in response.json.get("message") + assert mock_mail_send.call_count == 0 + + +def test_send_motd_ok_all(client: flask.testing.FlaskClient) -> None: """Send a motd to all users.""" # Authenticate token: typing.Dict = get_token(username=users["Super Admin"], client=client) @@ -596,7 +623,39 @@ def test_send_motd_ok(client: flask.testing.FlaskClient) -> None: # Attempt request and catch email with mail.record_messages() as outbox: response: werkzeug.test.WrapperTestResponse = client.post( - tests.DDSEndpoint.MOTD_SEND, headers=token, json={"motd_id": created_motd.id} + tests.DDSEndpoint.MOTD_SEND, + headers=token, + json={"motd_id": created_motd.id, "unit_only": False}, + ) + assert response.status_code == http.HTTPStatus.OK + assert len(outbox) == num_users + assert "Important Information: Data Delivery System" in outbox[-1].subject + + +def test_send_motd_ok_unitusers(client: flask.testing.FlaskClient) -> None: + """Send a motd to all unitusers users.""" + # Authenticate + token: typing.Dict = get_token(username=users["Super Admin"], client=client) + + # Create a motd + message: str = "This is a message that should become a MOTD and then be sent to all the users." + new_motd: models.MOTD = models.MOTD(message=message) + db.session.add(new_motd) + db.session.commit() + + # Make sure the motd is created + created_motd: models.MOTD = models.MOTD.query.filter_by(message=message).one_or_none() + assert created_motd + + # Get number of users + num_users = models.UnitUser.query.count() + + # Attempt request and catch email + with mail.record_messages() as outbox: + response: werkzeug.test.WrapperTestResponse = client.post( + tests.DDSEndpoint.MOTD_SEND, + headers=token, + json={"motd_id": created_motd.id, "unit_only": True}, ) assert response.status_code == http.HTTPStatus.OK assert len(outbox) == num_users