Merge branch 'staging' into to-builder-previous-button

This commit is contained in:
leigh-mil 2020-01-29 15:09:27 -05:00 committed by GitHub
commit f48404215a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 2845 additions and 859 deletions

View File

@ -21,11 +21,8 @@ LICENSE
# Skip envrc # Skip envrc
.envrc .envrc
# Skip ansible-container stuff # Skip terraform
ansible* terraform
container.yml
meta.yml
requirements.yml
# Skip kubernetes and Docker config stuff # Skip kubernetes and Docker config stuff
deploy deploy

View File

@ -257,6 +257,7 @@ To generate coverage reports for the Javascript tests:
- `SESSION_COOKIE_DOMAIN`: String value specifying the name to use for the session cookie. This should be set to the root domain so that it is valid for both the main site and the authentication subdomain. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_DOMAIN - `SESSION_COOKIE_DOMAIN`: String value specifying the name to use for the session cookie. This should be set to the root domain so that it is valid for both the main site and the authentication subdomain. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_DOMAIN
- `SESSION_KEY_PREFIX`: A prefix that is added before all session keys: https://pythonhosted.org/Flask-Session/#configuration - `SESSION_KEY_PREFIX`: A prefix that is added before all session keys: https://pythonhosted.org/Flask-Session/#configuration
- `SESSION_TYPE`: String value specifying the cookie storage backend. https://pythonhosted.org/Flask-Session/ - `SESSION_TYPE`: String value specifying the cookie storage backend. https://pythonhosted.org/Flask-Session/
- `SESSION_COOKIE_SECURE`: https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_SECURE
- `SESSION_USE_SIGNER`: Boolean value specifying if the cookie sid should be signed. - `SESSION_USE_SIGNER`: Boolean value specifying if the cookie sid should be signed.
- `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout. - `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout.
- `STATIC_URL`: URL specifying where static assets are hosted. - `STATIC_URL`: URL specifying where static assets are hosted.

View File

