diff --git a/.secrets.baseline b/.secrets.baseline index 4e393738..45f10336 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "lines": null }, - "generated_at": "2020-02-10T21:40:38Z", + "generated_at": "2020-02-12T18:51:01Z", "plugins_used": [ { "base64_limit": 4.5, @@ -82,7 +82,7 @@ "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 43, + "line_number": 44, "type": "Secret Keyword" } ], diff --git a/README.md b/README.md index b8530135..accecd38 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ To generate coverage reports for the Javascript tests: - `ASSETS_URL`: URL to host which serves static assets (such as a CDN). - `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account +- `AZURE_LOGIN_URL`: The URL used to login for an Azure instance. - `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account - `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads - `BLOB_STORAGE_URL`: URL to Azure blob storage container. diff --git a/atst/jobs.py b/atst/jobs.py index 0d462f0f..0094a6dd 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -18,6 +18,7 @@ from atst.domain.environments import Environments from atst.domain.environment_roles import EnvironmentRoles from atst.domain.portfolios import Portfolios from atst.models import CSPRole, JobFailure +from atst.models.mixins.state_machines import FSMStates from atst.domain.task_orders import TaskOrders from atst.models.utils import claim_for_update, claim_many_for_update from atst.queue import celery @@ -177,10 +178,30 @@ def do_work(fn, task, csp, **kwargs): raise task.retry(exc=e) +def send_PPOC_email(portfolio_dict): + ppoc_email = portfolio_dict.get("password_recovery_email_address") + user_id = portfolio_dict.get("user_id") + domain_name = portfolio_dict.get("domain_name") + + send_mail( + recipients=[ppoc_email], + subject=translate("email.portfolio_ready.subject"), + body=translate( + "email.portfolio_ready.body", + { + "password_reset_address": app.config.get("AZURE_LOGIN_URL"), + "username": f"{user_id}@{domain_name}.onmicrosoft.com", + }, + ), + ) + + def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None): portfolio = Portfolios.get_for_update(portfolio_id) fsm = Portfolios.get_or_create_state_machine(portfolio) fsm.trigger_next_transition(csp_data=portfolio.to_dictionary()) + if fsm.current_state == FSMStates.COMPLETED: + send_PPOC_email(portfolio.to_dictionary()) @celery.task(bind=True, base=RecordFailure) diff --git a/config/base.ini b/config/base.ini index 727172d8..3aa9ad86 100644 --- a/config/base.ini +++ b/config/base.ini @@ -4,6 +4,7 @@ AZURE_AADP_QTY=5 AZURE_ACCOUNT_NAME AZURE_CLIENT_ID AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/" +AZURE_LOGIN_URL="https://portal.azure.com/" AZURE_POLICY_LOCATION=policies AZURE_POWERSHELL_CLIENT_ID AZURE_ROLE_DEF_ID_BILLING_READER="fa23ad8b-c56e-40d8-ac0c-ce449e1d2c64" diff --git a/tests/test_jobs.py b/tests/test_jobs.py index c14b626c..3edfd769 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -7,8 +7,7 @@ from azure.core.exceptions import AzureError from atst.domain.csp.cloud import MockCloudProvider from atst.domain.csp.cloud.models import UserRoleCSPResult -from atst.domain.portfolios import Portfolios -from atst.models import ApplicationRoleStatus +from atst.models import ApplicationRoleStatus, Portfolio, FSMStates from atst.jobs import ( RecordFailure, @@ -24,6 +23,7 @@ from atst.jobs import ( do_create_environment, do_create_environment_role, do_create_application, + send_PPOC_email, ) from tests.factories import ( ApplicationFactory, @@ -281,10 +281,57 @@ def test_dispatch_provision_portfolio(csp, monkeypatch): mock.delay.assert_called_once_with(portfolio_id=portfolio.id) -def test_do_provision_portfolio(csp, session, portfolio): - do_provision_portfolio(csp=csp, portfolio_id=portfolio.id) - session.refresh(portfolio) - assert portfolio.state_machine +class TestDoProvisionPortfolio: + def test_portfolio_has_state_machine(self, csp, session, portfolio): + do_provision_portfolio(csp=csp, portfolio_id=portfolio.id) + session.refresh(portfolio) + assert portfolio.state_machine + + def test_sends_email_to_PPOC_on_completion( + self, monkeypatch, csp, portfolio: Portfolio + ): + mock = Mock() + monkeypatch.setattr("atst.jobs.send_PPOC_email", mock) + + csp._authorize.return_value = None + csp._maybe_raise.return_value = None + sm: PortfolioStateMachine = PortfolioStateMachineFactory.create( + portfolio=portfolio + ) + # The stage before "COMPLETED" + sm.state = FSMStates.BILLING_OWNER_CREATED + do_provision_portfolio(csp=csp, portfolio_id=portfolio.id) + + # send_PPOC_email was called + assert mock.assert_called_once + + +def test_send_ppoc_email(monkeypatch, app): + mock = Mock() + monkeypatch.setattr("atst.jobs.send_mail", mock) + + ppoc_email = "example@example.com" + user_id = "user_id" + domain_name = "domain" + + send_PPOC_email( + { + "password_recovery_email_address": ppoc_email, + "user_id": user_id, + "domain_name": domain_name, + } + ) + mock.assert_called_once_with( + recipients=[ppoc_email], + subject=translate("email.portfolio_ready.subject"), + body=translate( + "email.portfolio_ready.body", + { + "password_reset_address": app.config.get("AZURE_LOGIN_URL"), + "username": f"{user_id}@{domain_name}.onmicrosoft.com", + }, + ), + ) def test_provision_portfolio_create_tenant(