Merge branch 'staging' into ci-fix-again
This commit is contained in:
commit
478a5b3e0b
@ -3,7 +3,7 @@
|
||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2020-02-12T18:51:01Z",
|
||||
"generated_at": "2020-02-17T20:49:33Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"base64_limit": 4.5,
|
||||
@ -82,7 +82,7 @@
|
||||
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 44,
|
||||
"line_number": 48,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
|
@ -219,6 +219,10 @@ 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_CALC_CLIENT_ID`: The client id used to generate a token for the Azure pricing calculator
|
||||
- `AZURE_CALC_RESOURCE`: The resource URL used to generate a token for the Azure pricing calculator
|
||||
- `AZURE_CALC_SECRET`: The secret key used to generate a token for the Azure pricing calculator
|
||||
- `AZURE_CALC_URL`: The redirect URL for the Azure pricing calculator
|
||||
- `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
|
||||
|
@ -1738,7 +1738,6 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
cost_mgmt_url = (
|
||||
f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01"
|
||||
)
|
||||
|
||||
try:
|
||||
result = self.sdk.requests.post(
|
||||
f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}",
|
||||
@ -1770,3 +1769,17 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
result.status_code,
|
||||
f"azure application error getting reporting data. {str(exc)}",
|
||||
)
|
||||
|
||||
def _get_calculator_creds(self):
|
||||
authority = f"{self.sdk.cloud.endpoints.active_directory}/{self.tenant_id}"
|
||||
context = self.sdk.adal.AuthenticationContext(authority=authority)
|
||||
response = context.acquire_token_with_client_credentials(
|
||||
self.config.get("AZURE_CALC_RESOURCE"),
|
||||
self.config.get("AZURE_CALC_CLIENT_ID"),
|
||||
self.config.get("AZURE_CALC_SECRET"),
|
||||
)
|
||||
return response.get("accessToken")
|
||||
|
||||
def get_calculator_url(self):
|
||||
calc_access_token = self._get_calculator_creds()
|
||||
return f"{self.config.get('AZURE_CALC_URL')}?access_token={calc_access_token}"
|
||||
|
@ -33,6 +33,7 @@ class PortfolioForm(BaseForm):
|
||||
class PortfolioCreationForm(PortfolioForm):
|
||||
defense_component = SelectMultipleField(
|
||||
translate("forms.portfolio.defense_component.title"),
|
||||
description=translate("forms.portfolio.defense_component.help_text"),
|
||||
choices=SERVICE_BRANCHES,
|
||||
widget=ListWidget(prefix_label=False),
|
||||
option_widget=CheckboxInput(),
|
||||
|
16
atst/jobs.py
16
atst/jobs.py
@ -111,6 +111,18 @@ def do_create_user(csp: CloudProviderInterface, application_role_ids=None):
|
||||
db.session.add(app_role)
|
||||
|
||||
db.session.commit()
|
||||
username = payload.user_principal_name
|
||||
send_mail(
|
||||
recipients=[user.email],
|
||||
subject=translate("email.app_role_created.subject"),
|
||||
body=translate(
|
||||
"email.app_role_created.body",
|
||||
{"url": app.config.get("AZURE_LOGIN_URL"), "username": username},
|
||||
),
|
||||
)
|
||||
app.logger.info(
|
||||
f"Application role created notification email sent. User id: {user.id}"
|
||||
)
|
||||
|
||||
|
||||
def do_create_environment(csp: CloudProviderInterface, environment_id=None):
|
||||
@ -280,7 +292,7 @@ def dispatch_create_atat_admin_user(self):
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_send_task_order_files(self):
|
||||
def send_task_order_files(self):
|
||||
task_orders = TaskOrders.get_for_send_task_order_files()
|
||||
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
|
||||
|
||||
@ -301,7 +313,7 @@ def dispatch_send_task_order_files(self):
|
||||
app.logger.exception(err)
|
||||
continue
|
||||
|
||||
task_order.pdf_last_sent_at = pendulum.now()
|
||||
task_order.pdf_last_sent_at = pendulum.now(tz="UTC")
|
||||
db.session.add(task_order)
|
||||
|
||||
db.session.commit()
|
||||
|
@ -27,6 +27,10 @@ def update_celery(celery, app):
|
||||
"task": "atst.jobs.dispatch_create_environment_role",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-send_task_order_files": {
|
||||
"task": "atst.jobs.send_task_order_files",
|
||||
"schedule": 60,
|
||||
},
|
||||
}
|
||||
|
||||
class ContextTask(celery.Task):
|
||||
|
@ -50,9 +50,7 @@ def reports(portfolio_id):
|
||||
"portfolios/reports/index.html",
|
||||
portfolio=portfolio,
|
||||
# wrapped in str() because this sum returns a Decimal object
|
||||
total_portfolio_value=str(
|
||||
portfolio.total_obligated_funds + portfolio.upcoming_obligated_funds
|
||||
),
|
||||
total_portfolio_value=str(portfolio.total_obligated_funds),
|
||||
current_obligated_funds=current_obligated_funds,
|
||||
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||
retrieved=pendulum.now(), # mocked datetime of reporting data retrival
|
||||
|
@ -3,6 +3,10 @@ ASSETS_URL
|
||||
AZURE_AADP_QTY=5
|
||||
AZURE_ACCOUNT_NAME
|
||||
AZURE_CLIENT_ID
|
||||
AZURE_CALC_CLIENT_ID
|
||||
AZURE_CALC_RESOURCE="http://azurecom.onmicrosoft.com/acom-prod/"
|
||||
AZURE_CALC_SECRET
|
||||
AZURE_CALC_URL="https://azure.microsoft.com/en-us/pricing/calculator/"
|
||||
AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/"
|
||||
AZURE_LOGIN_URL="https://portal.azure.com/"
|
||||
AZURE_POLICY_LOCATION=policies
|
||||
|
@ -2,10 +2,6 @@
|
||||
.form-row {
|
||||
margin: ($gap * 4) 0;
|
||||
|
||||
&--bordered {
|
||||
border-bottom: $color-gray-lighter 1px solid;
|
||||
}
|
||||
|
||||
.form-col {
|
||||
flex-grow: 1;
|
||||
|
||||
@ -22,8 +18,9 @@
|
||||
}
|
||||
|
||||
.usa-input {
|
||||
input {
|
||||
max-width: none;
|
||||
input,
|
||||
textarea {
|
||||
max-width: $max-input-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -204,6 +201,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.form-container__half {
|
||||
max-width: 46rem;
|
||||
.form-container {
|
||||
margin-bottom: $action-footer-height + $large-spacing;
|
||||
|
||||
&--narrow {
|
||||
max-width: $max-input-width;
|
||||
}
|
||||
}
|
||||
|
@ -5,17 +5,11 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
text-align: left;
|
||||
|
||||
.usa-input__choices label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
padding: $gap * 2;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
|
||||
&.checked {
|
||||
border: 1px solid $color-blue;
|
||||
@ -33,7 +27,7 @@
|
||||
|
||||
.user-info {
|
||||
.usa-input {
|
||||
width: 45rem;
|
||||
max-width: $max-input-width;
|
||||
|
||||
input,
|
||||
label,
|
||||
@ -53,8 +47,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
#modal--add-app-mem,
|
||||
#modal--add-portfolio-manager {
|
||||
.form-content--member-form {
|
||||
.modal__body {
|
||||
min-width: 75rem;
|
||||
}
|
||||
|
@ -519,3 +519,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#portfolio-create {
|
||||
.usa-input__choices {
|
||||
.usa-input__title {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: $base-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,3 @@ hr {
|
||||
.usa-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-bottom: $action-footer-height + $large-spacing;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ $home-pg-icon-width: 6rem;
|
||||
$large-spacing: 4rem;
|
||||
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
|
||||
$action-footer-height: 6rem;
|
||||
$max-input-width: 46rem;
|
||||
|
||||
/*
|
||||
* USWDS Variables
|
||||
|
@ -58,7 +58,7 @@
|
||||
|
||||
.usa-input {
|
||||
margin: ($gap * 2) 0;
|
||||
max-width: 75rem;
|
||||
max-width: $max-input-width;
|
||||
|
||||
&-label-helper {
|
||||
font-size: $small-font-size;
|
||||
@ -111,8 +111,7 @@
|
||||
@include h5;
|
||||
|
||||
font-weight: normal;
|
||||
|
||||
@include line-max;
|
||||
max-width: $max-input-width;
|
||||
|
||||
.icon-link {
|
||||
padding: 0 ($gap / 2);
|
||||
@ -180,6 +179,10 @@
|
||||
left: -3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-input__title {
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@ -372,19 +375,15 @@ select {
|
||||
.phone-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
&__phone {
|
||||
margin-right: $gap * 4;
|
||||
flex-grow: 1;
|
||||
|
||||
.usa-input {
|
||||
margin: 0;
|
||||
|
||||
input,
|
||||
label,
|
||||
.usa-input__message {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.icon-validation {
|
||||
left: 20rem;
|
||||
}
|
||||
@ -392,7 +391,8 @@ select {
|
||||
}
|
||||
|
||||
&__extension {
|
||||
margin-left: $gap * 4;
|
||||
margin-right: $gap * 4;
|
||||
flex-grow: 0;
|
||||
|
||||
.usa-input {
|
||||
margin: 0;
|
||||
|
@ -140,6 +140,10 @@
|
||||
&__confirmation {
|
||||
margin-left: $gap * 8;
|
||||
}
|
||||
|
||||
.usa-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order__modal-cancel {
|
||||
|
@ -36,7 +36,7 @@
|
||||
{% set invite_expired = member.role_status == 'invite_expired' %}
|
||||
{%- if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
|
||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||
{% call Modal(modal_name, classes="form-content--member-form") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||
</div>
|
||||
@ -56,7 +56,7 @@
|
||||
|
||||
{%- if invite_pending or invite_expired %}
|
||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||
{% call Modal(resend_invite_modal, classes="form-content--member-form") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||
</div>
|
||||
@ -183,6 +183,7 @@
|
||||
modal=new_member_modal_name,
|
||||
)
|
||||
],
|
||||
classes="form-content--member-form",
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<base-form inline-template :enable-save="true">
|
||||
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form">
|
||||
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form-container">
|
||||
{{ form.csrf_token }}
|
||||
<div class="form-row">
|
||||
<div class="form-col">
|
||||
|
@ -21,7 +21,7 @@
|
||||
</p>
|
||||
<hr>
|
||||
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit" class="form">
|
||||
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit" class="form-container">
|
||||
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
||||
<div class="panel">
|
||||
<div class="panel__content">
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
{% if user_can(permissions.EDIT_APPLICATION) %}
|
||||
<base-form inline-template>
|
||||
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
|
||||
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="form-container--narrow">
|
||||
{{ application_form.csrf_token }}
|
||||
{{ TextInput(application_form.name, validation="applicationName", optional=False) }}
|
||||
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
|
||||
|
@ -25,10 +25,10 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro MultiStepModalForm(name, form, form_action, steps, dismissable=False) -%}
|
||||
{% macro MultiStepModalForm(name, form, form_action, steps, dismissable=False, classes="") -%}
|
||||
{% set step_count = steps|length %}
|
||||
<multi-step-modal-form inline-template :steps={{ step_count }}>
|
||||
{% call Modal(name=name, dismissable=dismissable) %}
|
||||
{% call Modal(name=name, dismissable=dismissable, classes=classes) %}
|
||||
<form id="{{ name }}" action="{{ form_action }}" method="POST" v-on:submit="handleSubmit">
|
||||
{{ form.csrf_token }}
|
||||
<div v-if="$root.activeModal === '{{ name }}'">
|
||||
|
@ -13,7 +13,7 @@
|
||||
<div v-cloak class="portfolio-admin">
|
||||
{% include "fragments/flash.html" %}
|
||||
<!-- max width of this section is 460px -->
|
||||
<section class="form-container__half">
|
||||
<section class="form-container--narrow">
|
||||
<h3>Portfolio name and component</h3>
|
||||
{% if user_can(permissions.EDIT_PORTFOLIO_NAME) %}
|
||||
<base-form inline-template>
|
||||
|
@ -14,7 +14,7 @@
|
||||
{% set invite_expired = member.status == 'invite_expired' %}
|
||||
|
||||
{% set modal_name = "edit_member-{}".format(loop.index) %}
|
||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||
{% call Modal(modal_name, classes="form-content--member-form") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||
</div>
|
||||
@ -34,7 +34,7 @@
|
||||
|
||||
{% if invite_pending or invite_expired -%}
|
||||
{% set resend_invite_modal = "resend_invite-{}".format(member.role_id) %}
|
||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||
{% call Modal(resend_invite_modal, classes="form-content--member-form") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||
</div>
|
||||
@ -182,6 +182,7 @@
|
||||
modal=new_manager_modal,
|
||||
)
|
||||
],
|
||||
classes="form-content--member-form",
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -19,24 +19,25 @@
|
||||
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
||||
<base-form inline-template>
|
||||
<div class="row">
|
||||
<form id="portfolio-create" class="col form" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||
<form id="portfolio-create" class="col form-container" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<div class="form-row form-row--bordered">
|
||||
<div class="form-row">
|
||||
<div class="form-col">
|
||||
{{ TextInput(form.name, validation="portfolioName", optional=False, classes="form-col") }}
|
||||
{{"forms.portfolio.name.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row form-row--bordered">
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
<div class="form-col">
|
||||
{{ TextInput(form.description, validation="defaultTextAreaField", paragraph=True) }}
|
||||
{{"forms.portfolio.description.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
<div class="form-col">
|
||||
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
|
||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<div class="task-order form">
|
||||
<div class="task-order form-container">
|
||||
{% block to_builder_form_field %}{% endblock %}
|
||||
</div>
|
||||
<div
|
||||
|
@ -1523,3 +1523,23 @@ def test_update_tenant_creds(mock_azure: AzureCloudProvider):
|
||||
assert updated_secret == KeyVaultCredentials(
|
||||
**{**existing_secrets, **MOCK_CREDS}
|
||||
)
|
||||
|
||||
|
||||
def test_get_calculator_creds(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
assert mock_azure._get_calculator_creds() == "TOKEN"
|
||||
|
||||
|
||||
def test_get_calculator_url(mock_azure: AzureCloudProvider):
|
||||
with patch.object(
|
||||
AzureCloudProvider,
|
||||
"_get_calculator_creds",
|
||||
wraps=mock_azure._get_calculator_creds,
|
||||
) as _get_calculator_creds:
|
||||
_get_calculator_creds.return_value = "TOKEN"
|
||||
assert (
|
||||
mock_azure.get_calculator_url()
|
||||
== f"{mock_azure.config.get('AZURE_CALC_URL')}?access_token=TOKEN"
|
||||
)
|
||||
|
@ -4,6 +4,9 @@ from unittest.mock import Mock
|
||||
from atst.domain.csp.cloud import AzureCloudProvider
|
||||
|
||||
AZURE_CONFIG = {
|
||||
"AZURE_CALC_CLIENT_ID": "MOCK",
|
||||
"AZURE_CALC_SECRET": "MOCK", # pragma: allowlist secret
|
||||
"AZURE_CALC_RESOURCE": "http://calc",
|
||||
"AZURE_CLIENT_ID": "MOCK",
|
||||
"AZURE_SECRET_KEY": "MOCK",
|
||||
"AZURE_TENANT_ID": "MOCK",
|
||||
|
@ -16,7 +16,6 @@ from atst.jobs import (
|
||||
dispatch_create_user,
|
||||
dispatch_create_environment_role,
|
||||
dispatch_provision_portfolio,
|
||||
dispatch_send_task_order_files,
|
||||
create_environment,
|
||||
do_create_user,
|
||||
do_provision_portfolio,
|
||||
@ -24,6 +23,7 @@ from atst.jobs import (
|
||||
do_create_environment_role,
|
||||
do_create_application,
|
||||
send_PPOC_email,
|
||||
send_task_order_files,
|
||||
)
|
||||
from tests.factories import (
|
||||
ApplicationFactory,
|
||||
@ -135,28 +135,63 @@ def test_create_application_job_is_idempotent(csp):
|
||||
csp.create_application.assert_not_called()
|
||||
|
||||
|
||||
def test_create_user_job(session, csp, app):
|
||||
portfolio = PortfolioFactory.create(
|
||||
csp_data={
|
||||
"tenant_id": str(uuid4()),
|
||||
"domain_name": f"rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}",
|
||||
}
|
||||
)
|
||||
application = ApplicationFactory.create(portfolio=portfolio, cloud_id="321")
|
||||
user = UserFactory.create(
|
||||
first_name="Han", last_name="Solo", email="han@example.com"
|
||||
)
|
||||
app_role = ApplicationRoleFactory.create(
|
||||
application=application,
|
||||
user=user,
|
||||
status=ApplicationRoleStatus.ACTIVE,
|
||||
cloud_id=None,
|
||||
)
|
||||
class TestCreateUserJob:
|
||||
@pytest.fixture
|
||||
def portfolio(self, app):
|
||||
return PortfolioFactory.create(
|
||||
csp_data={
|
||||
"tenant_id": str(uuid4()),
|
||||
"domain_name": f"rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}",
|
||||
}
|
||||
)
|
||||
|
||||
do_create_user(csp, [app_role.id])
|
||||
session.refresh(app_role)
|
||||
@pytest.fixture
|
||||
def app_1(self, portfolio):
|
||||
return ApplicationFactory.create(portfolio=portfolio, cloud_id="321")
|
||||
|
||||
assert app_role.cloud_id
|
||||
@pytest.fixture
|
||||
def app_2(self, portfolio):
|
||||
return ApplicationFactory.create(portfolio=portfolio, cloud_id="123")
|
||||
|
||||
@pytest.fixture
|
||||
def user(self):
|
||||
return UserFactory.create(
|
||||
first_name="Han", last_name="Solo", email="han@example.com"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def app_role_1(self, app_1, user):
|
||||
return ApplicationRoleFactory.create(
|
||||
application=app_1,
|
||||
user=user,
|
||||
status=ApplicationRoleStatus.ACTIVE,
|
||||
cloud_id=None,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def app_role_2(self, app_2, user):
|
||||
return ApplicationRoleFactory.create(
|
||||
application=app_2,
|
||||
user=user,
|
||||
status=ApplicationRoleStatus.ACTIVE,
|
||||
cloud_id=None,
|
||||
)
|
||||
|
||||
def test_create_user_job(self, session, csp, app_role_1):
|
||||
assert not app_role_1.cloud_id
|
||||
|
||||
session.begin_nested()
|
||||
do_create_user(csp, [app_role_1.id])
|
||||
session.rollback()
|
||||
|
||||
assert app_role_1.cloud_id
|
||||
|
||||
def test_create_user_sends_email(self, monkeypatch, csp, app_role_1, app_role_2):
|
||||
mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||
|
||||
do_create_user(csp, [app_role_1.id, app_role_2.id])
|
||||
assert mock.call_count == 1
|
||||
|
||||
|
||||
def test_dispatch_create_environment(session, monkeypatch):
|
||||
@ -380,82 +415,77 @@ def test_create_environment_role():
|
||||
assert env_role.cloud_id == "a-cloud-id"
|
||||
|
||||
|
||||
# TODO: Refactor the tests related to dispatch_send_task_order_files() into a class
|
||||
# and separate the success test into two tests
|
||||
def test_dispatch_send_task_order_files(monkeypatch, app):
|
||||
mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||
class TestSendTaskOrderFiles:
|
||||
@pytest.fixture(scope="function")
|
||||
def send_mail(self, monkeypatch):
|
||||
mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||
return mock
|
||||
|
||||
def _download_task_order(MockFileService, object_name):
|
||||
return {"name": object_name}
|
||||
@pytest.fixture(scope="function")
|
||||
def download_task_order(self, monkeypatch):
|
||||
def _download_task_order(MockFileService, object_name):
|
||||
return {"name": object_name}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||
_download_task_order,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||
_download_task_order,
|
||||
)
|
||||
|
||||
# Create 3 new Task Orders
|
||||
for i in range(3):
|
||||
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||
def test_sends_multiple_emails(self, send_mail, download_task_order):
|
||||
# Create 3 Task Orders
|
||||
for i in range(3):
|
||||
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||
|
||||
dispatch_send_task_order_files.run()
|
||||
send_task_order_files.run()
|
||||
|
||||
# Check that send_with_attachment was called once for each task order
|
||||
assert mock.call_count == 3
|
||||
mock.reset_mock()
|
||||
# Check that send_with_attachment was called once for each task order
|
||||
assert send_mail.call_count == 3
|
||||
|
||||
# Create new TO
|
||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||
assert not task_order.pdf_last_sent_at
|
||||
def test_kwargs(self, send_mail, download_task_order, app):
|
||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||
send_task_order_files.run()
|
||||
|
||||
dispatch_send_task_order_files.run()
|
||||
# Check that send_with_attachment was called with correct kwargs
|
||||
send_mail.assert_called_once_with(
|
||||
recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")],
|
||||
subject=translate(
|
||||
"email.task_order_sent.subject", {"to_number": task_order.number}
|
||||
),
|
||||
body=translate(
|
||||
"email.task_order_sent.body", {"to_number": task_order.number}
|
||||
),
|
||||
attachments=[
|
||||
{
|
||||
"name": task_order.pdf.object_name,
|
||||
"maintype": "application",
|
||||
"subtype": "pdf",
|
||||
}
|
||||
],
|
||||
)
|
||||
assert task_order.pdf_last_sent_at
|
||||
|
||||
# Check that send_with_attachment was called with correct kwargs
|
||||
mock.assert_called_once_with(
|
||||
recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")],
|
||||
subject=translate(
|
||||
"email.task_order_sent.subject", {"to_number": task_order.number}
|
||||
),
|
||||
body=translate("email.task_order_sent.body", {"to_number": task_order.number}),
|
||||
attachments=[
|
||||
{
|
||||
"name": task_order.pdf.object_name,
|
||||
"maintype": "application",
|
||||
"subtype": "pdf",
|
||||
}
|
||||
],
|
||||
)
|
||||
def test_send_failure(self, monkeypatch):
|
||||
def _raise_smtp_exception(**kwargs):
|
||||
raise SMTPException
|
||||
|
||||
assert task_order.pdf_last_sent_at
|
||||
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
|
||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||
send_task_order_files.run()
|
||||
|
||||
# Check that pdf_last_sent_at has not been updated
|
||||
assert not task_order.pdf_last_sent_at
|
||||
|
||||
def test_dispatch_send_task_order_files_send_failure(monkeypatch):
|
||||
def _raise_smtp_exception(**kwargs):
|
||||
raise SMTPException
|
||||
def test_download_failure(self, send_mail, monkeypatch):
|
||||
def _download_task_order(MockFileService, object_name):
|
||||
raise AzureError("something went wrong")
|
||||
|
||||
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||
_download_task_order,
|
||||
)
|
||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0002"}])
|
||||
send_task_order_files.run()
|
||||
|
||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||
dispatch_send_task_order_files.run()
|
||||
|
||||
# Check that pdf_last_sent_at has not been updated
|
||||
assert not task_order.pdf_last_sent_at
|
||||
|
||||
|
||||
def test_dispatch_send_task_order_files_download_failure(monkeypatch):
|
||||
mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||
|
||||
def _download_task_order(MockFileService, object_name):
|
||||
raise AzureError("something went wrong")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||
_download_task_order,
|
||||
)
|
||||
|
||||
task_order = TaskOrderFactory.create(create_clins=[{"number": "0002"}])
|
||||
dispatch_send_task_order_files.run()
|
||||
|
||||
# Check that pdf_last_sent_at has not been updated
|
||||
assert not task_order.pdf_last_sent_at
|
||||
# Check that pdf_last_sent_at has not been updated
|
||||
assert not task_order.pdf_last_sent_at
|
||||
|
@ -83,7 +83,10 @@ errors:
|
||||
not_found_sub: This page does not exist.
|
||||
email:
|
||||
application_invite: "{inviter_name} has invited you to a JEDI cloud application"
|
||||
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio"
|
||||
app_role_created:
|
||||
subject: Application Role Created
|
||||
body: "Your application role has been created.\nVisit {url}, and use your username, {username}, to log in."
|
||||
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio."
|
||||
portfolio_ready:
|
||||
subject: Portfolio Provisioned
|
||||
body: "Your portfolio has been provisioned.\nVisit {password_reset_address}, and use your username, {username}, to create a password."
|
||||
@ -268,7 +271,7 @@ forms:
|
||||
label: Portfolio Name
|
||||
length_validation_message: Portfolio names can be between 4-100 characters
|
||||
help_text: |
|
||||
<div>
|
||||
<div class="usa-input__help">
|
||||
<p>
|
||||
Naming can be difficult. Choose a name that is descriptive enough for users to identify the Portfolio. You may consider naming based on your organization.
|
||||
</p>
|
||||
@ -282,7 +285,7 @@ forms:
|
||||
description:
|
||||
label: Portfolio Description
|
||||
help_text: |
|
||||
<div>
|
||||
<div class="usa-input__help">
|
||||
<p>
|
||||
Add a brief one to two sentence description of your Portfolio. Consider this your statement of work.
|
||||
</p>
|
||||
@ -307,11 +310,9 @@ forms:
|
||||
title: Select DoD component(s) funding your Portfolio
|
||||
validation_message: You must select at least one defense component.
|
||||
help_text: |
|
||||
<p>
|
||||
Select the DOD component(s) that will fund all Applications within this Portfolio.
|
||||
In JEDI, multiple DoD organizations can fund the same Portfolio.<br/>
|
||||
Select all that apply.<br/>
|
||||
</p>
|
||||
Select all that apply.
|
||||
attachment:
|
||||
object_name:
|
||||
length_error: Object name may be no longer than 40 characters.
|
||||
@ -515,7 +516,7 @@ portfolios:
|
||||
estimate_warning: Reports displayed in JEDI are estimates and not a system of record. To manage your costs, go to Azure by selecting the Login to Azure button above.
|
||||
total_value:
|
||||
header: Total Portfolio Value
|
||||
tooltip: Total portfolio value is all obligated funds for current and upcoming task orders in this portfolio.
|
||||
tooltip: Total portfolio value is all obligated funds for active task orders in this portfolio.
|
||||
task_orders:
|
||||
add_new_button: Add New Task Order
|
||||
review:
|
||||
|
Loading…
x
Reference in New Issue
Block a user