@ -0,0 +1,29 @@
"""add applications.claimed_until
Revision ID: 07e0598199f6
Revises: 26319c44a8d5
Create Date: 2020-01-25 13:33:17.711548
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '07e0598199f6' # pragma: allowlist secret
down_revision = '26319c44a8d5' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('applications', sa.Column('claimed_until', sa.TIMESTAMP(timezone=True), nullable=True))
op.add_column('applications', sa.Column('cloud_id', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('applications', 'claimed_until')
op.drop_column('applications', 'cloud_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,60 @@
"""combine job failures
Revision ID: 508957112ed6
Revises: 07e0598199f6
Create Date: 2020-01-25 15:03:06.377442
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '508957112ed6' # pragma: allowlist secret
down_revision = '07e0598199f6' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('job_failures',
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.String(), nullable=False),
sa.Column('entity', sa.String(), nullable=False),
sa.Column('entity_id', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('environment_job_failures')
op.drop_table('environment_role_job_failures')
op.drop_table('portfolio_job_failures')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('portfolio_job_failures',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('task_id', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('portfolio_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['portfolio_id'], ['portfolios.id'], name='portfolio_job_failures_portfolio_id_fkey'),
sa.PrimaryKeyConstraint('id', name='portfolio_job_failures_pkey')
)
op.create_table('environment_role_job_failures',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('task_id', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('environment_role_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['environment_role_id'], ['environment_roles.id'], name='environment_role_job_failures_environment_role_id_fkey'),
sa.PrimaryKeyConstraint('id', name='environment_role_job_failures_pkey')
)
op.create_table('environment_job_failures',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('task_id', sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column('environment_id', postgresql.UUID(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['environment_id'], ['environments.id'], name='environment_job_failures_environment_id_fkey'),
sa.PrimaryKeyConstraint('id', name='environment_job_failures_pkey')
)
op.drop_table('job_failures')
# ### end Alembic commands ###

View File

@ -1,5 +1,9 @@
from . import BaseDomainClass
from flask import g from flask import g
from sqlalchemy import func, or_
from typing import List
from uuid import UUID
from . import BaseDomainClass
from atst.database import db from atst.database import db
from atst.domain.application_roles import ApplicationRoles from atst.domain.application_roles import ApplicationRoles
from atst.domain.environments import Environments from atst.domain.environments import Environments
@ -10,7 +14,10 @@ from atst.models import (
ApplicationRole, ApplicationRole,
ApplicationRoleStatus, ApplicationRoleStatus,
EnvironmentRole, EnvironmentRole,
Portfolio,
PortfolioStateMachine,
) )
from atst.models.mixins.state_machines import FSMStates
from atst.utils import first_or_none, commit_or_raise_already_exists_error from atst.utils import first_or_none, commit_or_raise_already_exists_error
@ -118,3 +125,21 @@ class Applications(BaseDomainClass):
db.session.commit() db.session.commit()
return invitation return invitation
@classmethod
def get_applications_pending_creation(cls) -> List[UUID]:
results = (
db.session.query(Application.id)
.join(Portfolio)
.join(PortfolioStateMachine)
.filter(PortfolioStateMachine.state == FSMStates.COMPLETED)
.filter(Application.deleted == False)
.filter(Application.cloud_id.is_(None))
.filter(
or_(
Application.claimed_until.is_(None),
Application.claimed_until <= func.now(),
)
)
).all()
return [id_ for id_, in results]

View File

@ -1,15 +1,14 @@
import json
import re import re
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Dict from typing import Dict
from uuid import uuid4 from uuid import uuid4
from atst.models.application import Application
from atst.models.environment import Environment
from atst.models.user import User
from .cloud_provider_interface import CloudProviderInterface from .cloud_provider_interface import CloudProviderInterface
from .exceptions import AuthenticationException from .exceptions import AuthenticationException
from .models import ( from .models import (
ApplicationCSPPayload,
ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
@ -18,6 +17,8 @@ from .models import (
BillingProfileTenantAccessCSPResult, BillingProfileTenantAccessCSPResult,
BillingProfileVerificationCSPPayload, BillingProfileVerificationCSPPayload,
BillingProfileVerificationCSPResult, BillingProfileVerificationCSPResult,
KeyVaultCredentials,
ManagementGroupCSPResponse,
TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPPayload,
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
@ -26,6 +27,7 @@ from .models import (
TenantCSPResult, TenantCSPResult,
) )
from .policy import AzurePolicyManager from .policy import AzurePolicyManager
from atst.utils import sha256_hex
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI
@ -47,6 +49,7 @@ class AzureSDKProvider(object):
import azure.common.credentials as credentials import azure.common.credentials as credentials
import azure.identity as identity import azure.identity as identity
from azure.keyvault import secrets from azure.keyvault import secrets
from azure.core import exceptions
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
import adal import adal
@ -85,7 +88,7 @@ class AzureCloudProvider(CloudProviderInterface):
def set_secret(self, secret_key, secret_value): def set_secret(self, secret_key, secret_value):
credential = self._get_client_secret_credential_obj({}) credential = self._get_client_secret_credential_obj({})
secret_client = self.secrets.SecretClient( secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential, vault_url=self.vault_url, credential=credential,
) )
try: try:
@ -98,7 +101,7 @@ class AzureCloudProvider(CloudProviderInterface):
def get_secret(self, secret_key): def get_secret(self, secret_key):
credential = self._get_client_secret_credential_obj({}) credential = self._get_client_secret_credential_obj({})
secret_client = self.secrets.SecretClient( secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential, vault_url=self.vault_url, credential=credential,
) )
try: try:
@ -109,9 +112,7 @@ class AzureCloudProvider(CloudProviderInterface):
exc_info=1, exc_info=1,
) )
def create_environment( def create_environment(self, auth_credentials: Dict, user, environment):
self, auth_credentials: Dict, user: User, environment: Environment
):
# since this operation would only occur within a tenant, should we source the tenant # since this operation would only occur within a tenant, should we source the tenant
# via lookup from environment once we've created the portfolio csp data schema # via lookup from environment once we've created the portfolio csp data schema
# something like this: # something like this:
@ -128,7 +129,7 @@ class AzureCloudProvider(CloudProviderInterface):
credentials, management_group_id, display_name, parent_id, credentials, management_group_id, display_name, parent_id,
) )
return management_group return ManagementGroupCSPResponse(**management_group)
def create_atat_admin_user( def create_atat_admin_user(
self, auth_credentials: Dict, csp_environment_id: str self, auth_credentials: Dict, csp_environment_id: str
@ -167,16 +168,26 @@ class AzureCloudProvider(CloudProviderInterface):
"role_name": role_assignment_id, "role_name": role_assignment_id,
} }
def _create_application(self, auth_credentials: Dict, application: Application): def create_application(self, payload: ApplicationCSPPayload):
management_group_name = str(uuid4()) # can be anything, not just uuid creds = self._source_creds(payload.tenant_id)
display_name = application.name # Does this need to be unique? credentials = self._get_credential_obj(
credentials = self._get_credential_obj(auth_credentials) {
parent_id = "?" # application.portfolio.csp_details.management_group_id "client_id": creds.root_sp_client_id,
"secret_key": creds.root_sp_key,
return self._create_management_group( "tenant_id": creds.root_tenant_id,
credentials, management_group_name, display_name, parent_id, },
resource=AZURE_MANAGEMENT_API,
) )
response = self._create_management_group(
credentials,
payload.management_group_name,
payload.display_name,
payload.parent_id,
)
return ApplicationCSPResult(**response)
def _create_management_group( def _create_management_group(
self, credentials, management_group_id, display_name, parent_id=None, self, credentials, management_group_id, display_name, parent_id=None,
): ):
@ -198,6 +209,9 @@ class AzureCloudProvider(CloudProviderInterface):
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create # result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally # since we were told it could take 10+ minutes to complete, unless this handles that polling internally
# TODO: what to do is status is not 'Succeeded' on the
# response object? Will it always raise its own error
# instead?
return create_request.result() return create_request.result()
def _create_subscription( def _create_subscription(
@ -290,6 +304,7 @@ class AzureCloudProvider(CloudProviderInterface):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_sp_token(payload.creds)
if sp_token is None: if sp_token is None:
raise AuthenticationException("Could not resolve token for tenant creation") raise AuthenticationException("Could not resolve token for tenant creation")
payload.password = token_urlsafe(16) payload.password = token_urlsafe(16)
create_tenant_body = payload.dict(by_alias=True) create_tenant_body = payload.dict(by_alias=True)
@ -626,3 +641,24 @@ class AzureCloudProvider(CloudProviderInterface):
"secret_key": self.secret_key, "secret_key": self.secret_key,
"tenant_id": self.tenant_id, "tenant_id": self.tenant_id,
} }
def _source_creds(self, tenant_id=None) -> KeyVaultCredentials:
if tenant_id:
return self._source_tenant_creds(tenant_id)
else:
return KeyVaultCredentials(
root_tenant_id=self._root_creds.get("tenant_id"),
root_sp_client_id=self._root_creds.get("client_id"),
root_sp_key=self._root_creds.get("secret_key"),
)
def update_tenant_creds(self, tenant_id, secret):
hashed = sha256_hex(tenant_id)
self.set_secret(hashed, json.dumps(secret))
return secret
def _source_tenant_creds(self, tenant_id):
hashed = sha256_hex(tenant_id)
raw_creds = self.get_secret(hashed)
return KeyVaultCredentials(**json.loads(raw_creds))

View File

@ -1,9 +1,5 @@
from typing import Dict from typing import Dict
from atst.models.user import User
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
class CloudProviderInterface: class CloudProviderInterface:
def set_secret(self, secret_key: str, secret_value: str): def set_secret(self, secret_key: str, secret_value: str):
@ -15,9 +11,7 @@ class CloudProviderInterface:
def root_creds(self) -> Dict: def root_creds(self) -> Dict:
raise NotImplementedError() raise NotImplementedError()
def create_environment( def create_environment(self, auth_credentials: Dict, user, environment) -> str:
self, auth_credentials: Dict, user: User, environment: Environment
) -> str:
"""Create a new environment in the CSP. """Create a new environment in the CSP.
Arguments: Arguments:
@ -65,7 +59,7 @@ class CloudProviderInterface:
raise NotImplementedError() raise NotImplementedError()
def create_or_update_user( def create_or_update_user(
self, auth_credentials: Dict, user_info: EnvironmentRole, csp_role_id: str self, auth_credentials: Dict, user_info, csp_role_id: str
) -> str: ) -> str:
"""Creates a user or updates an existing user's role. """Creates a user or updates an existing user's role.

View File

@ -17,6 +17,9 @@ from .exceptions import (
UnknownServerException, UnknownServerException,
) )
from .models import ( from .models import (
AZURE_MGMNT_PATH,
ApplicationCSPPayload,
ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
@ -340,3 +343,16 @@ class MockCloudProvider(CloudProviderInterface):
self._delay(1, 5) self._delay(1, 5)
if self._with_authorization and credentials != self._auth_credentials: if self._with_authorization and credentials != self._auth_credentials:
raise self.AUTHENTICATION_EXCEPTION raise self.AUTHENTICATION_EXCEPTION
def create_application(self, payload: ApplicationCSPPayload):
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
return ApplicationCSPResult(
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
)
def get_credentials(self, scope="portfolio", tenant_id=None):
return self.root_creds()
def update_tenant_creds(self, tenant_id, secret):
return secret

View File

@ -1,6 +1,8 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
import re
from uuid import uuid4
from pydantic import BaseModel, validator from pydantic import BaseModel, validator, root_validator
from atst.utils import snake_to_camel from atst.utils import snake_to_camel
@ -232,3 +234,110 @@ class BillingInstructionCSPResult(AliasModel):
fields = { fields = {
"reported_clin_name": "name", "reported_clin_name": "name",
} }
AZURE_MGMNT_PATH = "/providers/Microsoft.Management/managementGroups/"
MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$"
class ManagementGroupCSPPayload(AliasModel):
"""
:param: management_group_name: Just pass a UUID for this.
:param: display_name: This can contain any character and
spaces, but should be 90 characters or fewer long.
:param: parent_id: This should be the fully qualified Azure ID,
i.e. /providers/Microsoft.Management/managementGroups/[management group ID]
"""
tenant_id: str
management_group_name: Optional[str]
display_name: str
parent_id: str
@validator("management_group_name", pre=True, always=True)
def supply_management_group_name_default(cls, name):
if name:
if re.match(MANAGEMENT_GROUP_NAME_REGEX, name) is None:
raise ValueError(
f"Management group name must match {MANAGEMENT_GROUP_NAME_REGEX}"
)
return name[0:90]
else:
return str(uuid4())
@validator("display_name", pre=True, always=True)
def enforce_display_name_length(cls, name):
return name[0:90]
@validator("parent_id", pre=True, always=True)
def enforce_parent_id_pattern(cls, id_):
if AZURE_MGMNT_PATH not in id_:
return f"{AZURE_MGMNT_PATH}{id_}"
else:
return id_
class ManagementGroupCSPResponse(AliasModel):
id: str
class ApplicationCSPPayload(ManagementGroupCSPPayload):
pass
class ApplicationCSPResult(ManagementGroupCSPResponse):
pass
class KeyVaultCredentials(BaseModel):
root_sp_client_id: Optional[str]
root_sp_key: Optional[str]
root_tenant_id: Optional[str]
tenant_id: Optional[str]
tenant_admin_username: Optional[str]
tenant_admin_password: Optional[str]
tenant_sp_client_id: Optional[str]
tenant_sp_key: Optional[str]
@root_validator(pre=True)
def enforce_admin_creds(cls, values):
tenant_id = values.get("tenant_id")
username = values.get("tenant_admin_username")
password = values.get("tenant_admin_password")
if any([username, password]) and not all([tenant_id, username, password]):
raise ValueError(
"tenant_id, tenant_admin_username, and tenant_admin_password must all be set if any one is"
)
return values
@root_validator(pre=True)
def enforce_sp_creds(cls, values):
tenant_id = values.get("tenant_id")
client_id = values.get("tenant_sp_client_id")
key = values.get("tenant_sp_key")
if any([client_id, key]) and not all([tenant_id, client_id, key]):
raise ValueError(
"tenant_id, tenant_sp_client_id, and tenant_sp_key must all be set if any one is"
)
return values
@root_validator(pre=True)
def enforce_root_creds(cls, values):
sp_creds = [
values.get("root_tenant_id"),
values.get("root_sp_client_id"),
values.get("root_sp_key"),
]
if any(sp_creds) and not all(sp_creds):
raise ValueError(
"root_tenant_id, root_sp_client_id, and root_sp_key must all be set if any one is"
)
return values

View File

@ -93,10 +93,13 @@ class Users(object):
return user return user
@classmethod @classmethod
def give_ccpo_perms(cls, user): def give_ccpo_perms(cls, user, commit=True):
user.permission_sets = PermissionSets.get_all() user.permission_sets = PermissionSets.get_all()
db.session.add(user) db.session.add(user)
db.session.commit()
if commit:
db.session.commit()
return user return user
@classmethod @classmethod

View File

@ -10,11 +10,13 @@ from wtforms.fields.html5 import DateField
from wtforms.validators import ( from wtforms.validators import (
Required, Required,
Length, Length,
Optional,
NumberRange, NumberRange,
ValidationError, ValidationError,
) )
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import numbers import numbers
from atst.forms.validators import Number, AlphaNumeric from atst.forms.validators import Number, AlphaNumeric
from .data import JEDI_CLIN_TYPES from .data import JEDI_CLIN_TYPES
@ -60,6 +62,14 @@ def validate_date_in_range(form, field):
) )
def remove_dashes(value):
return value.replace("-", "") if value else None
def coerce_upper(value):
return value.upper() if value else None
class CLINForm(FlaskForm): class CLINForm(FlaskForm):
jedi_clin_type = SelectField( jedi_clin_type = SelectField(
translate("task_orders.form.clin_type_label"), translate("task_orders.form.clin_type_label"),
@ -149,8 +159,8 @@ class AttachmentForm(BaseForm):
class TaskOrderForm(BaseForm): class TaskOrderForm(BaseForm):
number = StringField( number = StringField(
label=translate("forms.task_order.number_description"), label=translate("forms.task_order.number_description"),
filters=[remove_empty_string], filters=[remove_empty_string, remove_dashes, coerce_upper],
validators=[Number(), Length(max=13)], validators=[AlphaNumeric(), Length(min=13, max=17), Optional()],
) )
pdf = FormField( pdf = FormField(
AttachmentForm, AttachmentForm,

View File

@ -3,47 +3,38 @@ import pendulum
from atst.database import db from atst.database import db
from atst.queue import celery from atst.queue import celery
from atst.models import ( from atst.models import EnvironmentRole, JobFailure
EnvironmentJobFailure,
EnvironmentRoleJobFailure,
EnvironmentRole,
PortfolioJobFailure,
)
from atst.domain.csp.cloud.exceptions import GeneralCSPException from atst.domain.csp.cloud.exceptions import GeneralCSPException
from atst.domain.csp.cloud import CloudProviderInterface from atst.domain.csp.cloud import CloudProviderInterface
from atst.domain.applications import Applications
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.models.utils import claim_for_update from atst.models.utils import claim_for_update
from atst.utils.localization import translate from atst.utils.localization import translate
from atst.domain.csp.cloud.models import ApplicationCSPPayload
class RecordPortfolioFailure(celery.Task): class RecordFailure(celery.Task):
_ENTITIES = [
"portfolio_id",
"application_id",
"environment_id",
"environment_role_id",
]
def _derive_entity_info(self, kwargs):
matches = [e for e in self._ENTITIES if e in kwargs.keys()]
if matches:
match = matches[0]
return {"entity": match.replace("_id", ""), "entity_id": kwargs[match]}
else:
return None
def on_failure(self, exc, task_id, args, kwargs, einfo): def on_failure(self, exc, task_id, args, kwargs, einfo):
if "portfolio_id" in kwargs: info = self._derive_entity_info(kwargs)
failure = PortfolioJobFailure( if info:
portfolio_id=kwargs["portfolio_id"], task_id=task_id failure = JobFailure(**info, task_id=task_id)
)
db.session.add(failure)
db.session.commit()
class RecordEnvironmentFailure(celery.Task):
def on_failure(self, exc, task_id, args, kwargs, einfo):
if "environment_id" in kwargs:
failure = EnvironmentJobFailure(
environment_id=kwargs["environment_id"], task_id=task_id
)
db.session.add(failure)
db.session.commit()
class RecordEnvironmentRoleFailure(celery.Task):
def on_failure(self, exc, task_id, args, kwargs, einfo):
if "environment_role_id" in kwargs:
failure = EnvironmentRoleJobFailure(
environment_role_id=kwargs["environment_role_id"], task_id=task_id
)
db.session.add(failure) db.session.add(failure)
db.session.commit() db.session.commit()
@ -63,6 +54,27 @@ def send_notification_mail(recipients, subject, body):
app.mailer.send(recipients, subject, body) app.mailer.send(recipients, subject, body)
def do_create_application(csp: CloudProviderInterface, application_id=None):
application = Applications.get(application_id)
with claim_for_update(application) as application:
if application.cloud_id:
return
csp_details = application.portfolio.csp_data
parent_id = csp_details.get("root_management_group_id")
tenant_id = csp_details.get("tenant_id")
payload = ApplicationCSPPayload(
tenant_id=tenant_id, display_name=application.name, parent_id=parent_id
)
app_result = csp.create_application(payload)
application.cloud_id = app_result.id
db.session.add(application)
db.session.commit()
def do_create_environment(csp: CloudProviderInterface, environment_id=None): def do_create_environment(csp: CloudProviderInterface, environment_id=None):
environment = Environments.get(environment_id) environment = Environments.get(environment_id)
@ -144,17 +156,22 @@ def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
fsm.trigger_next_transition() fsm.trigger_next_transition()
@celery.task(bind=True, base=RecordPortfolioFailure) @celery.task(bind=True, base=RecordFailure)
def provision_portfolio(self, portfolio_id=None): def provision_portfolio(self, portfolio_id=None):
do_work(do_provision_portfolio, self, app.csp.cloud, portfolio_id=portfolio_id) do_work(do_provision_portfolio, self, app.csp.cloud, portfolio_id=portfolio_id)
@celery.task(bind=True, base=RecordEnvironmentFailure) @celery.task(bind=True, base=RecordFailure)
def create_application(self, application_id=None):
do_work(do_create_application, self, app.csp.cloud, application_id=application_id)
@celery.task(bind=True, base=RecordFailure)
def create_environment(self, environment_id=None): def create_environment(self, environment_id=None):
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id) do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id)
@celery.task(bind=True, base=RecordEnvironmentFailure) @celery.task(bind=True, base=RecordFailure)
def create_atat_admin_user(self, environment_id=None): def create_atat_admin_user(self, environment_id=None):
do_work( do_work(
do_create_atat_admin_user, self, app.csp.cloud, environment_id=environment_id do_create_atat_admin_user, self, app.csp.cloud, environment_id=environment_id
@ -177,6 +194,12 @@ def dispatch_provision_portfolio(self):
provision_portfolio.delay(portfolio_id=portfolio_id) provision_portfolio.delay(portfolio_id=portfolio_id)
@celery.task(bind=True)
def dispatch_create_application(self):
for application_id in Applications.get_applications_pending_creation():
create_application.delay(application_id=application_id)
@celery.task(bind=True) @celery.task(bind=True)
def dispatch_create_environment(self): def dispatch_create_environment(self):
for environment_id in Environments.get_environments_pending_creation( for environment_id in Environments.get_environments_pending_creation(

View File

@ -7,11 +7,7 @@ from .audit_event import AuditEvent
from .clin import CLIN, JEDICLINType from .clin import CLIN, JEDICLINType
from .environment import Environment from .environment import Environment
from .environment_role import EnvironmentRole, CSPRole from .environment_role import EnvironmentRole, CSPRole
from .job_failure import ( from .job_failure import JobFailure
EnvironmentJobFailure,
EnvironmentRoleJobFailure,
PortfolioJobFailure,
)
from .notification_recipient import NotificationRecipient from .notification_recipient import NotificationRecipient
from .permissions import Permissions from .permissions import Permissions
from .permission_set import PermissionSet from .permission_set import PermissionSet

View File

@ -1,4 +1,4 @@
from sqlalchemy import and_, Column, ForeignKey, String, UniqueConstraint from sqlalchemy import and_, Column, ForeignKey, String, UniqueConstraint, TIMESTAMP
from sqlalchemy.orm import relationship, synonym from sqlalchemy.orm import relationship, synonym
from atst.models.base import Base from atst.models.base import Base
@ -40,6 +40,9 @@ class Application(
), ),
) )
cloud_id = Column(String)
claimed_until = Column(TIMESTAMP(timezone=True))
@property @property
def users(self): def users(self):
return set(role.user for role in self.members) return set(role.user for role in self.members)

View File

@ -30,8 +30,6 @@ class Environment(
claimed_until = Column(TIMESTAMP(timezone=True)) claimed_until = Column(TIMESTAMP(timezone=True))
job_failures = relationship("EnvironmentJobFailure")
roles = relationship( roles = relationship(
"EnvironmentRole", "EnvironmentRole",
back_populates="environment", back_populates="environment",

View File

@ -32,8 +32,6 @@ class EnvironmentRole(
) )
application_role = relationship("ApplicationRole") application_role = relationship("ApplicationRole")
job_failures = relationship("EnvironmentRoleJobFailure")
csp_user_id = Column(String()) csp_user_id = Column(String())
claimed_until = Column(TIMESTAMP(timezone=True)) claimed_until = Column(TIMESTAMP(timezone=True))

View File

@ -1,22 +1,21 @@
from sqlalchemy import Column, ForeignKey from celery.result import AsyncResult
from sqlalchemy import Column, String, Integer
from atst.models.base import Base from atst.models.base import Base
import atst.models.mixins as mixins import atst.models.mixins as mixins
class EnvironmentJobFailure(Base, mixins.JobFailureMixin): class JobFailure(Base, mixins.TimestampsMixin):
__tablename__ = "environment_job_failures" __tablename__ = "job_failures"
environment_id = Column(ForeignKey("environments.id"), nullable=False) id = Column(Integer(), primary_key=True)
task_id = Column(String(), nullable=False)
entity = Column(String(), nullable=False)
entity_id = Column(String(), nullable=False)
@property
def task(self):
if not hasattr(self, "_task"):
self._task = AsyncResult(self.task_id)
class EnvironmentRoleJobFailure(Base, mixins.JobFailureMixin): return self._task
__tablename__ = "environment_role_job_failures"
environment_role_id = Column(ForeignKey("environment_roles.id"), nullable=False)
class PortfolioJobFailure(Base, mixins.JobFailureMixin):
__tablename__ = "portfolio_job_failures"
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)

View File

@ -3,5 +3,4 @@ from .auditable import AuditableMixin
from .permissions import PermissionsMixin from .permissions import PermissionsMixin
from .deletable import DeletableMixin from .deletable import DeletableMixin
from .invites import InvitesMixin from .invites import InvitesMixin
from .job_failure import JobFailureMixin
from .state_machines import FSMMixin from .state_machines import FSMMixin

View File

@ -1,14 +0,0 @@
from celery.result import AsyncResult
from sqlalchemy import Column, String, Integer
class JobFailureMixin(object):
id = Column(Integer(), primary_key=True)
task_id = Column(String(), nullable=False)
@property
def task(self):
if not hasattr(self, "_task"):
self._task = AsyncResult(self.task_id)
return self._task

View File

@ -175,7 +175,7 @@ class PortfolioStateMachine(
tenant_id = new_creds.get("tenant_id") tenant_id = new_creds.get("tenant_id")
secret = self.csp.get_secret(tenant_id, new_creds) secret = self.csp.get_secret(tenant_id, new_creds)
secret.update(new_creds) secret.update(new_creds)
self.csp.set_secret(tenant_id, secret) self.csp.update_tenant_creds(tenant_id, secret)
except PydanticValidationError as exc: except PydanticValidationError as exc:
app.logger.error( app.logger.error(
f"Failed to cast response to valid result class {self.__repr__()}:", f"Failed to cast response to valid result class {self.__repr__()}:",

View File

@ -11,6 +11,10 @@ def update_celery(celery, app):
"task": "atst.jobs.dispatch_provision_portfolio", "task": "atst.jobs.dispatch_provision_portfolio",
"schedule": 60, "schedule": 60,
}, },
"beat-dispatch_create_application": {
"task": "atst.jobs.dispatch_create_application",
"schedule": 60,
},
"beat-dispatch_create_environment": { "beat-dispatch_create_environment": {
"task": "atst.jobs.dispatch_create_environment", "task": "atst.jobs.dispatch_create_environment",
"schedule": 60, "schedule": 60,

View File

@ -1,3 +1,4 @@
import hashlib
import re import re
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@ -41,3 +42,8 @@ def commit_or_raise_already_exists_error(message):
except IntegrityError: except IntegrityError:
db.session.rollback() db.session.rollback()
raise AlreadyExistsError(message) raise AlreadyExistsError(message)
def sha256_hex(string):
hsh = hashlib.sha256(string.encode())
return hsh.digest().hex()

View File

@ -43,6 +43,7 @@ SERVER_NAME
SESSION_COOKIE_NAME=atat SESSION_COOKIE_NAME=atat
SESSION_COOKIE_DOMAIN SESSION_COOKIE_DOMAIN
SESSION_KEY_PREFIX=session: SESSION_KEY_PREFIX=session:
SESSION_COOKIE_SECURE=false
SESSION_TYPE = redis SESSION_TYPE = redis
SESSION_USE_SIGNER = True SESSION_USE_SIGNER = True
SQLALCHEMY_ECHO = False SQLALCHEMY_ECHO = False

View File

@ -32,6 +32,7 @@ data:
REDIS_HOST: atat.redis.cache.windows.net:6380 REDIS_HOST: atat.redis.cache.windows.net:6380
REDIS_TLS: "true" REDIS_TLS: "true"
SESSION_COOKIE_DOMAIN: atat.code.mil SESSION_COOKIE_DOMAIN: atat.code.mil
SESSION_COOKIE_SECURE: "true"
STATIC_URL: https://atat-cdn.azureedge.net/static/ STATIC_URL: https://atat-cdn.azureedge.net/static/
TZ: UTC TZ: UTC
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini

View File

@ -29,6 +29,8 @@ spec:
containers: containers:
- name: atst - name: atst
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
securityContext:
allowPrivilegeEscalation: false
env: env:
- name: UWSGI_PROCESSES - name: UWSGI_PROCESSES
value: "2" value: "2"
@ -64,6 +66,8 @@ spec:
cpu: 940m cpu: 940m
- name: nginx - name: nginx
image: nginx:alpine image: nginx:alpine
securityContext:
allowPrivilegeEscalation: false
ports: ports:
- containerPort: 8342 - containerPort: 8342
name: main-upgrade name: main-upgrade
@ -189,6 +193,8 @@ spec:
containers: containers:
- name: atst-worker - name: atst-worker
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
securityContext:
allowPrivilegeEscalation: false
args: args:
[ [
"/opt/atat/atst/.venv/bin/python", "/opt/atat/atst/.venv/bin/python",
@ -261,6 +267,8 @@ spec:
containers: containers:
- name: atst-beat - name: atst-beat
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
securityContext:
allowPrivilegeEscalation: false
args: args:
[ [
"/opt/atat/atst/.venv/bin/python", "/opt/atat/atst/.venv/bin/python",

View File

@ -20,6 +20,8 @@ spec:
containers: containers:
- name: crls - name: crls
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
securityContext:
allowPrivilegeEscalation: false
command: [ command: [
"/bin/sh", "-c" "/bin/sh", "-c"
] ]

View File

@ -16,6 +16,8 @@ spec:
containers: containers:
- name: migration - name: migration
image: $CONTAINER_IMAGE image: $CONTAINER_IMAGE
securityContext:
allowPrivilegeEscalation: false
command: [ command: [
"/bin/sh", "-c" "/bin/sh", "-c"
] ]

View File

@ -0,0 +1,98 @@
import { mount } from '@vue/test-utils'
import textinput from '../text_input'
import { makeTestWrapper } from '../../test_utils/component_test_helpers'
const ToNumberWrapperComponent = makeTestWrapper({
components: {
textinput,
},
templatePath: 'text_input_to_number.html',
data: function() {
const { validation, initialValue } = this.initialData
return { validation, initialValue }
},
})
describe('TextInput Validates Correctly', () => {
describe('taskOrderNumber validator', () => {
it('Should initialize with the validator and no validation icon', () => {
const wrapper = mount(ToNumberWrapperComponent, {
propsData: {
name: 'testTextInput',
initialData: {
validation: 'taskOrderNumber',
},
},
})
expect(wrapper.contains('.usa-input--success')).toBe(false)
expect(wrapper.contains('.usa-input--error')).toBe(false)
expect(wrapper.contains('.usa-input--validation--taskOrderNumber')).toBe(
true
)
})
it('Should allow valid TO numbers', () => {
const wrapper = mount(ToNumberWrapperComponent, {
propsData: {
name: 'testTextInput',
initialData: {
validation: 'taskOrderNumber',
},
},
})
var textInputField = wrapper.find('input[id="number"]')
var hiddenField = wrapper.find('input[name="number"]')
const validToNumbers = [
'12345678901234567',
'1234567890123',
'abc1234567890', // pragma: allowlist secret
'abc-1234567890',
'DC12-123-1234567890',
'fg34-987-1234567890',
]
for (const number of validToNumbers) {
// set value to be a valid TO number
textInputField.setValue(number)
// manually trigger change event in hidden fields
hiddenField.trigger('change')
// check for validation classes
expect(wrapper.contains('.usa-input--success')).toBe(true)
expect(wrapper.contains('.usa-input--error')).toBe(false)
}
})
it('Should not allow invalid TO numbers', () => {
const wrapper = mount(ToNumberWrapperComponent, {
propsData: {
name: 'testTextInput',
initialData: {
validation: 'taskOrderNumber',
},
},
})
var textInputField = wrapper.find('input[id="number"]')
var hiddenField = wrapper.find('input[name="number"]')
const invalidToNumbers = [
'1234567890',
'12345678901234567890', // pragma: allowlist secret
'123:4567890123',
'123_1234567890',
]
for (const number of invalidToNumbers) {
// set value to be a valid TO number
textInputField.setValue(number)
// manually trigger change event in hidden fields
hiddenField.trigger('change')
// check for validation classes
expect(wrapper.contains('.usa-input--success')).toBe(false)
expect(wrapper.contains('.usa-input--error')).toBe(true)
}
})
})
})

View File

@ -106,9 +106,9 @@ export default {
}, },
taskOrderNumber: { taskOrderNumber: {
mask: false, mask: false,
match: /^.{13}$/, match: /(^[0-9a-zA-Z]{13,17}$)/,
unmask: [], unmask: ['-'],
validationError: 'TO number must be 13 digits', validationError: 'TO number must be between 13 and 17 characters',
}, },
usPhone: { usPhone: {
mask: [ mask: [

41
script/create_database.py Normal file
View File

@ -0,0 +1,41 @@
# Add root application dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
import sqlalchemy
from atst.app import make_config
def _root_connection(config, root_db):
# Assemble DATABASE_URI value
database_uri = "postgresql://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
config.get("PGUSER"),
config.get("PGPASSWORD"),
config.get("PGHOST"),
config.get("PGPORT"),
root_db,
)
engine = sqlalchemy.create_engine(database_uri)
return engine.connect()
def create_database(conn, dbname):
conn.execute("commit")
conn.execute(f"CREATE DATABASE {dbname};")
conn.close()
return True
if __name__ == "__main__":
dbname = sys.argv[1]
config = make_config()
conn = _root_connection(config, "postgres")
print(f"Creating database {dbname}")
create_database(conn, dbname)

76
script/database_setup.py Normal file
View File

@ -0,0 +1,76 @@
# Add root application dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
import sqlalchemy
import yaml
from atst.app import make_config, make_app
from atst.database import db
from atst.domain.users import Users
from atst.models import User
from reset_database import reset_database
def database_setup(username, password, dbname, ccpo_users):
print(
f"Creating Postgres user role for '{username}' and granting all privileges to database '{dbname}'."
)
try:
_create_database_user(username, password, dbname)
except sqlalchemy.exc.ProgrammingError as err:
print(f"Postgres user role '{username}' already exists.")
print("Applying schema and seeding roles and permissions.")
reset_database()
print("Creating initial set of CCPO users.")
_add_ccpo_users(ccpo_users)
def _create_database_user(username, password, dbname):
conn = db.engine.connect()
meta = sqlalchemy.MetaData(bind=conn)
meta.reflect()
trans = conn.begin()
engine = trans.connection.engine
engine.execute(
f"CREATE ROLE {username} WITH LOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION PASSWORD '{password}';\n"
f"GRANT ALL PRIVILEGES ON DATABASE {dbname} TO {username};\n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {username}; \n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO {username}; \n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO {username}; \n"
)
trans.commit()
def _add_ccpo_users(ccpo_users):
for user_data in ccpo_users:
user = User(**user_data)
Users.give_ccpo_perms(user, commit=False)
db.session.add(user)
db.session.commit()
def _load_yaml(file_):
with open(file_) as f:
return yaml.safe_load(f)
if __name__ == "__main__":
config = make_config({"DISABLE_CRL_CHECK": True, "DEBUG": False})
app = make_app(config)
with app.app_context():
dbname = config.get("PGDATABASE", "atat")
username = sys.argv[1]
password = sys.argv[2]
ccpo_user_file = sys.argv[3]
ccpo_users = _load_yaml(ccpo_user_file)
database_setup(username, password, dbname, ccpo_users)

View File

@ -16,7 +16,9 @@ from atst.app import make_config, make_app
def reset_database(): def reset_database():
conn = db.engine.connect() conn = db.engine.connect()
meta = sqlalchemy.MetaData(bind=conn, reflect=True) meta = sqlalchemy.MetaData(bind=conn)
meta.reflect()
trans = conn.begin() trans = conn.begin()
# drop all tables # drop all tables

1
static/icons/clock.svg Normal file
View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="clock" class="svg-inline--fa fa-clock fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"></path></svg>

After

Width:  |  Height:  |  Size: 554 B

1
static/icons/user.svg Normal file
View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" x="0px" y="0px"><path d="M16 16h-2v-3.225l-3.919-.781c-.626-.125-1.081-.68-1.081-1.319v-1.433c0-.477.236-.921.631-1.187.288-.195 1.369-1.46 1.369-3.055 0-1.853-1.558-3-3-3-1.449 0-3 1.206-3 3 0 1.596 1.081 2.859 1.371 3.056.395.268.629.711.629 1.186v1.433c0 .64-.455 1.194-1.083 1.319l-3.916.783-.001 3.223h-2v-3.221c0-.951.677-1.776 1.609-1.963l3.391-.677v-.623c-.765-.677-2-2.38-2-4.516 0-3.088 2.595-5 5-5 2.757 0 5 2.243 5 5 0 2.134-1.234 3.837-2 4.516v.623l3.396.679c.929.187 1.604 1.01 1.604 1.957v3.225z"></path></svg>

After

Width:  |  Height:  |  Size: 664 B

View File

@ -1,8 +1,6 @@
.empty-state { .empty-state {
padding: $gap * 3; max-width: $max-panel-width;
max-width: 100%;
background-color: $color-gray-lightest; background-color: $color-gray-lightest;
margin-top: $gap * 5;
&--white { &--white {
background-color: $color-white; background-color: $color-white;
@ -18,17 +16,28 @@
margin-top: 3rem; margin-top: 3rem;
} }
h3 {
margin: 0 0 1rem;
padding: 3.2rem 2.4rem 0;
}
p {
margin: 0;
padding: 0 $gap * 3;
}
hr { hr {
margin-left: -$gap * 3; margin: $gap * 4 0 0;
margin-right: -$gap * 3;
} }
&__footer { &__footer {
text-align: center; text-align: center;
background-color: $color-gray-lightest;
padding: $gap * 3;
a.usa-button { a.usa-button {
width: 60%; width: 60%;
display: inline-block; margin: 0 auto;
} }
} }
} }

View File

@ -3,9 +3,7 @@
background-color: $color-white; background-color: $color-white;
border-top: 1px solid $color-gray-lightest; border-top: 1px solid $color-gray-lightest;
display: flex; display: flex;
flex-direction: row-reverse;
align-items: center; align-items: center;
padding: $gap * 1.5;
position: fixed; position: fixed;
left: 0; left: 0;
bottom: 0; bottom: 0;
@ -13,8 +11,11 @@
height: $footer-height; height: $footer-height;
color: $color-gray-dark; color: $color-gray-dark;
font-size: 1.5rem; font-size: 1.5rem;
padding: 0 $gap * 1.5;
&__login { &__login {
padding-left: 0.8rem; width: 100%;
max-width: 1175px;
text-align: right;
} }
} }

View File

@ -22,15 +22,18 @@ body {
padding-bottom: $footer-height * 2.5; padding-bottom: $footer-height * 2.5;
.global-panel-container { .global-panel-container {
margin: $gap;
flex-grow: 1; flex-grow: 1;
-ms-flex-negative: 1; -ms-flex-negative: 1;
top: $usa-banner-height + $topbar-height; top: $usa-banner-height + $topbar-height;
position: relative; position: relative;
padding: 0 $large-spacing;
@include media($medium-screen) { @include media($medium-screen) {
margin: $gap * 2;
top: $usa-banner-height + $topbar-height; top: $usa-banner-height + $topbar-height;
} }
.user-edit {
max-width: $max-panel-width;
}
} }
} }

View File

@ -3,26 +3,34 @@
@include grid-row; @include grid-row;
min-height: 500px; min-height: 500px;
} }
}
margin-left: 2 * $gap; .portfolio-header-new .portfolio-header__name {
padding: 1.6rem 0;
} }
.portfolio-header { .portfolio-header {
flex-direction: column; flex-direction: column;
margin: $gap * 2 0;
max-width: $max-panel-width;
@include media($small-screen) { @include media($small-screen) {
flex-direction: row; flex-direction: row;
} }
margin-bottom: $gap * 1;
.col--grow { .col--grow {
overflow: inherit; overflow: inherit;
display: table;
min-height: 10rem;
} }
&__name { &__name {
@include h1; @include h1;
display: table-cell;
vertical-align: middle;
h1 { h1 {
margin: 0 $gap ($gap * 2) 0; margin: 0;
font-size: 3.5rem; font-size: 3.5rem;
} }
@ -30,6 +38,7 @@
font-size: $small-font-size; font-size: $small-font-size;
margin: 0 0 (-$gap * 0.5); margin: 0 0 (-$gap * 0.5);
color: $color-gray-medium; color: $color-gray-medium;
max-width: 100%;
} }
} }
@ -38,9 +47,15 @@
font-size: $small-font-size; font-size: $small-font-size;
.icon-link { .icon-link {
padding: $gap; padding: 0;
border-radius: 0; border-radius: 0;
color: $color-blue-darkest; color: $color-blue-darkest;
min-width: 10rem;
min-height: 10rem;
.col {
margin: 0 auto;
}
&:hover { &:hover {
background-color: $color-aqua-lightest; background-color: $color-aqua-lightest;
@ -53,6 +68,7 @@
&.active { &.active {
color: $color-blue; color: $color-blue;
background-color: $color-gray-lightest; background-color: $color-gray-lightest;
text-decoration: none;
&:hover { &:hover {
background-color: $color-aqua-lightest; background-color: $color-aqua-lightest;
@ -82,11 +98,19 @@
margin-bottom: 3 * $gap; margin-bottom: 3 * $gap;
} }
.portfolio-content { .portfolio-admin {
margin: (4 * $gap) $gap 0 $gap; margin: $large-spacing 0;
max-width: $max-panel-width;
}
.portfolio-content {
.panel { .panel {
padding-bottom: 2rem; padding-bottom: 2rem;
max-width: $max-panel-width;
}
hr {
max-width: $max-panel-width;
} }
a.add-new-button { a.add-new-button {
@ -251,6 +275,7 @@
.portfolio-applications { .portfolio-applications {
margin-top: $gap * 5; margin-top: $gap * 5;
max-width: $max-panel-width;
&__header { &__header {
&--title { &--title {
@ -296,8 +321,8 @@
} }
.portfolio-funding { .portfolio-funding {
padding: 2 * $gap; max-width: $max-panel-width;
padding-top: 0; margin: $large-spacing 0;
.panel { .panel {
@include shadow-panel; @include shadow-panel;
@ -366,6 +391,8 @@
} }
.portfolio-reports { .portfolio-reports {
max-width: $max-panel-width;
&__header { &__header {
margin-bottom: 4 * $gap; margin-bottom: 4 * $gap;

View File

@ -20,12 +20,10 @@
.sticky-cta-container { .sticky-cta-container {
display: flex; display: flex;
align-items: center; align-items: center;
max-width: 90rem;
.usa-button { .usa-button {
margin: $gap $gap * 1.5 $gap 0; margin: 0;
width: 20rem;
height: 3.2rem;
font-size: $small-font-size;
} }
} }
@ -42,6 +40,10 @@
&-buttons { &-buttons {
display: flex; display: flex;
a {
font-size: 1.5rem;
}
.action-group { .action-group {
margin: 0; margin: 0;

View File

@ -4,14 +4,15 @@
height: $topbar-height; height: $topbar-height;
position: fixed; position: fixed;
top: $usa-banner-height; top: $usa-banner-height;
width: 100%;
z-index: 10; z-index: 10;
width: 100%;
&__navigation { &__navigation {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
justify-content: space-between; justify-content: space-between;
max-width: 1190px;
a { a {
color: $color-white; color: $color-white;
@ -64,3 +65,11 @@
justify-content: flex-end; justify-content: flex-end;
} }
} }
.login-topbar .topbar__navigation {
max-width: 100%;
}
.login-topbar .topbar__context .topbar__link-icon {
margin: 0 0 0 0.8rem;
}

View File

@ -41,7 +41,6 @@
&.col--grow { &.col--grow {
flex: 1 auto; flex: 1 auto;
padding-right: $spacing-small;
} }
&.col--half { &.col--half {

View File

@ -94,3 +94,7 @@ hr {
margin: ($gap * 3) ($site-margins * -4); margin: ($gap * 3) ($site-margins * -4);
} }
} }
.usa-section {
padding: 0;
}

View File

@ -16,8 +16,9 @@ $footer-height: 5rem;
$usa-banner-height: 2.8rem; $usa-banner-height: 2.8rem;
$sidenav-expanded-width: 25rem; $sidenav-expanded-width: 25rem;
$sidenav-collapsed-width: 10rem; $sidenav-collapsed-width: 10rem;
$max-panel-width: 80rem; $max-panel-width: 90rem;
$home-pg-icon-width: 6rem; $home-pg-icon-width: 6rem;
$large-spacing: 4rem;
/* /*
* USWDS Variables * USWDS Variables
@ -189,4 +190,4 @@ $spacing-x-small: 0.5rem;
$spacing-small: 1rem; $spacing-small: 1rem;
$spacing-md-small: 1.5rem; $spacing-md-small: 1.5rem;
$spacing-medium: 2rem; $spacing-medium: 2rem;
$spacing-large: 3rem; $spacing-large: 4rem;

View File

@ -21,7 +21,7 @@
text-transform: uppercase; text-transform: uppercase;
&--default { &--default {
background-color: $color-gray-dark; background-color: $color-gray;
} }
&--info { &--info {

View File

@ -19,10 +19,7 @@
} }
@mixin panel-margin { @mixin panel-margin {
margin-top: 0; margin: $spacing-large 0;
margin-left: 0;
margin-right: 0;
margin-bottom: $site-margins-mobile * 6;
@include media($medium-screen) { @include media($medium-screen) {
margin-bottom: $site-margins * 8; margin-bottom: $site-margins * 8;
@ -56,9 +53,10 @@
@include panel-theme-default; @include panel-theme-default;
@include panel-margin; @include panel-margin;
@include shadow-panel; @include shadow-panel;
max-width: $max-panel-width;
&__content { &__content {
padding: $gap * 2; padding: 3.2rem 2.4rem;
} }
&__body { &__body {
@ -66,7 +64,7 @@
} }
&__heading { &__heading {
padding: $gap * 2; padding: 3.2rem 2.4rem;
@include media($medium-screen) { @include media($medium-screen) {
padding: $gap * 4; padding: $gap * 4;

View File

@ -113,8 +113,8 @@
text-overflow: ellipsis; text-overflow: ellipsis;
&--active { &--active {
@include h4; font-size: $base-font-size;
font-weight: $font-bold;
background-color: $color-aqua-lightest !important; background-color: $color-aqua-lightest !important;
color: $color-primary-darker !important; color: $color-primary-darker !important;
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker; box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;

View File

@ -1,12 +1,11 @@
.home { .home {
margin: $gap * 3;
.sticky-cta { .sticky-cta {
margin: -1.6rem -1.6rem 0 -1.6rem; margin: -1.6rem -1.6rem 0 -1.6rem;
} }
&__content { &__content {
margin: 4rem; margin: $large-spacing 0;
max-width: 900px; max-width: $max-panel-width;
&--descriptions { &--descriptions {
.col { .col {
@ -29,7 +28,7 @@
background-color: $color-white; background-color: $color-white;
.home-container { .home-container {
max-width: 90rem; max-width: $max-panel-width;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 8rem; margin-bottom: 8rem;

View File

@ -1,3 +1,4 @@
{% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/label.html" import Label %} {% from "components/label.html" import Label %}
{% from 'components/save_button.html' import SaveButton %} {% from 'components/save_button.html' import SaveButton %}
@ -10,10 +11,13 @@
new_env_form) %} new_env_form) %}
<h3>{{ "portfolios.applications.settings.environments" | translate }}</h3> <h3>{{ "portfolios.applications.settings.environments" | translate }}</h3>
{% if portfolio.num_task_orders == 0 -%}
{{ Alert(message="portfolios.applications.environments.funding_alert"|translate({'name': portfolio.name})) }}
{%- endif %}
{% if g.matchesPath("application-environments") -%}
{% include "fragments/flash.html" %}
{%- endif %}
<section class="panel" id="application-environments"> <section class="panel" id="application-environments">
{% if g.matchesPath("application-environments") -%}
{% include "fragments/flash.html" %}
{%- endif %}
{% if 0 == environments_obj | length -%} {% if 0 == environments_obj | length -%}
<div class="empty-state panel__content"> <div class="empty-state panel__content">
<p class="empty-state__message"> <p class="empty-state__message">
@ -30,14 +34,21 @@
<li class="accordion-table__item"> <li class="accordion-table__item">
<div class="accordion-table__item-content"> <div class="accordion-table__item-content">
<div class="environment-list__item"> <div class="environment-list__item">
<span> {% if not env["pending"] -%}
<a <span>
href='{{ url_for("applications.access_environment", environment_id=env.id)}}' <a
target='_blank' href='{{ url_for("applications.access_environment", environment_id=env.id)}}'
rel='noopener noreferrer'> target='_blank'
{{ env['name'] }} {{ Icon('link', classes='icon--medium icon--primary') }} rel='noopener noreferrer'>
</a> {{ env['name'] }} {{ Icon('link', classes='icon--medium icon--primary') }}
</span> </a>
</span>
{% else -%}
<span>
{{ env['name'] }}
</span>
{{ Label(type="pending_creation", classes='label--below')}}
{%- endif %}
{% if user_can(permissions.EDIT_ENVIRONMENT) -%} {% if user_can(permissions.EDIT_ENVIRONMENT) -%}
{{ {{
ToggleButton( ToggleButton(
@ -57,10 +68,6 @@
classes="environment-list__item__members" classes="environment-list__item__members"
) )
}} }}
<br>
{% if env['pending'] -%}
{{ Label(type="changes_pending", classes='label--below')}}
{%- endif %}
</div> </div>
</div> </div>

View File

@ -24,11 +24,8 @@
{% if not portfolio.applications %} {% if not portfolio.applications %}
{{ EmptyState( {{ EmptyState(
header="portfolios.applications.empty_state.header"|translate, resource='applications',
message="portfolios.applications.empty_state.message"|translate,
button_text="portfolios.applications.empty_state.button_text"|translate,
button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id), button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id),
view_only_text="portfolios.applications.empty_state.view_only_text"|translate,
user_can_create=can_create_applications, user_can_create=can_create_applications,
) }} ) }}

View File

@ -17,7 +17,7 @@
<div id='app-root'> <div id='app-root'>
{% include 'components/usa_header.html' %} {% include 'components/usa_header.html' %}
{% include 'navigation/topbar.html' %} <div class='login-topbar'>{% include 'navigation/topbar.html' %}</div>
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@ -1,14 +1,22 @@
{% macro EmptyState(header, message, button_text, button_link, view_only_text, user_can_create=True) %} {% macro EmptyState(resource, button_link, user_can_create=False) %}
{% if user_can_create %}
{% set perms = 'edit' %}
{% else %}
{% set perms = 'view' %}
{% endif %}
{% set header = "empty_state.{}.header.{}".format(resource, perms) | translate | safe %}
{% set message = "empty_state.{}.message.{}".format(resource, perms) | translate | safe %}
{% set button_text = "empty_state.{}.button_text".format(resource) | translate | safe %}
<div class="empty-state"> <div class="empty-state">
<h3>{{ header }}</h3> <h3>{{ header }}</h3>
<p>{{ message }}</p> <p>{{ message }}</p>
<hr> {% if user_can_create -%}
<div class="empty-state__footer"> <hr>
{% if user_can_create %} <div class="empty-state__footer">
<a href="{{ button_link }}" class="usa-button usa-button-primary">{{ button_text }}</a> <a href="{{ button_link }}" class="usa-button usa-button-primary">{{ button_text }}</a>
{% else %} </div>
<p>{{ view_only_text }}</p> {%- endif %}
{% endif %}
</div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@ -9,6 +9,11 @@
"text": "changes pending", "text": "changes pending",
"color": "default", "color": "default",
}, },
"pending_creation": {
"icon": "clock",
"text": "pending creation",
"color": "default",
},
"ppoc": {"text": "primary point of contact"} "ppoc": {"text": "primary point of contact"}
} %} } %}

View File

@ -11,7 +11,7 @@
<div class="topbar__context"> <div class="topbar__context">
{% if g.current_user %} {% if g.current_user %}
<a href="{{ url_for('users.user') }}" class="topbar__link"> <a href="{{ url_for('users.user') }}" class="topbar__link">
{{ Icon('avatar', classes='topbar__link-icon') }} {{ Icon('user', classes='topbar__link-icon') }}
<span class="topbar__link-label">{{ g.current_user.first_name + " " + g.current_user.last_name }}</span> <span class="topbar__link-label">{{ g.current_user.first_name + " " + g.current_user.last_name }}</span>
</a> </a>
<a href="#" class="topbar__link"> <a href="#" class="topbar__link">

View File

@ -22,7 +22,7 @@
{{ TextInput(portfolio_form.name, validation="portfolioName", optional=False) }} {{ TextInput(portfolio_form.name, validation="portfolioName", optional=False) }}
{{ TextInput(portfolio_form.description, validation="defaultTextAreaField", paragraph=True) }} {{ TextInput(portfolio_form.description, validation="defaultTextAreaField", paragraph=True) }}
<div class='edit-portfolio-name action-group'> <div class='edit-portfolio-name action-group'>
{{ SaveButton(text='Save Changes', additional_classes='usa-button-big') }} {{ SaveButton(text='Save Changes') }}
</div> </div>
</form> </form>
</base-form> </base-form>

View File

@ -10,10 +10,11 @@
<main class="usa-section usa-content"> <main class="usa-section usa-content">
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<div class='portfolio-header__name'> <div class="portfolio-header-new">
<p>{{ "portfolios.header" | translate }}</p> <div class='portfolio-header__name'>
<h1>{{ "portfolios.new.title" | translate }}</h1> <p>{{ "portfolios.header" | translate }}</p>
</div> <h1>{{ 'portfolios.new.title' | translate }}</h1>
</div>
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }} {{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
<base-form inline-template> <base-form inline-template>
<div class="row"> <div class="row">

View File

@ -6,17 +6,10 @@
{% if not portfolio.applications %} {% if not portfolio.applications %}
{% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %} {% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
{% set message = ('portfolios.reports.empty_state.sub_message.can_create_applications' | translate)
if can_create_applications
else ('portfolios.reports.empty_state.sub_message.cannot_create_applications' | translate)
%}
{{ EmptyState( {{ EmptyState(
header='portfolios.reports.empty_state.message' | translate, resource='applications_reporting',
message=message,
button_text="portfolios.applications.empty_state.button_text"|translate,
button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id), button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id),
view_only_text="portfolios.applications.empty_state.view_only_text"|translate,
user_can_create=can_create_applications, user_can_create=can_create_applications,
) }} ) }}

View File

@ -85,11 +85,8 @@
{% endcall %} {% endcall %}
{% else %} {% else %}
{{ EmptyState( {{ EmptyState(
header="task_orders.empty_state.header"|translate, resource="task_orders",
message="task_orders.empty_state.message"|translate,
button_link=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id), button_link=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id),
button_text="task_orders.empty_state.button_text"|translate,
view_only_text="task_orders.empty_state.view_only_text"|translate,
user_can_create=user_can(permissions.CREATE_TASK_ORDER), user_can_create=user_can(permissions.CREATE_TASK_ORDER),
) }} ) }}
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class='col'> <div class='col user-edit'>
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}

View File

@ -282,3 +282,24 @@ secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ cr
`terraform apply` `terraform apply`
*[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)* *[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)*
*Then we need an instance of the container*
Change directories to the repo root. Ensure that you've checked out the staging or master branch:
`docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
*Create secrets for ATAT database user*
Change directories back to terraform/secrets-tool. There is a sample file there. Make sure you know the URL for the aplication Key Vault (distinct from the operator Key Vault). Run:
`secrets-tool secrets --keyvault [application key vault URL] load -f ./postgres-user.yaml
*Create the database, database user, schema, and initial data set*
This is discussed in more detail [here](https://github.com/dod-ccpo/atst/tree/staging/terraform/secrets-tool#setting-up-the-initial-atat-database). Be sure to read the requirements section.
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```

View File

@ -35,11 +35,3 @@ resource "azurerm_postgresql_virtual_network_rule" "sql" {
subnet_id = var.subnet_id subnet_id = var.subnet_id
ignore_missing_vnet_service_endpoint = true ignore_missing_vnet_service_endpoint = true
} }
resource "azurerm_postgresql_database" "db" {
name = "${var.name}-${var.environment}-atat"
resource_group_name = azurerm_resource_group.sql.name
server_name = azurerm_postgresql_server.sql.name
charset = "UTF8"
collation = "en-US"
}

View File

@ -1,3 +0,0 @@
output "db_name" {
value = azurerm_postgresql_database.db.name
}

View File

@ -30,6 +30,51 @@ Terraform typically expects user defined secrets to be stored in either a file,
This provides a number of security benefits. First, secrets are not on disk. Secondly, users/operators never see the secrets fly by (passerbys or voyeurs that like to look over your shoulder when deploying to production) This provides a number of security benefits. First, secrets are not on disk. Secondly, users/operators never see the secrets fly by (passerbys or voyeurs that like to look over your shoulder when deploying to production)
## Setting up the initial ATAT database
This handles bootstrapping the ATAT database with a user, schema, and initial data.
It does the following:
- Sources the Postgres root user credentials
- Source the Postgres ATAT user password
- Runs a script inside an ATAT docker container to set up the initial database user, schema, and seed data in the database
Requirements:
- docker
- A copy of the ATAT docker image. This can be built in the repo root with: `docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
- You need to know the hostname for the Postgres database. Your IP must either be whitelisted in its firewall rules or you must be behind the VPN.
- You will need a YAML file listing all the CCPO users to be added to the database, with the format:
```
- dod_id: "2323232323"
first_name: "Luke"
last_name: "Skywalker"
- dod_id: "5656565656"
first_name: "Han"
last_name: "Solo"
```
- There should be a password for the ATAT database user in the application Key Vault, preferably named `PGPASSWORD`. You can load this by running `secrets-tool --keyvault [operator key vault url] load -f postgres-user.yml` and supplying YAML like:
```
---
- PGPASSWORD:
type: 'password'
length: 30
```
This command takes a lot of arguments. Run `secrets-tool database --keyvault [operator key vault url] provision -- help` to see the full list of available options.
The command supplies some defaults by assuming you've followed the patterns in sample-secrets.yml and elsewhere.
An example would be:
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```
# Setup # Setup
*Requirements* *Requirements*

View File

@ -0,0 +1,143 @@
import os
import click
import logging
import subprocess
from utils.keyvault.secrets import SecretsClient
logger = logging.getLogger(__name__)
def _run_cmd(command):
try:
env = os.environ.copy()
with subprocess.Popen(
command, env=env, stdout=subprocess.PIPE, shell=True
) as proc:
for line in proc.stdout:
logging.info(line.decode("utf-8"))
except Exception as e:
print(e)
@click.group()
@click.option("--keyvault", required=True, help="Specify the keyvault to operate on")
@click.pass_context
def database(ctx, keyvault):
ctx.ensure_object(dict)
ctx.obj["keyvault"] = keyvault
# root password, root username
@click.command("provision")
@click.option(
"--app-keyvault",
"app_keyvault",
required=True,
help="The username for the new Postgres user.",
)
@click.option(
"--user-username",
"user_username",
default="atat",
required=True,
help="The username for the new Postgres user.",
)
@click.option(
"--user-password-key",
"user_password_key",
default="PGPASSWORD",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--root-username-key",
"root_username_key",
default="postgres-root-user",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--root-password-key",
"root_password_key",
default="postgres-root-password",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--dbname",
"dbname",
required=True,
help="The name of the database the user will be given full access to.",
)
@click.option(
"--dbhost",
"dbhost",
required=True,
help="The name of the database the user will be given full access to.",
)
@click.option(
"--container",
"container",
default="atat:latest",
required=True,
help="The container to run the provisioning command in.",
)
@click.option(
"--ccpo-users",
"ccpo_users",
required=True,
help="The full path to a YAML file listing CCPO users to be seeded to the database.",
)
@click.pass_context
def provision(
ctx,
app_keyvault,
user_username,
user_password_key,
root_username_key,
root_password_key,
dbname,
dbhost,
container,
ccpo_users,
):
"""
Set up the initial ATAT database.
"""
logger.info("obtaining postgres root user credentials")
operator_keyvault = SecretsClient(vault_url=ctx.obj["keyvault"])
root_password = operator_keyvault.get_secret(root_password_key)
root_name = operator_keyvault.get_secret(root_username_key)
logger.info("obtaining postgres database user password")
app_keyvault = SecretsClient(vault_url=app_keyvault)
user_password = app_keyvault.get_secret(user_password_key)
logger.info("starting docker process")
create_database_cmd = (
f"docker run -e PGHOST='{dbhost}'"
f" -e PGPASSWORD='{root_password}'"
f" -e PGUSER='{root_name}@{dbhost}'"
f" -e PGDATABASE='{dbname}'"
f" -e PGSSLMODE=require"
f" {container}"
f" .venv/bin/python script/create_database.py {dbname}"
)
_run_cmd(create_database_cmd)
seed_database_cmd = (
f"docker run -e PGHOST='{dbhost}'"
f" -e PGPASSWORD='{root_password}'"
f" -e PGUSER='{root_name}@{dbhost}'"
f" -e PGDATABASE='{dbname}'"
f" -e PGSSLMODE=require"
f" -v {ccpo_users}:/opt/atat/atst/users.yml"
f" {container}"
f" .venv/bin/python script/database_setup.py {user_username} '{user_password}' users.yml"
)
_run_cmd(seed_database_cmd)
database.add_command(provision)

View File

@ -0,0 +1,4 @@
---
- PGPASSWORD:
type: 'password'
length: 30

View File

@ -7,6 +7,7 @@ import logging
from commands.secrets import secrets from commands.secrets import secrets
from commands.terraform import terraform from commands.terraform import terraform
from commands.database import database
config.setup_logging() config.setup_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,6 +22,7 @@ def cli():
# Add additional command groups # Add additional command groups
cli.add_command(secrets) cli.add_command(secrets)
cli.add_command(terraform) cli.add_command(terraform)
cli.add_command(database)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -66,6 +66,8 @@ class GenerateSecrets():
""" """
def __init__(self, definitions: dict): def __init__(self, definitions: dict):
self.definitions = definitions self.definitions = definitions
most_punctuation = string.punctuation.replace("'", "").replace('"', "")
self.password_characters = string.ascii_letters + string.digits + most_punctuation
def process_definition(self): def process_definition(self):
""" """
@ -101,7 +103,6 @@ class GenerateSecrets():
# Types. Can be usernames, passwords, or in the future things like salted # Types. Can be usernames, passwords, or in the future things like salted
# tokens, uuid, or other specialized types # tokens, uuid, or other specialized types
def _generate_password(self, length: int): def _generate_password(self, length: int):
self.password_characters = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(self.password_characters) for i in range(length)) return ''.join(secrets.choice(self.password_characters) for i in range(length))
def _generate_username(self, length: int): def _generate_username(self, length: int):

View File

@ -1,11 +1,15 @@
from unittest.mock import Mock import pytest
import json
from uuid import uuid4 from uuid import uuid4
from unittest.mock import Mock, patch
from tests.factories import ApplicationFactory, EnvironmentFactory from tests.factories import ApplicationFactory, EnvironmentFactory
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud import AzureCloudProvider
from atst.domain.csp.cloud.models import ( from atst.domain.csp.cloud.models import (
ApplicationCSPPayload,
ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
@ -65,8 +69,8 @@ def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
def mock_management_group_create(mock_azure, spec_dict): def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock( mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = (
**spec_dict spec_dict
) )
@ -82,12 +86,30 @@ def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
assert result.id == "Test Id" assert result.id == "Test Id"
# mock the get_secret so it returns a JSON string
MOCK_CREDS = {
"tenant_id": str(uuid4()),
"tenant_sp_client_id": str(uuid4()),
"tenant_sp_key": "1234",
}
def mock_get_secret(azure, func):
azure.get_secret = func
return azure
def test_create_application_succeeds(mock_azure: AzureCloudProvider): def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create() application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"}) mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure._create_application(AUTH_CREDENTIALS, application) mock_azure = mock_get_secret(mock_azure, lambda *a, **k: json.dumps(MOCK_CREDS))
payload = ApplicationCSPPayload(
tenant_id="1234", display_name=application.name, parent_id=str(uuid4())
)
result = mock_azure.create_application(payload)
assert result.id == "Test Id" assert result.id == "Test Id"

View File

@ -0,0 +1,99 @@
import pytest
from pydantic import ValidationError
from atst.domain.csp.cloud.models import (
AZURE_MGMNT_PATH,
KeyVaultCredentials,
ManagementGroupCSPPayload,
ManagementGroupCSPResponse,
)
def test_ManagementGroupCSPPayload_management_group_name():
# supplies management_group_name when absent
payload = ManagementGroupCSPPayload(
tenant_id="any-old-id",
display_name="Council of Naboo",
parent_id="Galactic_Senate",
)
assert payload.management_group_name
# validates management_group_name
with pytest.raises(ValidationError):
payload = ManagementGroupCSPPayload(
tenant_id="any-old-id",
management_group_name="council of Naboo 1%^&",
display_name="Council of Naboo",
parent_id="Galactic_Senate",
)
# shortens management_group_name to fit
name = "council_of_naboo".ljust(95, "1")
assert len(name) > 90
payload = ManagementGroupCSPPayload(
tenant_id="any-old-id",
management_group_name=name,
display_name="Council of Naboo",
parent_id="Galactic_Senate",
)
assert len(payload.management_group_name) == 90
def test_ManagementGroupCSPPayload_display_name():
# shortens display_name to fit
name = "Council of Naboo".ljust(95, "1")
assert len(name) > 90
payload = ManagementGroupCSPPayload(
tenant_id="any-old-id", display_name=name, parent_id="Galactic_Senate"
)
assert len(payload.display_name) == 90
def test_ManagementGroupCSPPayload_parent_id():
full_path = f"{AZURE_MGMNT_PATH}Galactic_Senate"
# adds full path
payload = ManagementGroupCSPPayload(
tenant_id="any-old-id",
display_name="Council of Naboo",
parent_id="Galactic_Senate",
)
assert payload.parent_id == full_path
# keeps full path
payload = ManagementGroupCSPPayload(
tenant_id="any-old-id", display_name="Council of Naboo", parent_id=full_path
)
assert payload.parent_id == full_path
def test_ManagementGroupCSPResponse_id():
full_id = "/path/to/naboo-123"
response = ManagementGroupCSPResponse(
**{"id": "/path/to/naboo-123", "other": "stuff"}
)
assert response.id == full_id
def test_KeyVaultCredentials_enforce_admin_creds():
with pytest.raises(ValidationError):
KeyVaultCredentials(tenant_id="an id", tenant_admin_username="C3PO")
assert KeyVaultCredentials(
tenant_id="an id",
tenant_admin_username="C3PO",
tenant_admin_password="beep boop",
)
def test_KeyVaultCredentials_enforce_sp_creds():
with pytest.raises(ValidationError):
KeyVaultCredentials(tenant_id="an id", tenant_sp_client_id="C3PO")
assert KeyVaultCredentials(
tenant_id="an id", tenant_sp_client_id="C3PO", tenant_sp_key="beep boop"
)
def test_KeyVaultCredentials_enforce_root_creds():
with pytest.raises(ValidationError):
KeyVaultCredentials(root_tenant_id="an id", root_sp_client_id="C3PO")
assert KeyVaultCredentials(
root_tenant_id="an id", root_sp_client_id="C3PO", root_sp_key="beep boop"
)

View File

@ -1,3 +1,4 @@
from datetime import datetime, timedelta
import pytest import pytest
from uuid import uuid4 from uuid import uuid4
@ -196,3 +197,20 @@ def test_update_does_not_duplicate_names_within_portfolio():
with pytest.raises(AlreadyExistsError): with pytest.raises(AlreadyExistsError):
Applications.update(dupe_application, {"name": name}) Applications.update(dupe_application, {"name": name})
def test_get_applications_pending_creation():
now = datetime.now()
later = now + timedelta(minutes=30)
portfolio1 = PortfolioFactory.create(state="COMPLETED")
app_ready = ApplicationFactory.create(portfolio=portfolio1)
app_done = ApplicationFactory.create(portfolio=portfolio1, cloud_id="123456")
portfolio2 = PortfolioFactory.create(state="UNSTARTED")
app_not_ready = ApplicationFactory.create(portfolio=portfolio2)
uuids = Applications.get_applications_pending_creation()
assert [app_ready.id] == uuids

View File

@ -71,7 +71,7 @@ def test_update_adds_clins():
def test_update_does_not_duplicate_clins(): def test_update_does_not_duplicate_clins():
task_order = TaskOrderFactory.create( task_order = TaskOrderFactory.create(
number="3453453456", create_clins=[{"number": "123"}, {"number": "456"}] number="3453453456123", create_clins=[{"number": "123"}, {"number": "456"}]
) )
clins = [ clins = [
{ {
@ -93,7 +93,7 @@ def test_update_does_not_duplicate_clins():
] ]
task_order = TaskOrders.update( task_order = TaskOrders.update(
task_order_id=task_order.id, task_order_id=task_order.id,
number="0000000000", number="0000000000000",
clins=clins, clins=clins,
pdf={"filename": "sample.pdf", "object_name": "1234567"}, pdf={"filename": "sample.pdf", "object_name": "1234567"},
) )
@ -170,3 +170,11 @@ def test_update_enforces_unique_number():
dupe_task_order = TaskOrderFactory.create() dupe_task_order = TaskOrderFactory.create()
with pytest.raises(AlreadyExistsError): with pytest.raises(AlreadyExistsError):
TaskOrders.update(dupe_task_order.id, task_order.number, [], None) TaskOrders.update(dupe_task_order.id, task_order.number, [], None)
def test_allows_alphanumeric_number():
portfolio = PortfolioFactory.create()
valid_to_numbers = ["1234567890123", "ABC1234567890"]
for number in valid_to_numbers:
assert TaskOrders.create(portfolio.id, number, [], None)

View File

@ -7,6 +7,7 @@ import datetime
from atst.forms import data from atst.forms import data
from atst.models import * from atst.models import *
from atst.models.mixins.state_machines import FSMStates
from atst.domain.invitations import PortfolioInvitations from atst.domain.invitations import PortfolioInvitations
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
@ -121,6 +122,7 @@ class PortfolioFactory(Base):
owner = kwargs.pop("owner", UserFactory.create()) owner = kwargs.pop("owner", UserFactory.create())
members = kwargs.pop("members", []) members = kwargs.pop("members", [])
with_task_orders = kwargs.pop("task_orders", []) with_task_orders = kwargs.pop("task_orders", [])
state = kwargs.pop("state", None)
portfolio = super()._create(model_class, *args, **kwargs) portfolio = super()._create(model_class, *args, **kwargs)
@ -161,6 +163,12 @@ class PortfolioFactory(Base):
permission_sets=perms_set, permission_sets=perms_set,
) )
if state:
state = getattr(FSMStates, state)
fsm = PortfolioStateMachineFactory.create(state=state, portfolio=portfolio)
# setting it in the factory is not working for some reason
fsm.state = state
portfolio.applications = applications portfolio.applications = applications
portfolio.task_orders = task_orders portfolio.task_orders = task_orders
return portfolio return portfolio

View File

@ -112,3 +112,37 @@ def test_no_number():
http_request_form_data = {} http_request_form_data = {}
form = TaskOrderForm(http_request_form_data) form = TaskOrderForm(http_request_form_data)
assert form.data["number"] is None assert form.data["number"] is None
def test_number_allows_alphanumeric():
valid_to_numbers = ["1234567890123", "ABC1234567890"]
for number in valid_to_numbers:
form = TaskOrderForm({"number": number})
assert form.validate()
def test_number_allows_between_13_and_17_characters():
valid_to_numbers = ["123456789012345", "ABCDEFG1234567890"]
for number in valid_to_numbers:
form = TaskOrderForm({"number": number})
assert form.validate()
def test_number_strips_dashes():
valid_to_numbers = ["123-456789-012345", "ABCD-EFG12345-67890"]
for number in valid_to_numbers:
form = TaskOrderForm({"number": number})
assert form.validate()
assert not "-" in form.number.data
def test_number_case_coerces_all_caps():
valid_to_numbers = ["12345678012345", "AbcEFg1234567890"]
for number in valid_to_numbers:
form = TaskOrderForm({"number": number})
assert form.validate()
assert form.number.data == number.upper()

View File

@ -72,6 +72,12 @@ def mock_secrets():
return Mock(spec=secrets) return Mock(spec=secrets)
def mock_identity():
import azure.identity as identity
return Mock(spec=identity)
class MockAzureSDK(object): class MockAzureSDK(object):
def __init__(self): def __init__(self):
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
@ -88,6 +94,7 @@ class MockAzureSDK(object):
self.requests = mock_requests() self.requests = mock_requests()
# may change to a JEDI cloud # may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD self.cloud = AZURE_PUBLIC_CLOUD
self.identity = mock_identity()
@pytest.fixture(scope="function") @pytest.fixture(scope="function")

View File

@ -35,6 +35,7 @@ class TaskOrderPdfForm(Form):
class TaskOrderForm(Form): class TaskOrderForm(Form):
pdf = FormField(TaskOrderPdfForm, label="task_order_pdf") pdf = FormField(TaskOrderPdfForm, label="task_order_pdf")
number = StringField(label="task_order_number", default="number")
@pytest.fixture @pytest.fixture
@ -63,6 +64,12 @@ def multi_checkbox_input_macro(env):
return getattr(multi_checkbox_template.module, "MultiCheckboxInput") return getattr(multi_checkbox_template.module, "MultiCheckboxInput")
@pytest.fixture
def text_input_macro(env):
text_input_template = env.get_template("components/text_input.html")
return getattr(text_input_template.module, "TextInput")
@pytest.fixture @pytest.fixture
def initial_value_form(scope="function"): def initial_value_form(scope="function"):
return InitialValueForm() return InitialValueForm()
@ -170,3 +177,10 @@ def test_make_pop_date_range(env, app):
index=1, index=1,
) )
write_template(pop_date_range, "pop_date_range.html") write_template(pop_date_range, "pop_date_range.html")
def test_make_text_input_template(text_input_macro, task_order_form):
text_input_to_number = text_input_macro(
task_order_form.number, validation="taskOrderNumber"
)
write_template(text_input_to_number, "text_input_to_number.html")

View File

@ -158,7 +158,7 @@ def test_task_orders_form_step_two_add_number(client, user_session, task_order):
def test_task_orders_submit_form_step_two_add_number(client, user_session, task_order): def test_task_orders_submit_form_step_two_add_number(client, user_session, task_order):
user_session(task_order.portfolio.owner) user_session(task_order.portfolio.owner)
form_data = {"number": "1234567890"} form_data = {"number": "abc-1234567890"}
response = client.post( response = client.post(
url_for( url_for(
"task_orders.submit_form_step_two_add_number", task_order_id=task_order.id "task_orders.submit_form_step_two_add_number", task_order_id=task_order.id
@ -167,7 +167,7 @@ def test_task_orders_submit_form_step_two_add_number(client, user_session, task_
) )
assert response.status_code == 302 assert response.status_code == 302
assert task_order.number == "1234567890" assert task_order.number == "ABC1234567890" # pragma: allowlist secret
def test_task_orders_submit_form_step_two_enforces_unique_number( def test_task_orders_submit_form_step_two_enforces_unique_number(
@ -194,7 +194,7 @@ def test_task_orders_submit_form_step_two_add_number_existing_to(
client, user_session, task_order client, user_session, task_order
): ):
user_session(task_order.portfolio.owner) user_session(task_order.portfolio.owner)
form_data = {"number": "0000000000"} form_data = {"number": "0000000000000"}
original_number = task_order.number original_number = task_order.number
response = client.post( response = client.post(
url_for( url_for(
@ -203,7 +203,7 @@ def test_task_orders_submit_form_step_two_add_number_existing_to(
data=form_data, data=form_data,
) )
assert response.status_code == 302 assert response.status_code == 302
assert task_order.number == "0000000000" assert task_order.number == "0000000000000"
assert task_order.number != original_number assert task_order.number != original_number

View File

@ -663,7 +663,7 @@ def test_task_orders_new_get_routes(get_url_assert_status):
def test_task_orders_new_post_routes(post_url_assert_status): def test_task_orders_new_post_routes(post_url_assert_status):
post_routes = [ post_routes = [
("task_orders.submit_form_step_one_add_pdf", {"pdf": ""}), ("task_orders.submit_form_step_one_add_pdf", {"pdf": ""}),
("task_orders.submit_form_step_two_add_number", {"number": "1234567890"}), ("task_orders.submit_form_step_two_add_number", {"number": "1234567890123"}),
( (
"task_orders.submit_form_step_three_add_clins", "task_orders.submit_form_step_three_add_clins",
{ {

View File

@ -8,9 +8,9 @@ from atst.domain.csp.cloud import MockCloudProvider
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.jobs import ( from atst.jobs import (
RecordEnvironmentFailure, RecordFailure,
RecordEnvironmentRoleFailure,
dispatch_create_environment, dispatch_create_environment,
dispatch_create_application,
dispatch_create_atat_admin_user, dispatch_create_atat_admin_user,
dispatch_provision_portfolio, dispatch_provision_portfolio,
dispatch_provision_user, dispatch_provision_user,
@ -18,6 +18,7 @@ from atst.jobs import (
do_provision_user, do_provision_user,
do_provision_portfolio, do_provision_portfolio,
do_create_environment, do_create_environment,
do_create_application,
do_create_atat_admin_user, do_create_atat_admin_user,
) )
from atst.models.utils import claim_for_update from atst.models.utils import claim_for_update
@ -27,9 +28,10 @@ from tests.factories import (
EnvironmentRoleFactory, EnvironmentRoleFactory,
PortfolioFactory, PortfolioFactory,
PortfolioStateMachineFactory, PortfolioStateMachineFactory,
ApplicationFactory,
ApplicationRoleFactory, ApplicationRoleFactory,
) )
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
@pytest.fixture(autouse=True, scope="function") @pytest.fixture(autouse=True, scope="function")
@ -43,8 +45,17 @@ def portfolio():
return portfolio return portfolio
def test_environment_job_failure(celery_app, celery_worker): def _find_failure(session, entity, id_):
@celery_app.task(bind=True, base=RecordEnvironmentFailure) return (
session.query(JobFailure)
.filter(JobFailure.entity == entity)
.filter(JobFailure.entity_id == id_)
.one()
)
def test_environment_job_failure(session, celery_app, celery_worker):
@celery_app.task(bind=True, base=RecordFailure)
def _fail_hard(self, environment_id=None): def _fail_hard(self, environment_id=None):
raise ValueError("something bad happened") raise ValueError("something bad happened")
@ -56,13 +67,12 @@ def test_environment_job_failure(celery_app, celery_worker):
with pytest.raises(ValueError): with pytest.raises(ValueError):
task.get() task.get()
assert environment.job_failures job_failure = _find_failure(session, "environment", str(environment.id))
job_failure = environment.job_failures[0]
assert job_failure.task == task assert job_failure.task == task
def test_environment_role_job_failure(celery_app, celery_worker): def test_environment_role_job_failure(session, celery_app, celery_worker):
@celery_app.task(bind=True, base=RecordEnvironmentRoleFailure) @celery_app.task(bind=True, base=RecordFailure)
def _fail_hard(self, environment_role_id=None): def _fail_hard(self, environment_role_id=None):
raise ValueError("something bad happened") raise ValueError("something bad happened")
@ -74,8 +84,7 @@ def test_environment_role_job_failure(celery_app, celery_worker):
with pytest.raises(ValueError): with pytest.raises(ValueError):
task.get() task.get()
assert role.job_failures job_failure = _find_failure(session, "environment_role", str(role.id))
job_failure = role.job_failures[0]
assert job_failure.task == task assert job_failure.task == task
@ -99,6 +108,24 @@ def test_create_environment_job_is_idempotent(csp, session):
csp.create_environment.assert_not_called() csp.create_environment.assert_not_called()
def test_create_application_job(session, csp):
portfolio = PortfolioFactory.create(
csp_data={"tenant_id": str(uuid4()), "root_management_group_id": str(uuid4())}
)
application = ApplicationFactory.create(portfolio=portfolio, cloud_id=None)
do_create_application(csp, application.id)
session.refresh(application)
assert application.cloud_id
def test_create_application_job_is_idempotent(csp):
application = ApplicationFactory.create(cloud_id=uuid4())
do_create_application(csp, application.id)
csp.create_application.assert_not_called()
def test_create_atat_admin_user(csp, session): def test_create_atat_admin_user(csp, session):
environment = EnvironmentFactory.create(cloud_id="something") environment = EnvironmentFactory.create(cloud_id="something")
do_create_atat_admin_user(csp, environment.id) do_create_atat_admin_user(csp, environment.id)
@ -139,6 +166,21 @@ def test_dispatch_create_environment(session, monkeypatch):
mock.delay.assert_called_once_with(environment_id=e1.id) mock.delay.assert_called_once_with(environment_id=e1.id)
def test_dispatch_create_application(monkeypatch):
portfolio = PortfolioFactory.create(state="COMPLETED")
app = ApplicationFactory.create(portfolio=portfolio)
mock = Mock()
monkeypatch.setattr("atst.jobs.create_application", mock)
# When dispatch_create_application is called
dispatch_create_application.run()
# It should cause the create_application task to be called once
# with the application id
mock.delay.assert_called_once_with(application_id=app.id)
def test_dispatch_create_atat_admin_user(session, monkeypatch): def test_dispatch_create_atat_admin_user(session, monkeypatch):
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
applications=[ applications=[

16
tests/utils/test_hash.py Normal file
View File

@ -0,0 +1,16 @@
import random
import re
import string
from atst.utils import sha256_hex
def test_sha256_hex():
sample = "".join(
random.choices(string.ascii_uppercase + string.digits, k=random.randrange(200))
)
hashed = sha256_hex(sample)
assert re.match("^[a-zA-Z0-9]+$", hashed)
assert len(hashed) == 64
hashed_again = sha256_hex(sample)
assert hashed == hashed_again

View File

@ -84,6 +84,31 @@ email:
application_invite: "{inviter_name} has invited you to a JEDI cloud application" application_invite: "{inviter_name} has invited you to a JEDI cloud application"
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio" portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio"
environment_ready: JEDI cloud environment ready environment_ready: JEDI cloud environment ready
empty_state:
applications:
header:
edit: You dont have any Applications yet
view: This portfolio has no Applications
message:
edit: You can manage multiple Applications within a single Portfolio as long as the funding sources are the same.
view: A Portfolio member with <b>Edit Application</b> permissions can add Applications to this Portfolio.
button_text: Create Your First Application
applications_reporting:
header:
edit: Nothing to report.
view: Nothing to report.
message:
edit: This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.
view: This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.
button_text: Add a new application
task_orders:
header:
edit: Add approved task orders
view: This Portfolio has no Task Orders
message:
edit: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
view: A Portfolio member with <b>Edit Funding</b> permissions can fund this Portfolio with approved Task Orders.
button_text: Add Task Order
flash: flash:
application: application:
created: created:
@ -370,11 +395,6 @@ portfolios:
add_member: Add Team Member add_member: Add Team Member
add_another_environment: Add another environment add_another_environment: Add another environment
create_button: Create Application create_button: Create Application
empty_state:
header: You don't have any Applications yet
message: You can manage multiple Applications within a single Portfolio as long as the funding sources are the same.
button_text: Create Your First Application
view_only_text: Contact your portfolio administrator to add an application.
new: new:
step_1_header: Name and Describe New Application step_1_header: Name and Describe New Application
step_1_button_text: "Next: Add Environments" step_1_button_text: "Next: Add Environments"
@ -417,6 +437,7 @@ portfolios:
add_subscription: Add new subscription add_subscription: Add new subscription
blank_slate: This Application has no environments blank_slate: This Application has no environments
disabled: ": Access Suspended" disabled: ": Access Suspended"
funding_alert: "Application environments will not be created until the {name} portfolio is funded."
environments_heading: Application Environments environments_heading: Application Environments
existing_application_title: "{application_name} Application Settings" existing_application_title: "{application_name} Application Settings"
member_count: "{count} Members" member_count: "{count} Members"
@ -482,12 +503,6 @@ portfolios:
header: Funding Duration header: Funding Duration
tooltip: Funding duration is the period of time that there is a valid task order funding the portfolio. tooltip: Funding duration is the period of time that there is a valid task order funding the portfolio.
estimate_warning: Reports displayed in JEDI are estimates and not a system of record. estimate_warning: Reports displayed in JEDI are estimates and not a system of record.
empty_state:
message: Nothing to report.
sub_message:
can_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Create an application with some cloud environments to get started.
cannot_create_applications: This portfolio has no cloud environments set up, so there is no spending data to report. Contact the portfolio owner to set up some cloud environments.
action_label: "Add a new application"
total_value: total_value:
header: Total Portfolio Value header: Total Portfolio Value
tooltip: Total portfolio value is all obligated and projected funds for all task orders in this portfolio. tooltip: Total portfolio value is all obligated and projected funds for all task orders in this portfolio.
@ -558,11 +573,6 @@ task_orders:
sticky_header_text: "Add a Task Order" sticky_header_text: "Add a Task Order"
sticky_header_review_text: Review Changes sticky_header_review_text: Review Changes
sticky_header_context: "Step {step} of 5" sticky_header_context: "Step {step} of 5"
empty_state:
header: Add approved task orders
message: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
button_text: Add Task Order
view_only_text: Contact your portfolio administrator to add a Task Order.
sign: sign:
digital_signature_description: I confirm the uploaded Task Order is signed by the appropriate, duly warranted Agency Contracting Officer who authorized me to upload the Task Order. digital_signature_description: I confirm the uploaded Task Order is signed by the appropriate, duly warranted Agency Contracting Officer who authorized me to upload the Task Order.
confirmation_description: I confirm that the information entered here in matches that of the submitted Task Order. confirmation_description: I confirm that the information entered here in matches that of the submitted Task Order.

View File

@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New Application--> <!--Imported from: AT-AT CI - Create New Application-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Create New Application-->
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -160,7 +160,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -413,28 +413,13 @@ Imported from: AT-AT CI - login-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -449,7 +434,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -189,12 +189,12 @@ Imported from: AT-AT CI - login-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr original-target=".sticky-cta-buttons > .usa-button.usa-button-primary"> <tr original-target=".empty-state__footer > .usa-button.usa-button-primary">
<td>click</td> <td>click</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>

View File

@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New Application--> <!--Imported from: AT-AT CI - Create New Application-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Create New Application-->
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -656,28 +640,13 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.environment_roles.environment-roles-new > .form-row:nth-of-type(1) > .form-col.form-col--third > fieldset.usa-input__choices > select[name="environment_roles-2-role"]</td> <td>css=.environment_roles.environment-roles-new > .form-row:nth-of-type(1) > .form-col.form-col--third > fieldset.usa-input__choices > select[name="environment_roles-2-role"]</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=.environment_roles.environment-roles-new > .form-row:nth-of-type(1) > .form-col.form-col--third > fieldset.usa-input__choices > select[name="environment_roles-2-role"]</td> <td>css=.environment_roles.environment-roles-new > .form-row:nth-of-type(1) > .form-col.form-col--third > fieldset.usa-input__choices > select[name="environment_roles-2-role"]</td>
<td>Business Read-only</td> <td>CONTRIBUTOR</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -761,13 +730,13 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(1) > td.env_role--td > .row:nth-of-type(3) > .env-role__role</td> <td>css=table.atat-table > tbody > tr:nth-of-type(1) > td.toggle-menu__container > .row:nth-of-type(3) > .env-role__role</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(1) > td.env_role--td > .row:nth-of-type(3) > .env-role__role</td> <td>css=table.atat-table > tbody > tr:nth-of-type(1) > td.toggle-menu__container > .row:nth-of-type(3) > .env-role__role</td>
<td>*Business Read-only*</td> <td>*Contributor*</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -16,7 +16,7 @@
<meta name="ghost-inspector-screenshotTarget" content="" /> <meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" /> <meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" /> <meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.02" /> <meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head> </head>
<body> <body>
<table cellpadding="1" cellspacing="1" border="1"> <table cellpadding="1" cellspacing="1" border="1">
@ -174,7 +174,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -192,7 +192,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#name</td> <td>css=#name</td>
<td>Tatooine Energy Maintenance Systems</td> <td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -291,29 +291,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
Imported from: AT-AT CI - Portfolio Settings--> Imported from: AT-AT CI - Portfolio Settings-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.panel__content > p:nth-of-type(2)</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <td>assertElementPresent</td>
<td>css=.panel__content > p:nth-of-type(2)</td> <td>css=th.table-cell--third</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=td.name</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>css=td.name</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -331,41 +314,7 @@ Imported from: AT-AT CI - Portfolio Settings-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=button.usa-button.usa-button-primary.usa-button-big</td> <td>css=button.usa-button.usa-button-primary.usa-button-big</td>
<td>Save</td> <td>Save Changes</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=button.usa-button.usa-button-primary</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=button.usa-button.usa-button-primary</td>
<td>*Update*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=input.usa-button.usa-button-primary</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=input.usa-button.usa-button-primary</td>
<td>Save</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -375,12 +324,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=a.icon-link.modal-link</td> <td>css=a.usa-button.usa-button-secondary.add-new-button</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=a.icon-link.modal-link</td> <td>css=a.usa-button.usa-button-secondary.add-new-button</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -391,13 +340,13 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#add-port-mem > div > div:nth-of-type(1) > h1</td> <td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=#add-port-mem > div > div:nth-of-type(1) > h1</td> <td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td>*Invite new portfolio member*</td> <td>*Add Manager*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -487,13 +436,13 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#add-port-mem > div > div:nth-of-type(2) > h1</td> <td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=#add-port-mem > div > div:nth-of-type(2) > h1</td> <td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td>*Assign member permissions*</td> <td>*Set Portfolio Permissions*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -503,12 +452,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_app_mgmt</td> <td>css=#perms_app_mgmt-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=#permission_sets-perms_app_mgmt</td> <td>css=#perms_app_mgmt-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -519,12 +468,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td> <td>css=#perms_funding-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td> <td>css=#perms_funding-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -535,12 +484,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_funding</td> <td>css=#perms_reporting-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=#permission_sets-perms_funding</td> <td>css=#perms_reporting-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -551,60 +500,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_funding > option:nth-of-type(1)</td> <td>css=#perms_portfolio_mgmt-None</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#permission_sets-perms_funding > option:nth-of-type(1)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#permission_sets-perms_reporting</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#permission_sets-perms_reporting</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#permission_sets-perms_reporting > option:nth-of-type(1)</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#permission_sets-perms_reporting > option:nth-of-type(1)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#permission_sets-perms_portfolio_mgmt</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#permission_sets-perms_portfolio_mgmt</td> <td>css=#perms_portfolio_mgmt-None</td>
<td>edit_portfolio_admin</td> <td>edit_portfolio_admin</td>
</tr> </tr>
<tr> <tr>
@ -615,22 +516,6 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=#permission_sets-perms_portfolio_mgmt > option:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#permission_sets-perms_portfolio_mgmt > option:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=input[type="submit"].action-group__action</td> <td>css=input[type="submit"].action-group__action</td>
<td></td> <td></td>
</tr> </tr>
@ -647,12 +532,75 @@ Imported from: AT-AT CI - Portfolio Settings-->
<!--Imported from: AT-AT CI - New Portfolio Member--> <!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.name</td> <td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</td>
<td>*invite pending*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=.usa-alert-body</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.usa-alert-body</td>
<td>*Brandon Buchannan's invitation has been sent
Brandon Buchannan's access to this Portfolio is pending until they sign in for the first time.*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item__toggler > .icon.icon--ellipsis > svg.svg-inline--fa.fa-ellipsis-h.fa-w-16</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item__toggler > .icon.icon--ellipsis > svg.svg-inline--fa.fa-ellipsis-h.fa-w-16</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item-toggle-content.toggle-menu__toggle > a:nth-of-type(1)</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item-toggle-content.toggle-menu__toggle > a:nth-of-type(1)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.portfolio-content > div:nth-of-type(3) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > .modal__form--header > h1</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertElementPresent</td> <td>assertElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.name</td> <td>css=.portfolio-content > div:nth-of-type(3) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > .modal__form--header > h1</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -660,105 +608,59 @@ Imported from: AT-AT CI - Portfolio Settings-->
<td></td> <td></td>
<td></td> <td></td>
</tr> </tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.usa-alert-body > p:nth-of-type(2)</td> <td>css=.portfolio-perms > div:nth-of-type(2) > .usa-input.input__inline-fields.checked > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".portfolio-perms > div:nth-of-type(2) > .usa-input.input__inline-fields.checked > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Edit Funding")]">
<td>click</td>
<td>css=.portfolio-perms > div:nth-of-type(2) > .usa-input.input__inline-fields.checked > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.portfolio-perms > div:nth-of-type(4) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".portfolio-perms > div:nth-of-type(4) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Edit Portfolio")]">
<td>click</td>
<td>css=.portfolio-perms > div:nth-of-type(4) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.action-group__action.usa-button</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=.action-group__action.usa-button</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=h3.usa-alert-heading</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.usa-alert-body > p:nth-of-type(2)</td> <td>css=h3.usa-alert-heading</td>
<td>*You have successfully invited Brandon Buchannan to the portfolio.*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=select[name="members_permissions-1-perms_app_mgmt"]</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=select[name="members_permissions-1-perms_app_mgmt"]</td>
<td>edit_portfolio_application_management</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=select[name="members_permissions-1-perms_app_mgmt"] > option:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=select[name="members_permissions-1-perms_app_mgmt"] > option:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=select[name="members_permissions-1-perms_reporting"]</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=select[name="members_permissions-1-perms_reporting"]</td>
<td>edit_portfolio_reports</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=select[name="members_permissions-1-perms_reporting"] > option:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=select[name="members_permissions-1-perms_reporting"] > option:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=input[type="submit"]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=input[type="submit"]</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading</td>
<td>*Success!*</td> <td>*Success!*</td>
</tr> </tr>
<tr> <tr>
@ -768,13 +670,13 @@ Imported from: AT-AT CI - Portfolio Settings-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.usa-alert-body > p:nth-of-type(2)</td> <td>css=.usa-alert-text</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.usa-alert-body > p:nth-of-type(2)</td> <td>css=.usa-alert-text</td>
<td>*You have successfully updated access permissions for members of Tatooine Energy Maintenance Systems.*</td> <td>*You have successfully updated access permissions for*</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -106,7 +106,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -111,7 +111,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -170,7 +170,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -640,28 +640,13 @@ Imported from: AT-AT CI - New App Step 1-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=[name=environment_roles-0-role]</td> <td>css=[name=environment_roles-0-role]</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=[name=environment_roles-0-role]</td> <td>css=[name=environment_roles-0-role]</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -676,7 +661,7 @@ Imported from: AT-AT CI - New App Step 1-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=[name=environment_roles-1-role]</td> <td>css=[name=environment_roles-1-role]</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -96,7 +96,7 @@
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -165,7 +165,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -275,22 +275,6 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Portfolio Settings--> <!--Imported from: AT-AT CI - Portfolio Settings-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.panel__content > p:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>css=.panel__content > p:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=th.table-cell--third</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
@ -320,22 +304,6 @@ Imported from: AT-AT CI - New Portfolio-->
<td></td> <td></td>
<td></td> <td></td>
</tr> </tr>
<!--Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=button.usa-button.usa-button-primary</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=button.usa-button.usa-button-primary</td>
<td>*Update*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=a.usa-button.usa-button-secondary.add-new-button</td> <td>css=a.usa-button.usa-button-secondary.add-new-button</td>
@ -554,7 +522,9 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.usa-alert-body</td> <td>css=.usa-alert-body</td>
<td>*You have successfully invited Brandon Buchannan to the portfolio.*</td> <td>*Brandon Buchannan's invitation has been sent
Brandon Buchannan's access to this Portfolio is pending until they sign in for the first time.*</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -204,21 +204,6 @@ Imported from: AT-AT CI - login-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.panel__content > p:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>css=.panel__content > p:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=th.table-cell--third</td> <td>css=th.table-cell--third</td>
<td></td> <td></td>
</tr> </tr>
@ -242,21 +227,6 @@ Imported from: AT-AT CI - login-->
<td>css=button.usa-button.usa-button-primary.usa-button-big</td> <td>css=button.usa-button.usa-button-primary.usa-button-big</td>
<td>Save Changes</td> <td>Save Changes</td>
</tr> </tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=button.usa-button.usa-button-primary</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=button.usa-button.usa-button-primary</td>
<td>*Update*</td>
</tr>
</tbody> </tbody>
</table> </table>
</body> </body>

View File

@ -106,7 +106,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -200,12 +200,12 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New TO--> <!--Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr original-target=".sticky-cta-buttons > .usa-button.usa-button-primary"> <tr original-target=".empty-state__footer > .usa-button.usa-button-primary">
<td>click</td> <td>click</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>

View File

@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

View File

@ -111,7 +111,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -211,12 +211,12 @@ Imported from: AT-AT CI - Create New TO-->
Imported from: AT-AT CI - Create New TO--> Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr original-target=".sticky-cta-buttons > .usa-button.usa-button-primary"> <tr original-target=".empty-state__footer > .usa-button.usa-button-primary">
<td>click</td> <td>click</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>

View File

@ -111,7 +111,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -211,12 +211,12 @@ Imported from: AT-AT CI - Create New TO-->
Imported from: AT-AT CI - Create New TO--> Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr original-target=".sticky-cta-buttons > .usa-button.usa-button-primary"> <tr original-target=".empty-state__footer > .usa-button.usa-button-primary">
<td>click</td> <td>click</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -841,7 +841,7 @@ Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td> <td>css=.row > .col.col--grow.summary-item:nth-of-type(1) > .summary-item__value--large</td>
<td>*$100,000.00*</td> <td>*$800,000.00*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -856,7 +856,7 @@ Imported from: AT-AT CI - Create New TO-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td> <td>css=.row > .col.col--grow.summary-item:nth-of-type(2) > .summary-item__value--large</td>
<td>*$800,000.00*</td> <td>*$100,000.00*</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New Application--> <!--Imported from: AT-AT CI - Create New Application-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Create New Application-->
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -583,12 +567,12 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT Holding - Create TO after other steps--> <!--Imported from: AT-AT Holding - Create TO after other steps-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr original-target=".sticky-cta-buttons > .usa-button.usa-button-primary"> <tr original-target=".empty-state__footer > .usa-button.usa-button-primary">
<td>click</td> <td>click</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>

View File

@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New Application--> <!--Imported from: AT-AT CI - Create New Application-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Create New Application-->
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -583,12 +567,12 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT Holding - Create TO after other steps--> <!--Imported from: AT-AT Holding - Create TO after other steps-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr original-target=".sticky-cta-buttons > .usa-button.usa-button-primary"> <tr original-target=".empty-state__footer > .usa-button.usa-button-primary">
<td>click</td> <td>click</td>
<td>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td> <td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>

View File

@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New Application--> <!--Imported from: AT-AT CI - Create New Application-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Create New Application-->
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -626,12 +610,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]</td> <td>css=.user-info > .usa-input.usa-input--validation--name:nth-of-type(1) > input[id="first_name"][type="text"]</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]</td> <td>css=.user-info > .usa-input.usa-input--validation--name:nth-of-type(1) > input[id="first_name"][type="text"]</td>
<td>*Brandon*</td> <td>*Brandon*</td>
</tr> </tr>
<tr> <tr>
@ -641,12 +625,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]</td> <td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > input[type="submit"]</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]</td> <td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > input[type="submit"]</td>
<td>*Resend Invite*</td> <td>*Resend Invite*</td>
</tr> </tr>
<tr> <tr>
@ -656,12 +640,12 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]</td> <td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > input[type="submit"]</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>click</td> <td>click</td>
<td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]</td> <td>css=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > input[type="submit"]</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
@ -671,28 +655,13 @@ Imported from: AT-AT CI - New Portfolio-->
</tr> </tr>
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading</td>
<td>*Application invitation resent*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text</td> <td>css=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text</td> <td>css=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text</td>
<td>*You have successfully resent the invite for Brandon Buchannan*</td> <td>*jay+brandon@promptworks.com has been sent an invitation to access this Application*</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,639 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="selenium.base" href="" />
<title>Resend Portfolio Member Invite</title>
<meta name="ghost-inspector-details" content="" />
<meta name="ghost-inspector-browser" content="chrome" />
<meta name="ghost-inspector-userAgent" content="" />
<meta name="ghost-inspector-region" content="us-east-1" />
<meta name="ghost-inspector-globalStepDelay" content="250" />
<meta name="ghost-inspector-maxWaitDelay" content="15000" />
<meta name="ghost-inspector-maxAjaxDelay" content="10000" />
<meta name="ghost-inspector-viewportSize" content="1920x1080" />
<meta name="ghost-inspector-screenshotTarget" content="" />
<meta name="ghost-inspector-screenshotExclusions" content="div.global-navigation, time" />
<meta name="ghost-inspector-screenshotCompareEnabled" content="true" />
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">Resend Portfolio Member Invite</td>
</tr>
</thead>
<tbody>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Login Brandon-->
<tr>
<td>open</td>
<td>/login-dev?username=brandon</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Login Brandon-->
<tr>
<td>waitForElementPresent</td>
<td>css=a[href="/user"] > .topbar__link-label</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=a[href="/user"] > .topbar__link-label</td>
<td>*Brandon Buchannan*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Login Brandon-->
<tr>
<td>waitForElementPresent</td>
<td>css=a[href="/logout"] > .topbar__link-label</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=a[href="/logout"] > .topbar__link-label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Login Brandon-->
<tr>
<td>waitForElementPresent</td>
<td>css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading</td>
<td>*Logged out*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio
Imported from: AT-AT CI - login-->
<tr>
<td>open</td>
<td>/login-dev</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio
Imported from: AT-AT CI - login-->
<tr>
<td>waitForElementPresent</td>
<td>css=.home__content > h1</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.home__content > h1</td>
<td>JEDI Cloud Services</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=a[href="/portfolios/new"]</td>
<td></td>
</tr>
<tr original-target="a[href="/portfolios/new"],xpath=//a[contains(text(), "Add New Portfolio")]">
<td>click</td>
<td>css=a[href="/portfolios/new"]</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=.portfolio-header__name > h1</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.portfolio-header__name > h1</td>
<td>*New Portfolio*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=.sticky-cta-text > h3</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.sticky-cta-text > h3</td>
<td>*Name and Describe Portfolio*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=#name</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=#name</td>
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=fieldset.usa-input__choices > ul > li:nth-of-type(5) > label</td>
<td></td>
</tr>
<tr original-target="fieldset.usa-input__choices > ul > li:nth-of-type(5) > label,xpath=//label[contains(text(), "Other")]">
<td>click</td>
<td>css=fieldset.usa-input__choices > ul > li:nth-of-type(5) > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=input[type="submit"]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=input[type="submit"]</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings
Imported from: AT-AT CI - New Portfolio-->
<tr>
<td>waitForElementPresent</td>
<td>css=.empty-state > h3</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.empty-state > h3</td>
<td>*You don't have any Applications yet*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=.icon.icon--cog > svg</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=.icon.icon--cog > svg</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=.portfolio-header__name > h1</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.portfolio-header__name > h1</td>
<td>*Tatooine Energy Maintenance Systems*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=th.table-cell--third</td>
<td></td>
</tr>
<tr>
<td>assertElementPresent</td>
<td>css=th.table-cell--third</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member
Imported from: AT-AT CI - Portfolio Settings-->
<tr>
<td>waitForElementPresent</td>
<td>css=button.usa-button.usa-button-primary.usa-button-big</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=button.usa-button.usa-button-primary.usa-button-big</td>
<td>Save Changes</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=a.usa-button.usa-button-secondary.add-new-button</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=a.usa-button.usa-button-secondary.add-new-button</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td>*Add Manager*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#user_data-first_name</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=#user_data-first_name</td>
<td>Brandon</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#user_data-last_name</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=#user_data-last_name</td>
<td>Buchannan</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#user_data-email</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=#user_data-email</td>
<td>jay+brandon@promptworks.com</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#user_data-dod_id</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=#user_data-dod_id</td>
<td>3456789012</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=input[type="button"]</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=input[type="button"]</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
<td>*Set Portfolio Permissions*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#perms_app_mgmt-None</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#perms_app_mgmt-None</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#perms_funding-None</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#perms_funding-None</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#perms_reporting-None</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=#perms_reporting-None</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=#perms_portfolio_mgmt-None</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>css=#perms_portfolio_mgmt-None</td>
<td>edit_portfolio_admin</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=input[type="submit"].action-group__action</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=input[type="submit"].action-group__action</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=table.atat-table > tbody > tr > td > span.label.label--success.label--below</td>
<td>*invite pending*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - New Portfolio Member-->
<tr>
<td>waitForElementPresent</td>
<td>css=.usa-alert-body</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.usa-alert-body</td>
<td>*Brandon Buchannan's invitation has been sent
Brandon Buchannan's access to this Portfolio is pending until they sign in for the first time.*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item__toggler > .icon.icon--ellipsis > svg.svg-inline--fa.fa-ellipsis-h.fa-w-16 > path</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item__toggler > .icon.icon--ellipsis > svg.svg-inline--fa.fa-ellipsis-h.fa-w-16 > path</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item-toggle-content.toggle-menu__toggle > a:nth-of-type(2)</td>
<td></td>
</tr>
<tr original-target="table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item-toggle-content.toggle-menu__toggle > a:nth-of-type(2),xpath=//a[contains(text(), "Resend Invite")]">
<td>click</td>
<td>css=table.atat-table > tbody > tr:nth-of-type(2) > td.toggle-menu__container > .toggle-menu > .accordion-table__item-toggle-content.toggle-menu__toggle > a:nth-of-type(2)</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.portfolio-content > div:nth-of-type(4) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > .modal__form--header > h1</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.portfolio-content > div:nth-of-type(4) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > .modal__form--header > h1</td>
<td>*Verify Member Information*</td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.action-group__action.usa-button</td>
<td></td>
</tr>
<tr>
<td>click</td>
<td>css=.action-group__action.usa-button</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.usa-alert-text</td>
<td></td>
</tr>
<tr>
<td>assertText</td>
<td>css=.usa-alert-text</td>
<td>*jay+brandon@promptworks.com has been sent an invitation to access this Portfolio*</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>assertText</td> <td>assertText</td>
<td>css=.sticky-cta-text > h3</td> <td>css=.sticky-cta-text > h3</td>
<td>*Create New Portfolio*</td> <td>*Name and Describe Portfolio*</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
<!--Imported from: AT-AT CI - Create New Application--> <!--Imported from: AT-AT CI - Create New Application-->
<tr> <tr>
<td>waitForElementPresent</td> <td>waitForElementPresent</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr original-target=".application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label,xpath=//label[contains(text(), "Delete Application")]">
<td>click</td>
<td>css=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label</td>
<td></td>
</tr>
<tr>
<td>waitForPageToLoad</td>
<td></td>
<td></td>
</tr>
<!--Imported from: AT-AT CI - Create New Application-->
<tr>
<td>waitForElementPresent</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td></td> <td></td>
</tr> </tr>
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-0-role-None</td> <td>css=#environment_roles-0-role-None</td>
<td>Basic Access</td> <td>ADMIN</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
<tr> <tr>
<td>type</td> <td>type</td>
<td>css=#environment_roles-1-role-None</td> <td>css=#environment_roles-1-role-None</td>
<td>Network Admin</td> <td>BILLING_READ</td>
</tr> </tr>
<tr> <tr>
<td>waitForPageToLoad</td> <td>waitForPageToLoad</td>

Some files were not shown because too many files have changed in this diff Show More