Merge branch 'staging' into to-builder-previous-button
This commit is contained in:
commit
f48404215a
@ -21,11 +21,8 @@ LICENSE
|
||||
# Skip envrc
|
||||
.envrc
|
||||
|
||||
# Skip ansible-container stuff
|
||||
ansible*
|
||||
container.yml
|
||||
meta.yml
|
||||
requirements.yml
|
||||
# Skip terraform
|
||||
terraform
|
||||
|
||||
# Skip kubernetes and Docker config stuff
|
||||
deploy
|
||||
|
@ -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_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_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.
|
||||
- `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout.
|
||||
- `STATIC_URL`: URL specifying where static assets are hosted.
|
||||
|
@ -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 ###
|
60
alembic/versions/508957112ed6_combine_job_failures.py
Normal file
60
alembic/versions/508957112ed6_combine_job_failures.py
Normal 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 ###
|
@ -1,5 +1,9 @@
|
||||
from . import BaseDomainClass
|
||||
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.domain.application_roles import ApplicationRoles
|
||||
from atst.domain.environments import Environments
|
||||
@ -10,7 +14,10 @@ from atst.models import (
|
||||
ApplicationRole,
|
||||
ApplicationRoleStatus,
|
||||
EnvironmentRole,
|
||||
Portfolio,
|
||||
PortfolioStateMachine,
|
||||
)
|
||||
from atst.models.mixins.state_machines import FSMStates
|
||||
from atst.utils import first_or_none, commit_or_raise_already_exists_error
|
||||
|
||||
|
||||
@ -118,3 +125,21 @@ class Applications(BaseDomainClass):
|
||||
db.session.commit()
|
||||
|
||||
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]
|
||||
|
@ -1,15 +1,14 @@
|
||||
import json
|
||||
import re
|
||||
from secrets import token_urlsafe
|
||||
from typing import Dict
|
||||
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 .exceptions import AuthenticationException
|
||||
from .models import (
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
@ -18,6 +17,8 @@ from .models import (
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
KeyVaultCredentials,
|
||||
ManagementGroupCSPResponse,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
@ -26,6 +27,7 @@ from .models import (
|
||||
TenantCSPResult,
|
||||
)
|
||||
from .policy import AzurePolicyManager
|
||||
from atst.utils import sha256_hex
|
||||
|
||||
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
|
||||
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.identity as identity
|
||||
from azure.keyvault import secrets
|
||||
from azure.core import exceptions
|
||||
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
import adal
|
||||
@ -85,7 +88,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
|
||||
def set_secret(self, secret_key, secret_value):
|
||||
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,
|
||||
)
|
||||
try:
|
||||
@ -98,7 +101,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
|
||||
def get_secret(self, secret_key):
|
||||
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,
|
||||
)
|
||||
try:
|
||||
@ -109,9 +112,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
exc_info=1,
|
||||
)
|
||||
|
||||
def create_environment(
|
||||
self, auth_credentials: Dict, user: User, environment: Environment
|
||||
):
|
||||
def create_environment(self, auth_credentials: Dict, user, environment):
|
||||
# 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
|
||||
# something like this:
|
||||
@ -128,7 +129,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
credentials, management_group_id, display_name, parent_id,
|
||||
)
|
||||
|
||||
return management_group
|
||||
return ManagementGroupCSPResponse(**management_group)
|
||||
|
||||
def create_atat_admin_user(
|
||||
self, auth_credentials: Dict, csp_environment_id: str
|
||||
@ -167,16 +168,26 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"role_name": role_assignment_id,
|
||||
}
|
||||
|
||||
def _create_application(self, auth_credentials: Dict, application: Application):
|
||||
management_group_name = str(uuid4()) # can be anything, not just uuid
|
||||
display_name = application.name # Does this need to be unique?
|
||||
credentials = self._get_credential_obj(auth_credentials)
|
||||
parent_id = "?" # application.portfolio.csp_details.management_group_id
|
||||
|
||||
return self._create_management_group(
|
||||
credentials, management_group_name, display_name, parent_id,
|
||||
def create_application(self, payload: ApplicationCSPPayload):
|
||||
creds = self._source_creds(payload.tenant_id)
|
||||
credentials = self._get_credential_obj(
|
||||
{
|
||||
"client_id": creds.root_sp_client_id,
|
||||
"secret_key": creds.root_sp_key,
|
||||
"tenant_id": creds.root_tenant_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(
|
||||
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
|
||||
# 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()
|
||||
|
||||
def _create_subscription(
|
||||
@ -290,6 +304,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
if sp_token is None:
|
||||
raise AuthenticationException("Could not resolve token for tenant creation")
|
||||
|
||||
payload.password = token_urlsafe(16)
|
||||
create_tenant_body = payload.dict(by_alias=True)
|
||||
|
||||
@ -626,3 +641,24 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"secret_key": self.secret_key,
|
||||
"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))
|
||||
|
@ -1,9 +1,5 @@
|
||||
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:
|
||||
def set_secret(self, secret_key: str, secret_value: str):
|
||||
@ -15,9 +11,7 @@ class CloudProviderInterface:
|
||||
def root_creds(self) -> Dict:
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_environment(
|
||||
self, auth_credentials: Dict, user: User, environment: Environment
|
||||
) -> str:
|
||||
def create_environment(self, auth_credentials: Dict, user, environment) -> str:
|
||||
"""Create a new environment in the CSP.
|
||||
|
||||
Arguments:
|
||||
@ -65,7 +59,7 @@ class CloudProviderInterface:
|
||||
raise NotImplementedError()
|
||||
|
||||
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:
|
||||
"""Creates a user or updates an existing user's role.
|
||||
|
||||
|
@ -17,6 +17,9 @@ from .exceptions import (
|
||||
UnknownServerException,
|
||||
)
|
||||
from .models import (
|
||||
AZURE_MGMNT_PATH,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
@ -340,3 +343,16 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
self._delay(1, 5)
|
||||
if self._with_authorization and credentials != self._auth_credentials:
|
||||
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
|
||||
|
@ -1,6 +1,8 @@
|
||||
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
|
||||
|
||||
@ -232,3 +234,110 @@ class BillingInstructionCSPResult(AliasModel):
|
||||
fields = {
|
||||
"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
|
||||
|
@ -93,10 +93,13 @@ class Users(object):
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def give_ccpo_perms(cls, user):
|
||||
def give_ccpo_perms(cls, user, commit=True):
|
||||
user.permission_sets = PermissionSets.get_all()
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
if commit:
|
||||
db.session.commit()
|
||||
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
|
@ -10,11 +10,13 @@ from wtforms.fields.html5 import DateField
|
||||
from wtforms.validators import (
|
||||
Required,
|
||||
Length,
|
||||
Optional,
|
||||
NumberRange,
|
||||
ValidationError,
|
||||
)
|
||||
from flask_wtf import FlaskForm
|
||||
import numbers
|
||||
|
||||
from atst.forms.validators import Number, AlphaNumeric
|
||||
|
||||
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):
|
||||
jedi_clin_type = SelectField(
|
||||
translate("task_orders.form.clin_type_label"),
|
||||
@ -149,8 +159,8 @@ class AttachmentForm(BaseForm):
|
||||
class TaskOrderForm(BaseForm):
|
||||
number = StringField(
|
||||
label=translate("forms.task_order.number_description"),
|
||||
filters=[remove_empty_string],
|
||||
validators=[Number(), Length(max=13)],
|
||||
filters=[remove_empty_string, remove_dashes, coerce_upper],
|
||||
validators=[AlphaNumeric(), Length(min=13, max=17), Optional()],
|
||||
)
|
||||
pdf = FormField(
|
||||
AttachmentForm,
|
||||
|
91
atst/jobs.py
91
atst/jobs.py
@ -3,47 +3,38 @@ import pendulum
|
||||
|
||||
from atst.database import db
|
||||
from atst.queue import celery
|
||||
from atst.models import (
|
||||
EnvironmentJobFailure,
|
||||
EnvironmentRoleJobFailure,
|
||||
EnvironmentRole,
|
||||
PortfolioJobFailure,
|
||||
)
|
||||
from atst.models import EnvironmentRole, JobFailure
|
||||
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||
from atst.domain.csp.cloud import CloudProviderInterface
|
||||
from atst.domain.applications import Applications
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.models.utils import claim_for_update
|
||||
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):
|
||||
if "portfolio_id" in kwargs:
|
||||
failure = PortfolioJobFailure(
|
||||
portfolio_id=kwargs["portfolio_id"], 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
|
||||
)
|
||||
info = self._derive_entity_info(kwargs)
|
||||
if info:
|
||||
failure = JobFailure(**info, task_id=task_id)
|
||||
db.session.add(failure)
|
||||
db.session.commit()
|
||||
|
||||
@ -63,6 +54,27 @@ def send_notification_mail(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):
|
||||
environment = Environments.get(environment_id)
|
||||
|
||||
@ -144,17 +156,22 @@ def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
|
||||
fsm.trigger_next_transition()
|
||||
|
||||
|
||||
@celery.task(bind=True, base=RecordPortfolioFailure)
|
||||
@celery.task(bind=True, base=RecordFailure)
|
||||
def provision_portfolio(self, portfolio_id=None):
|
||||
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):
|
||||
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):
|
||||
do_work(
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
def dispatch_create_environment(self):
|
||||
for environment_id in Environments.get_environments_pending_creation(
|
||||
|
@ -7,11 +7,7 @@ from .audit_event import AuditEvent
|
||||
from .clin import CLIN, JEDICLINType
|
||||
from .environment import Environment
|
||||
from .environment_role import EnvironmentRole, CSPRole
|
||||
from .job_failure import (
|
||||
EnvironmentJobFailure,
|
||||
EnvironmentRoleJobFailure,
|
||||
PortfolioJobFailure,
|
||||
)
|
||||
from .job_failure import JobFailure
|
||||
from .notification_recipient import NotificationRecipient
|
||||
from .permissions import Permissions
|
||||
from .permission_set import PermissionSet
|
||||
|
@ -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 atst.models.base import Base
|
||||
@ -40,6 +40,9 @@ class Application(
|
||||
),
|
||||
)
|
||||
|
||||
cloud_id = Column(String)
|
||||
claimed_until = Column(TIMESTAMP(timezone=True))
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
return set(role.user for role in self.members)
|
||||
|
@ -30,8 +30,6 @@ class Environment(
|
||||
|
||||
claimed_until = Column(TIMESTAMP(timezone=True))
|
||||
|
||||
job_failures = relationship("EnvironmentJobFailure")
|
||||
|
||||
roles = relationship(
|
||||
"EnvironmentRole",
|
||||
back_populates="environment",
|
||||
|
@ -32,8 +32,6 @@ class EnvironmentRole(
|
||||
)
|
||||
application_role = relationship("ApplicationRole")
|
||||
|
||||
job_failures = relationship("EnvironmentRoleJobFailure")
|
||||
|
||||
csp_user_id = Column(String())
|
||||
claimed_until = Column(TIMESTAMP(timezone=True))
|
||||
|
||||
|
@ -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
|
||||
import atst.models.mixins as mixins
|
||||
|
||||
|
||||
class EnvironmentJobFailure(Base, mixins.JobFailureMixin):
|
||||
__tablename__ = "environment_job_failures"
|
||||
class JobFailure(Base, mixins.TimestampsMixin):
|
||||
__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):
|
||||
__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)
|
||||
return self._task
|
||||
|
@ -3,5 +3,4 @@ from .auditable import AuditableMixin
|
||||
from .permissions import PermissionsMixin
|
||||
from .deletable import DeletableMixin
|
||||
from .invites import InvitesMixin
|
||||
from .job_failure import JobFailureMixin
|
||||
from .state_machines import FSMMixin
|
||||
|
@ -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
|
@ -175,7 +175,7 @@ class PortfolioStateMachine(
|
||||
tenant_id = new_creds.get("tenant_id")
|
||||
secret = self.csp.get_secret(tenant_id, 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:
|
||||
app.logger.error(
|
||||
f"Failed to cast response to valid result class {self.__repr__()}:",
|
||||
|
@ -11,6 +11,10 @@ def update_celery(celery, app):
|
||||
"task": "atst.jobs.dispatch_provision_portfolio",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-dispatch_create_application": {
|
||||
"task": "atst.jobs.dispatch_create_application",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-dispatch_create_environment": {
|
||||
"task": "atst.jobs.dispatch_create_environment",
|
||||
"schedule": 60,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@ -41,3 +42,8 @@ def commit_or_raise_already_exists_error(message):
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise AlreadyExistsError(message)
|
||||
|
||||
|
||||
def sha256_hex(string):
|
||||
hsh = hashlib.sha256(string.encode())
|
||||
return hsh.digest().hex()
|
||||
|
@ -43,6 +43,7 @@ SERVER_NAME
|
||||
SESSION_COOKIE_NAME=atat
|
||||
SESSION_COOKIE_DOMAIN
|
||||
SESSION_KEY_PREFIX=session:
|
||||
SESSION_COOKIE_SECURE=false
|
||||
SESSION_TYPE = redis
|
||||
SESSION_USE_SIGNER = True
|
||||
SQLALCHEMY_ECHO = False
|
||||
|
@ -32,6 +32,7 @@ data:
|
||||
REDIS_HOST: atat.redis.cache.windows.net:6380
|
||||
REDIS_TLS: "true"
|
||||
SESSION_COOKIE_DOMAIN: atat.code.mil
|
||||
SESSION_COOKIE_SECURE: "true"
|
||||
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
||||
TZ: UTC
|
||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
||||
|
@ -29,6 +29,8 @@ spec:
|
||||
containers:
|
||||
- name: atst
|
||||
image: $CONTAINER_IMAGE
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
env:
|
||||
- name: UWSGI_PROCESSES
|
||||
value: "2"
|
||||
@ -64,6 +66,8 @@ spec:
|
||||
cpu: 940m
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
ports:
|
||||
- containerPort: 8342
|
||||
name: main-upgrade
|
||||
@ -189,6 +193,8 @@ spec:
|
||||
containers:
|
||||
- name: atst-worker
|
||||
image: $CONTAINER_IMAGE
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
args:
|
||||
[
|
||||
"/opt/atat/atst/.venv/bin/python",
|
||||
@ -261,6 +267,8 @@ spec:
|
||||
containers:
|
||||
- name: atst-beat
|
||||
image: $CONTAINER_IMAGE
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
args:
|
||||
[
|
||||
"/opt/atat/atst/.venv/bin/python",
|
||||
|
@ -20,6 +20,8 @@ spec:
|
||||
containers:
|
||||
- name: crls
|
||||
image: $CONTAINER_IMAGE
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
command: [
|
||||
"/bin/sh", "-c"
|
||||
]
|
||||
|
@ -16,6 +16,8 @@ spec:
|
||||
containers:
|
||||
- name: migration
|
||||
image: $CONTAINER_IMAGE
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
command: [
|
||||
"/bin/sh", "-c"
|
||||
]
|
||||
|
98
js/components/__tests__/text_input.test.js
Normal file
98
js/components/__tests__/text_input.test.js
Normal 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
@ -106,9 +106,9 @@ export default {
|
||||
},
|
||||
taskOrderNumber: {
|
||||
mask: false,
|
||||
match: /^.{13}$/,
|
||||
unmask: [],
|
||||
validationError: 'TO number must be 13 digits',
|
||||
match: /(^[0-9a-zA-Z]{13,17}$)/,
|
||||
unmask: ['-'],
|
||||
validationError: 'TO number must be between 13 and 17 characters',
|
||||
},
|
||||
usPhone: {
|
||||
mask: [
|
||||
|
41
script/create_database.py
Normal file
41
script/create_database.py
Normal 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
76
script/database_setup.py
Normal 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)
|
@ -16,7 +16,9 @@ from atst.app import make_config, make_app
|
||||
def reset_database():
|
||||
conn = db.engine.connect()
|
||||
|
||||
meta = sqlalchemy.MetaData(bind=conn, reflect=True)
|
||||
meta = sqlalchemy.MetaData(bind=conn)
|
||||
meta.reflect()
|
||||
|
||||
trans = conn.begin()
|
||||
|
||||
# drop all tables
|
||||
|
1
static/icons/clock.svg
Normal file
1
static/icons/clock.svg
Normal 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
1
static/icons/user.svg
Normal 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 |
@ -1,8 +1,6 @@
|
||||
.empty-state {
|
||||
padding: $gap * 3;
|
||||
max-width: 100%;
|
||||
max-width: $max-panel-width;
|
||||
background-color: $color-gray-lightest;
|
||||
margin-top: $gap * 5;
|
||||
|
||||
&--white {
|
||||
background-color: $color-white;
|
||||
@ -18,17 +16,28 @@
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1rem;
|
||||
padding: 3.2rem 2.4rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0 $gap * 3;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-left: -$gap * 3;
|
||||
margin-right: -$gap * 3;
|
||||
margin: $gap * 4 0 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
text-align: center;
|
||||
background-color: $color-gray-lightest;
|
||||
padding: $gap * 3;
|
||||
|
||||
a.usa-button {
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,7 @@
|
||||
background-color: $color-white;
|
||||
border-top: 1px solid $color-gray-lightest;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
padding: $gap * 1.5;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@ -13,8 +11,11 @@
|
||||
height: $footer-height;
|
||||
color: $color-gray-dark;
|
||||
font-size: 1.5rem;
|
||||
padding: 0 $gap * 1.5;
|
||||
|
||||
&__login {
|
||||
padding-left: 0.8rem;
|
||||
width: 100%;
|
||||
max-width: 1175px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
@ -22,15 +22,18 @@ body {
|
||||
padding-bottom: $footer-height * 2.5;
|
||||
|
||||
.global-panel-container {
|
||||
margin: $gap;
|
||||
flex-grow: 1;
|
||||
-ms-flex-negative: 1;
|
||||
top: $usa-banner-height + $topbar-height;
|
||||
position: relative;
|
||||
padding: 0 $large-spacing;
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin: $gap * 2;
|
||||
top: $usa-banner-height + $topbar-height;
|
||||
}
|
||||
|
||||
.user-edit {
|
||||
max-width: $max-panel-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,26 +3,34 @@
|
||||
@include grid-row;
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
margin-left: 2 * $gap;
|
||||
.portfolio-header-new .portfolio-header__name {
|
||||
padding: 1.6rem 0;
|
||||
}
|
||||
|
||||
.portfolio-header {
|
||||
flex-direction: column;
|
||||
margin: $gap * 2 0;
|
||||
max-width: $max-panel-width;
|
||||
|
||||
@include media($small-screen) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
margin-bottom: $gap * 1;
|
||||
|
||||
.col--grow {
|
||||
overflow: inherit;
|
||||
display: table;
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include h1;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
|
||||
h1 {
|
||||
margin: 0 $gap ($gap * 2) 0;
|
||||
margin: 0;
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
@ -30,6 +38,7 @@
|
||||
font-size: $small-font-size;
|
||||
margin: 0 0 (-$gap * 0.5);
|
||||
color: $color-gray-medium;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,9 +47,15 @@
|
||||
font-size: $small-font-size;
|
||||
|
||||
.icon-link {
|
||||
padding: $gap;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
color: $color-blue-darkest;
|
||||
min-width: 10rem;
|
||||
min-height: 10rem;
|
||||
|
||||
.col {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-aqua-lightest;
|
||||
@ -53,6 +68,7 @@
|
||||
&.active {
|
||||
color: $color-blue;
|
||||
background-color: $color-gray-lightest;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-aqua-lightest;
|
||||
@ -82,11 +98,19 @@
|
||||
margin-bottom: 3 * $gap;
|
||||
}
|
||||
|
||||
.portfolio-content {
|
||||
margin: (4 * $gap) $gap 0 $gap;
|
||||
.portfolio-admin {
|
||||
margin: $large-spacing 0;
|
||||
max-width: $max-panel-width;
|
||||
}
|
||||
|
||||
.portfolio-content {
|
||||
.panel {
|
||||
padding-bottom: 2rem;
|
||||
max-width: $max-panel-width;
|
||||
}
|
||||
|
||||
hr {
|
||||
max-width: $max-panel-width;
|
||||
}
|
||||
|
||||
a.add-new-button {
|
||||
@ -251,6 +275,7 @@
|
||||
|
||||
.portfolio-applications {
|
||||
margin-top: $gap * 5;
|
||||
max-width: $max-panel-width;
|
||||
|
||||
&__header {
|
||||
&--title {
|
||||
@ -296,8 +321,8 @@
|
||||
}
|
||||
|
||||
.portfolio-funding {
|
||||
padding: 2 * $gap;
|
||||
padding-top: 0;
|
||||
max-width: $max-panel-width;
|
||||
margin: $large-spacing 0;
|
||||
|
||||
.panel {
|
||||
@include shadow-panel;
|
||||
@ -366,6 +391,8 @@
|
||||
}
|
||||
|
||||
.portfolio-reports {
|
||||
max-width: $max-panel-width;
|
||||
|
||||
&__header {
|
||||
margin-bottom: 4 * $gap;
|
||||
|
||||
|
@ -20,12 +20,10 @@
|
||||
.sticky-cta-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 90rem;
|
||||
|
||||
.usa-button {
|
||||
margin: $gap $gap * 1.5 $gap 0;
|
||||
width: 20rem;
|
||||
height: 3.2rem;
|
||||
font-size: $small-font-size;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +40,10 @@
|
||||
&-buttons {
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
margin: 0;
|
||||
|
||||
|
@ -4,14 +4,15 @@
|
||||
height: $topbar-height;
|
||||
position: fixed;
|
||||
top: $usa-banner-height;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
|
||||
&__navigation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
max-width: 1190px;
|
||||
|
||||
a {
|
||||
color: $color-white;
|
||||
@ -64,3 +65,11 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.login-topbar .topbar__navigation {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.login-topbar .topbar__context .topbar__link-icon {
|
||||
margin: 0 0 0 0.8rem;
|
||||
}
|
||||
|
@ -41,7 +41,6 @@
|
||||
|
||||
&.col--grow {
|
||||
flex: 1 auto;
|
||||
padding-right: $spacing-small;
|
||||
}
|
||||
|
||||
&.col--half {
|
||||
|
@ -94,3 +94,7 @@ hr {
|
||||
margin: ($gap * 3) ($site-margins * -4);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ $footer-height: 5rem;
|
||||
$usa-banner-height: 2.8rem;
|
||||
$sidenav-expanded-width: 25rem;
|
||||
$sidenav-collapsed-width: 10rem;
|
||||
$max-panel-width: 80rem;
|
||||
$max-panel-width: 90rem;
|
||||
$home-pg-icon-width: 6rem;
|
||||
$large-spacing: 4rem;
|
||||
|
||||
/*
|
||||
* USWDS Variables
|
||||
@ -189,4 +190,4 @@ $spacing-x-small: 0.5rem;
|
||||
$spacing-small: 1rem;
|
||||
$spacing-md-small: 1.5rem;
|
||||
$spacing-medium: 2rem;
|
||||
$spacing-large: 3rem;
|
||||
$spacing-large: 4rem;
|
||||
|
@ -21,7 +21,7 @@
|
||||
text-transform: uppercase;
|
||||
|
||||
&--default {
|
||||
background-color: $color-gray-dark;
|
||||
background-color: $color-gray;
|
||||
}
|
||||
|
||||
&--info {
|
||||
|
@ -19,10 +19,7 @@
|
||||
}
|
||||
|
||||
@mixin panel-margin {
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: $site-margins-mobile * 6;
|
||||
margin: $spacing-large 0;
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin-bottom: $site-margins * 8;
|
||||
@ -56,9 +53,10 @@
|
||||
@include panel-theme-default;
|
||||
@include panel-margin;
|
||||
@include shadow-panel;
|
||||
max-width: $max-panel-width;
|
||||
|
||||
&__content {
|
||||
padding: $gap * 2;
|
||||
padding: 3.2rem 2.4rem;
|
||||
}
|
||||
|
||||
&__body {
|
||||
@ -66,7 +64,7 @@
|
||||
}
|
||||
|
||||
&__heading {
|
||||
padding: $gap * 2;
|
||||
padding: 3.2rem 2.4rem;
|
||||
|
||||
@include media($medium-screen) {
|
||||
padding: $gap * 4;
|
||||
|
@ -113,8 +113,8 @@
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&--active {
|
||||
@include h4;
|
||||
|
||||
font-size: $base-font-size;
|
||||
font-weight: $font-bold;
|
||||
background-color: $color-aqua-lightest !important;
|
||||
color: $color-primary-darker !important;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
|
||||
|
@ -1,12 +1,11 @@
|
||||
.home {
|
||||
margin: $gap * 3;
|
||||
.sticky-cta {
|
||||
margin: -1.6rem -1.6rem 0 -1.6rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin: 4rem;
|
||||
max-width: 900px;
|
||||
margin: $large-spacing 0;
|
||||
max-width: $max-panel-width;
|
||||
|
||||
&--descriptions {
|
||||
.col {
|
||||
@ -29,7 +28,7 @@
|
||||
background-color: $color-white;
|
||||
|
||||
.home-container {
|
||||
max-width: 90rem;
|
||||
max-width: $max-panel-width;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 8rem;
|
||||
|
@ -1,3 +1,4 @@
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/label.html" import Label %}
|
||||
{% from 'components/save_button.html' import SaveButton %}
|
||||
@ -10,10 +11,13 @@
|
||||
new_env_form) %}
|
||||
|
||||
<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">
|
||||
{% if g.matchesPath("application-environments") -%}
|
||||
{% include "fragments/flash.html" %}
|
||||
{%- endif %}
|
||||
{% if 0 == environments_obj | length -%}
|
||||
<div class="empty-state panel__content">
|
||||
<p class="empty-state__message">
|
||||
@ -30,14 +34,21 @@
|
||||
<li class="accordion-table__item">
|
||||
<div class="accordion-table__item-content">
|
||||
<div class="environment-list__item">
|
||||
<span>
|
||||
<a
|
||||
href='{{ url_for("applications.access_environment", environment_id=env.id)}}'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'>
|
||||
{{ env['name'] }} {{ Icon('link', classes='icon--medium icon--primary') }}
|
||||
</a>
|
||||
</span>
|
||||
{% if not env["pending"] -%}
|
||||
<span>
|
||||
<a
|
||||
href='{{ url_for("applications.access_environment", environment_id=env.id)}}'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'>
|
||||
{{ env['name'] }} {{ Icon('link', classes='icon--medium icon--primary') }}
|
||||
</a>
|
||||
</span>
|
||||
{% else -%}
|
||||
<span>
|
||||
{{ env['name'] }}
|
||||
</span>
|
||||
{{ Label(type="pending_creation", classes='label--below')}}
|
||||
{%- endif %}
|
||||
{% if user_can(permissions.EDIT_ENVIRONMENT) -%}
|
||||
{{
|
||||
ToggleButton(
|
||||
@ -57,10 +68,6 @@
|
||||
classes="environment-list__item__members"
|
||||
)
|
||||
}}
|
||||
<br>
|
||||
{% if env['pending'] -%}
|
||||
{{ Label(type="changes_pending", classes='label--below')}}
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -24,11 +24,8 @@
|
||||
{% if not portfolio.applications %}
|
||||
|
||||
{{ EmptyState(
|
||||
header="portfolios.applications.empty_state.header"|translate,
|
||||
message="portfolios.applications.empty_state.message"|translate,
|
||||
button_text="portfolios.applications.empty_state.button_text"|translate,
|
||||
resource='applications',
|
||||
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,
|
||||
) }}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
<div id='app-root'>
|
||||
{% include 'components/usa_header.html' %}
|
||||
|
||||
{% include 'navigation/topbar.html' %}
|
||||
<div class='login-topbar'>{% include 'navigation/topbar.html' %}</div>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
|
@ -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">
|
||||
<h3>{{ header }}</h3>
|
||||
<p>{{ message }}</p>
|
||||
<hr>
|
||||
<div class="empty-state__footer">
|
||||
{% if user_can_create %}
|
||||
{% if user_can_create -%}
|
||||
<hr>
|
||||
<div class="empty-state__footer">
|
||||
<a href="{{ button_link }}" class="usa-button usa-button-primary">{{ button_text }}</a>
|
||||
{% else %}
|
||||
<p>{{ view_only_text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
@ -9,6 +9,11 @@
|
||||
"text": "changes pending",
|
||||
"color": "default",
|
||||
},
|
||||
"pending_creation": {
|
||||
"icon": "clock",
|
||||
"text": "pending creation",
|
||||
"color": "default",
|
||||
},
|
||||
"ppoc": {"text": "primary point of contact"}
|
||||
} %}
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div class="topbar__context">
|
||||
{% if g.current_user %}
|
||||
<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>
|
||||
</a>
|
||||
<a href="#" class="topbar__link">
|
||||
|
@ -22,7 +22,7 @@
|
||||
{{ TextInput(portfolio_form.name, validation="portfolioName", optional=False) }}
|
||||
{{ TextInput(portfolio_form.description, validation="defaultTextAreaField", paragraph=True) }}
|
||||
<div class='edit-portfolio-name action-group'>
|
||||
{{ SaveButton(text='Save Changes', additional_classes='usa-button-big') }}
|
||||
{{ SaveButton(text='Save Changes') }}
|
||||
</div>
|
||||
</form>
|
||||
</base-form>
|
||||
|
@ -10,10 +10,11 @@
|
||||
|
||||
<main class="usa-section usa-content">
|
||||
{% include "fragments/flash.html" %}
|
||||
<div class='portfolio-header__name'>
|
||||
<p>{{ "portfolios.header" | translate }}</p>
|
||||
<h1>{{ "portfolios.new.title" | translate }}</h1>
|
||||
</div>
|
||||
<div class="portfolio-header-new">
|
||||
<div class='portfolio-header__name'>
|
||||
<p>{{ "portfolios.header" | translate }}</p>
|
||||
<h1>{{ 'portfolios.new.title' | translate }}</h1>
|
||||
</div>
|
||||
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
||||
<base-form inline-template>
|
||||
<div class="row">
|
||||
|
@ -6,17 +6,10 @@
|
||||
{% if not portfolio.applications %}
|
||||
|
||||
{% 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(
|
||||
header='portfolios.reports.empty_state.message' | translate,
|
||||
message=message,
|
||||
button_text="portfolios.applications.empty_state.button_text"|translate,
|
||||
resource='applications_reporting',
|
||||
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,
|
||||
) }}
|
||||
|
||||
|
@ -85,11 +85,8 @@
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{{ EmptyState(
|
||||
header="task_orders.empty_state.header"|translate,
|
||||
message="task_orders.empty_state.message"|translate,
|
||||
resource="task_orders",
|
||||
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),
|
||||
) }}
|
||||
{% endif %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class='col'>
|
||||
<div class='col user-edit'>
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
# ATAT Terraform
|
||||
Welcome! You've found the ATAT IaC configurations.
|
||||
|
||||
ATAT is configured using terraform and a wrapper script called `secrets-tool`. With `terraform` we can configure infrastructure in a programatic way and ensure consistency across environments.
|
||||
ATAT is configured using terraform and a wrapper script called `secrets-tool`. With `terraform` we can configure infrastructure in a programatic way and ensure consistency across environments.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
**modules/** - Terraform modules. These are modules that can be re-used for multiple environments.
|
||||
**modules/** - Terraform modules. These are modules that can be re-used for multiple environments.
|
||||
|
||||
**providers/** - Specific environment configurations. (dev,production, etc)
|
||||
|
||||
@ -92,7 +92,7 @@ Check the output for errors. Sometimes the syntax is valid, but some of the conf
|
||||
|
||||
# After running TF (Manual Steps)
|
||||
|
||||
## VM Scale Set
|
||||
## VM Scale Set
|
||||
After running terraform, we need to make a manual change to the VM Scale Set that is used in the kubernetes. Terraform has a bug that is not applying this as of `v1.40` of the `azurerm` provider.
|
||||
|
||||
In order to get the `SystemAssigned` identity to be set, it needs to be set manually in the console.
|
||||
@ -253,7 +253,7 @@ Uncomment the `backend {}` section in the `provider.tf` file. Once uncommented,
|
||||
|
||||
*Say `yes` to the question*
|
||||
|
||||
Now we need to update the Update `variables.tf` with the principals for the users in `admin_users` variable map. If these are not defined yet, just leave it as an empty set.
|
||||
Now we need to update the Update `variables.tf` with the principals for the users in `admin_users` variable map. If these are not defined yet, just leave it as an empty set.
|
||||
|
||||
Next, we'll create the operator keyvault.
|
||||
|
||||
@ -281,4 +281,25 @@ secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ cr
|
||||
|
||||
`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
|
||||
```
|
||||
|
@ -35,11 +35,3 @@ resource "azurerm_postgresql_virtual_network_rule" "sql" {
|
||||
subnet_id = var.subnet_id
|
||||
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"
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
output "db_name" {
|
||||
value = azurerm_postgresql_database.db.name
|
||||
}
|
@ -15,7 +15,7 @@ With both usernames and passwords generated, the application only needs to make
|
||||
Ex.
|
||||
```
|
||||
{
|
||||
'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl',
|
||||
'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl',
|
||||
'postgres_root_password': "2+[A@E4:C=ubb/#R#'n<p|wCW-|%q^"
|
||||
}
|
||||
```
|
||||
@ -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)
|
||||
|
||||
## 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
|
||||
|
||||
*Requirements*
|
||||
@ -76,4 +121,4 @@ secrets-tool secrets --keyvault https://operator-dev-keyvault.vault.azure.net/ l
|
||||
This will fetch all secrets from the keyvault specified. `secrets-tool` then converts the keys to a variable name that terraform will look for. Essentially it prepends the keys found in KeyVault with `TF_VAR` and then executes terraform as a subprocess with the injected environment variables.
|
||||
```
|
||||
secrets-tool terraform --keyvault https://operator-dev-keyvault.vault.azure.net/ plan
|
||||
```
|
||||
```
|
||||
|
143
terraform/secrets-tool/commands/database.py
Normal file
143
terraform/secrets-tool/commands/database.py
Normal 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)
|
4
terraform/secrets-tool/postgres-user.yaml
Normal file
4
terraform/secrets-tool/postgres-user.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
- PGPASSWORD:
|
||||
type: 'password'
|
||||
length: 30
|
@ -7,6 +7,7 @@ import logging
|
||||
|
||||
from commands.secrets import secrets
|
||||
from commands.terraform import terraform
|
||||
from commands.database import database
|
||||
|
||||
config.setup_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -21,6 +22,7 @@ def cli():
|
||||
# Add additional command groups
|
||||
cli.add_command(secrets)
|
||||
cli.add_command(terraform)
|
||||
cli.add_command(database)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@ -41,12 +43,12 @@ if __name__ == "__main__":
|
||||
val = keyvault.get_secret(secret)
|
||||
#print(val)
|
||||
os.environ[name] = val
|
||||
env = os.environ.copy()
|
||||
env = os.environ.copy()
|
||||
command = "{} {}".format(PROCESS, sys.argv[1])
|
||||
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, traceback.print_stack)
|
||||
'''
|
||||
'''
|
||||
|
@ -36,7 +36,7 @@ class SecretsLoader():
|
||||
load the secrets in to keyvault
|
||||
"""
|
||||
def __init__(self, yaml_file: str, keyvault: object):
|
||||
assert Path(yaml_file).exists()
|
||||
assert Path(yaml_file).exists()
|
||||
self.yaml_file = yaml_file
|
||||
self.keyvault = keyvault
|
||||
self.config = dict()
|
||||
@ -47,7 +47,7 @@ class SecretsLoader():
|
||||
def _load_yaml(self):
|
||||
with open(self.yaml_file) as handle:
|
||||
self.config = yaml.load(handle, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def _generate_secrets(self):
|
||||
secrets = GenerateSecrets(self.config).process_definition()
|
||||
self.secrets = secrets
|
||||
@ -60,12 +60,14 @@ class SecretsLoader():
|
||||
|
||||
class GenerateSecrets():
|
||||
"""
|
||||
Read the secrets definition and generate requiesite
|
||||
Read the secrets definition and generate requiesite
|
||||
secrets based on the type of secret and arguments
|
||||
provided
|
||||
"""
|
||||
def __init__(self, definitions: dict):
|
||||
self.definitions = definitions
|
||||
most_punctuation = string.punctuation.replace("'", "").replace('"', "")
|
||||
self.password_characters = string.ascii_letters + string.digits + most_punctuation
|
||||
|
||||
def process_definition(self):
|
||||
"""
|
||||
@ -101,9 +103,8 @@ class GenerateSecrets():
|
||||
# Types. Can be usernames, passwords, or in the future things like salted
|
||||
# tokens, uuid, or other specialized types
|
||||
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))
|
||||
|
||||
|
||||
def _generate_username(self, length: int):
|
||||
self.username_characters = string.ascii_letters
|
||||
return ''.join(secrets.choice(self.username_characters) for i in range(length))
|
||||
|
@ -1,11 +1,15 @@
|
||||
from unittest.mock import Mock
|
||||
import pytest
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from tests.factories import ApplicationFactory, EnvironmentFactory
|
||||
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
||||
|
||||
from atst.domain.csp.cloud import AzureCloudProvider
|
||||
from atst.domain.csp.cloud.models import (
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
@ -65,8 +69,8 @@ def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
|
||||
|
||||
|
||||
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(
|
||||
**spec_dict
|
||||
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = (
|
||||
spec_dict
|
||||
)
|
||||
|
||||
|
||||
@ -82,12 +86,30 @@ def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||
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):
|
||||
application = ApplicationFactory.create()
|
||||
|
||||
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"
|
||||
|
||||
|
99
tests/domain/cloud/test_models.py
Normal file
99
tests/domain/cloud/test_models.py
Normal 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"
|
||||
)
|
@ -1,3 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
|
||||
@ -196,3 +197,20 @@ def test_update_does_not_duplicate_names_within_portfolio():
|
||||
|
||||
with pytest.raises(AlreadyExistsError):
|
||||
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
|
||||
|
@ -71,7 +71,7 @@ def test_update_adds_clins():
|
||||
|
||||
def test_update_does_not_duplicate_clins():
|
||||
task_order = TaskOrderFactory.create(
|
||||
number="3453453456", create_clins=[{"number": "123"}, {"number": "456"}]
|
||||
number="3453453456123", create_clins=[{"number": "123"}, {"number": "456"}]
|
||||
)
|
||||
clins = [
|
||||
{
|
||||
@ -93,7 +93,7 @@ def test_update_does_not_duplicate_clins():
|
||||
]
|
||||
task_order = TaskOrders.update(
|
||||
task_order_id=task_order.id,
|
||||
number="0000000000",
|
||||
number="0000000000000",
|
||||
clins=clins,
|
||||
pdf={"filename": "sample.pdf", "object_name": "1234567"},
|
||||
)
|
||||
@ -170,3 +170,11 @@ def test_update_enforces_unique_number():
|
||||
dupe_task_order = TaskOrderFactory.create()
|
||||
with pytest.raises(AlreadyExistsError):
|
||||
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)
|
||||
|
@ -7,6 +7,7 @@ import datetime
|
||||
|
||||
from atst.forms import data
|
||||
from atst.models import *
|
||||
from atst.models.mixins.state_machines import FSMStates
|
||||
|
||||
from atst.domain.invitations import PortfolioInvitations
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
@ -121,6 +122,7 @@ class PortfolioFactory(Base):
|
||||
owner = kwargs.pop("owner", UserFactory.create())
|
||||
members = kwargs.pop("members", [])
|
||||
with_task_orders = kwargs.pop("task_orders", [])
|
||||
state = kwargs.pop("state", None)
|
||||
|
||||
portfolio = super()._create(model_class, *args, **kwargs)
|
||||
|
||||
@ -161,6 +163,12 @@ class PortfolioFactory(Base):
|
||||
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.task_orders = task_orders
|
||||
return portfolio
|
||||
|
@ -112,3 +112,37 @@ def test_no_number():
|
||||
http_request_form_data = {}
|
||||
form = TaskOrderForm(http_request_form_data)
|
||||
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()
|
||||
|
@ -72,6 +72,12 @@ def mock_secrets():
|
||||
return Mock(spec=secrets)
|
||||
|
||||
|
||||
def mock_identity():
|
||||
import azure.identity as identity
|
||||
|
||||
return Mock(spec=identity)
|
||||
|
||||
|
||||
class MockAzureSDK(object):
|
||||
def __init__(self):
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
@ -88,6 +94,7 @@ class MockAzureSDK(object):
|
||||
self.requests = mock_requests()
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
self.identity = mock_identity()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
@ -35,6 +35,7 @@ class TaskOrderPdfForm(Form):
|
||||
|
||||
class TaskOrderForm(Form):
|
||||
pdf = FormField(TaskOrderPdfForm, label="task_order_pdf")
|
||||
number = StringField(label="task_order_number", default="number")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -63,6 +64,12 @@ def multi_checkbox_input_macro(env):
|
||||
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
|
||||
def initial_value_form(scope="function"):
|
||||
return InitialValueForm()
|
||||
@ -170,3 +177,10 @@ def test_make_pop_date_range(env, app):
|
||||
index=1,
|
||||
)
|
||||
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")
|
||||
|
@ -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):
|
||||
user_session(task_order.portfolio.owner)
|
||||
form_data = {"number": "1234567890"}
|
||||
form_data = {"number": "abc-1234567890"}
|
||||
response = client.post(
|
||||
url_for(
|
||||
"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 task_order.number == "1234567890"
|
||||
assert task_order.number == "ABC1234567890" # pragma: allowlist secret
|
||||
|
||||
|
||||
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
|
||||
):
|
||||
user_session(task_order.portfolio.owner)
|
||||
form_data = {"number": "0000000000"}
|
||||
form_data = {"number": "0000000000000"}
|
||||
original_number = task_order.number
|
||||
response = client.post(
|
||||
url_for(
|
||||
@ -203,7 +203,7 @@ def test_task_orders_submit_form_step_two_add_number_existing_to(
|
||||
data=form_data,
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert task_order.number == "0000000000"
|
||||
assert task_order.number == "0000000000000"
|
||||
assert task_order.number != original_number
|
||||
|
||||
|
||||
|
@ -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):
|
||||
post_routes = [
|
||||
("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",
|
||||
{
|
||||
|
@ -8,9 +8,9 @@ from atst.domain.csp.cloud import MockCloudProvider
|
||||
from atst.domain.portfolios import Portfolios
|
||||
|
||||
from atst.jobs import (
|
||||
RecordEnvironmentFailure,
|
||||
RecordEnvironmentRoleFailure,
|
||||
RecordFailure,
|
||||
dispatch_create_environment,
|
||||
dispatch_create_application,
|
||||
dispatch_create_atat_admin_user,
|
||||
dispatch_provision_portfolio,
|
||||
dispatch_provision_user,
|
||||
@ -18,6 +18,7 @@ from atst.jobs import (
|
||||
do_provision_user,
|
||||
do_provision_portfolio,
|
||||
do_create_environment,
|
||||
do_create_application,
|
||||
do_create_atat_admin_user,
|
||||
)
|
||||
from atst.models.utils import claim_for_update
|
||||
@ -27,9 +28,10 @@ from tests.factories import (
|
||||
EnvironmentRoleFactory,
|
||||
PortfolioFactory,
|
||||
PortfolioStateMachineFactory,
|
||||
ApplicationFactory,
|
||||
ApplicationRoleFactory,
|
||||
)
|
||||
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus
|
||||
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="function")
|
||||
@ -43,8 +45,17 @@ def portfolio():
|
||||
return portfolio
|
||||
|
||||
|
||||
def test_environment_job_failure(celery_app, celery_worker):
|
||||
@celery_app.task(bind=True, base=RecordEnvironmentFailure)
|
||||
def _find_failure(session, entity, id_):
|
||||
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):
|
||||
raise ValueError("something bad happened")
|
||||
|
||||
@ -56,13 +67,12 @@ def test_environment_job_failure(celery_app, celery_worker):
|
||||
with pytest.raises(ValueError):
|
||||
task.get()
|
||||
|
||||
assert environment.job_failures
|
||||
job_failure = environment.job_failures[0]
|
||||
job_failure = _find_failure(session, "environment", str(environment.id))
|
||||
assert job_failure.task == task
|
||||
|
||||
|
||||
def test_environment_role_job_failure(celery_app, celery_worker):
|
||||
@celery_app.task(bind=True, base=RecordEnvironmentRoleFailure)
|
||||
def test_environment_role_job_failure(session, celery_app, celery_worker):
|
||||
@celery_app.task(bind=True, base=RecordFailure)
|
||||
def _fail_hard(self, environment_role_id=None):
|
||||
raise ValueError("something bad happened")
|
||||
|
||||
@ -74,8 +84,7 @@ def test_environment_role_job_failure(celery_app, celery_worker):
|
||||
with pytest.raises(ValueError):
|
||||
task.get()
|
||||
|
||||
assert role.job_failures
|
||||
job_failure = role.job_failures[0]
|
||||
job_failure = _find_failure(session, "environment_role", str(role.id))
|
||||
assert job_failure.task == task
|
||||
|
||||
|
||||
@ -99,6 +108,24 @@ def test_create_environment_job_is_idempotent(csp, session):
|
||||
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):
|
||||
environment = EnvironmentFactory.create(cloud_id="something")
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
portfolio = PortfolioFactory.create(
|
||||
applications=[
|
||||
|
16
tests/utils/test_hash.py
Normal file
16
tests/utils/test_hash.py
Normal 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
|
@ -84,6 +84,31 @@ email:
|
||||
application_invite: "{inviter_name} has invited you to a JEDI cloud application"
|
||||
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio"
|
||||
environment_ready: JEDI cloud environment ready
|
||||
empty_state:
|
||||
applications:
|
||||
header:
|
||||
edit: You don’t 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:
|
||||
application:
|
||||
created:
|
||||
@ -370,11 +395,6 @@ portfolios:
|
||||
add_member: Add Team Member
|
||||
add_another_environment: Add another environment
|
||||
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:
|
||||
step_1_header: Name and Describe New Application
|
||||
step_1_button_text: "Next: Add Environments"
|
||||
@ -417,6 +437,7 @@ portfolios:
|
||||
add_subscription: Add new subscription
|
||||
blank_slate: This Application has no environments
|
||||
disabled: ": Access Suspended"
|
||||
funding_alert: "Application environments will not be created until the {name} portfolio is funded."
|
||||
environments_heading: Application Environments
|
||||
existing_application_title: "{application_name} Application Settings"
|
||||
member_count: "{count} Members"
|
||||
@ -482,12 +503,6 @@ portfolios:
|
||||
header: Funding Duration
|
||||
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.
|
||||
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:
|
||||
header: Total Portfolio Value
|
||||
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_review_text: Review Changes
|
||||
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:
|
||||
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.
|
||||
|
@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New Application-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -160,7 +160,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -413,28 +413,13 @@ Imported from: AT-AT CI - login-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -449,7 +434,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -189,12 +189,12 @@ Imported from: AT-AT CI - login-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</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>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td>
|
||||
<td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New Application-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -656,28 +640,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>Business Read-only</td>
|
||||
<td>CONTRIBUTOR</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -761,13 +730,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>*Business Read-only*</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>*Contributor*</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<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.02" />
|
||||
<meta name="ghost-inspector-screenshotCompareThreshold" content="0.03" />
|
||||
</head>
|
||||
<body>
|
||||
<table cellpadding="1" cellspacing="1" border="1">
|
||||
@ -174,7 +174,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -192,7 +192,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#name</td>
|
||||
<td>Tatooine Energy Maintenance Systems</td>
|
||||
<td>Tatooine Energy Maintenance Systems ${alphanumeric}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -291,29 +291,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.panel__content > p:nth-of-type(2)</td>
|
||||
<td>css=th.table-cell--third</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 - 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>css=th.table-cell--third</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -331,41 +314,7 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=button.usa-button.usa-button-primary.usa-button-big</td>
|
||||
<td>Save</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>
|
||||
<td>Save Changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -375,12 +324,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -391,13 +340,13 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=#add-port-mem > div > div:nth-of-type(1) > h1</td>
|
||||
<td>*Invite new portfolio member*</td>
|
||||
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
|
||||
<td>*Add Manager*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -487,13 +436,13 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=#add-port-mem > div > div:nth-of-type(2) > h1</td>
|
||||
<td>*Assign member permissions*</td>
|
||||
<td>css=#add-portfolio-manager > div > div > div.member-form > h2</td>
|
||||
<td>*Set Portfolio Permissions*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -503,12 +452,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt</td>
|
||||
<td>css=#perms_app_mgmt-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt</td>
|
||||
<td>css=#perms_app_mgmt-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -519,12 +468,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td>
|
||||
<td>css=#perms_funding-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_app_mgmt > option:nth-of-type(1)</td>
|
||||
<td>css=#perms_funding-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -535,12 +484,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_funding</td>
|
||||
<td>css=#perms_reporting-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>click</td>
|
||||
<td>css=#permission_sets-perms_funding</td>
|
||||
<td>css=#perms_reporting-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -551,60 +500,12 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=#permission_sets-perms_funding > option:nth-of-type(1)</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>css=#perms_portfolio_mgmt-None</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#permission_sets-perms_portfolio_mgmt</td>
|
||||
<td>css=#perms_portfolio_mgmt-None</td>
|
||||
<td>edit_portfolio_admin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -615,22 +516,6 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
@ -647,12 +532,75 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -660,105 +608,59 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<!--Imported from: AT-AT CI - New Portfolio Member-->
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.usa-alert-body > p:nth-of-type(2)</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>css=h3.usa-alert-heading</td>
|
||||
<td>*Success!*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -768,13 +670,13 @@ Imported from: AT-AT CI - Portfolio Settings-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForElementPresent</td>
|
||||
<td>css=.usa-alert-body > p:nth-of-type(2)</td>
|
||||
<td>css=.usa-alert-text</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.usa-alert-body > p:nth-of-type(2)</td>
|
||||
<td>*You have successfully updated access permissions for members of Tatooine Energy Maintenance Systems.*</td>
|
||||
<td>css=.usa-alert-text</td>
|
||||
<td>*You have successfully updated access permissions for*</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -106,7 +106,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -111,7 +111,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -170,7 +170,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -640,28 +640,13 @@ Imported from: AT-AT CI - New App Step 1-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=[name=environment_roles-0-role]</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -676,7 +661,7 @@ Imported from: AT-AT CI - New App Step 1-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=[name=environment_roles-1-role]</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -96,7 +96,7 @@
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -165,7 +165,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -275,22 +275,6 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Portfolio Settings-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
@ -320,22 +304,6 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<td></td>
|
||||
<td></td>
|
||||
</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>
|
||||
<td>waitForElementPresent</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>
|
||||
<td>assertText</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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -204,21 +204,6 @@ Imported from: AT-AT CI - login-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
@ -242,21 +227,6 @@ Imported from: AT-AT CI - login-->
|
||||
<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>
|
||||
<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>
|
||||
</table>
|
||||
</body>
|
||||
|
@ -106,7 +106,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -200,12 +200,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<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>
|
||||
</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>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td>
|
||||
<td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -101,7 +101,7 @@ Imported from: AT-AT CI - login-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
@ -111,7 +111,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -211,12 +211,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<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>
|
||||
</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>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td>
|
||||
<td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -111,7 +111,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -211,12 +211,12 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<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>
|
||||
</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>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td>
|
||||
<td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -841,7 +841,7 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<td>assertText</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>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -856,7 +856,7 @@ Imported from: AT-AT CI - Create New TO-->
|
||||
<tr>
|
||||
<td>assertText</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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New Application-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -583,12 +567,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT Holding - Create TO after other steps-->
|
||||
<tr>
|
||||
<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>
|
||||
</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>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td>
|
||||
<td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New Application-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -583,12 +567,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT Holding - Create TO after other steps-->
|
||||
<tr>
|
||||
<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>
|
||||
</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>css=.sticky-cta-buttons > .usa-button.usa-button-primary</td>
|
||||
<td>css=.empty-state__footer > .usa-button.usa-button-primary</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New Application-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -626,12 +610,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -641,12 +625,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -656,12 +640,12 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -671,28 +655,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
</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>*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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>assertText</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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
639
uitests/Resend_Portfolio_Member_Invite.html
Normal file
639
uitests/Resend_Portfolio_Member_Invite.html
Normal 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>
|
@ -169,7 +169,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>assertText</td>
|
||||
<td>css=.sticky-cta-text > h3</td>
|
||||
<td>*Create New Portfolio*</td>
|
||||
<td>*Name and Describe Portfolio*</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -439,29 +439,13 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<!--Imported from: AT-AT CI - Create New Application-->
|
||||
<tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-0-role-None</td>
|
||||
<td>Basic Access</td>
|
||||
<td>ADMIN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
@ -477,7 +461,7 @@ Imported from: AT-AT CI - New Portfolio-->
|
||||
<tr>
|
||||
<td>type</td>
|
||||
<td>css=#environment_roles-1-role-None</td>
|
||||
<td>Network Admin</td>
|
||||
<td>BILLING_READ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>waitForPageToLoad</td>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user