{% if not portfolio.applications %}
{{ EmptyState(
- 'This portfolio doesn’t have any applications',
- action_label='Add a new application' if can_create_applications else None,
- action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
- icon='cloud',
- sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.',
- add_perms=can_create_applications
+ header="portfolios.applications.empty_state.header"|translate,
+ message="portfolios.applications.empty_state.message"|translate,
+ button_text="portfolios.applications.empty_state.button_text"|translate,
+ button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id),
+ view_only_text="portfolios.applications.empty_state.view_only_text"|translate,
+ user_can_create=can_create_applications,
) }}
{% else %}
-
-
+ {% call AccordionList() %}
{% for application in portfolio.applications|sort(attribute='name') %}
{% set section_name = "application-{}".format(application.id) %}
-
-
-
{% include "portfolios/reports/portfolio_summary.html" %}
diff --git a/templates/portfolios/reports/obligated_funds.html b/templates/portfolios/reports/obligated_funds.html
index c8d6ff22..43046472 100644
--- a/templates/portfolios/reports/obligated_funds.html
+++ b/templates/portfolios/reports/obligated_funds.html
@@ -3,28 +3,61 @@
Current Obligated funds
- As of {{ now | formattedDate(formatter="%B %d, %Y at %H:%M") }}
+ As of {{ retrieved | formattedDate(formatter="%B %d, %Y at %H:%M") }}
- {% for JEDI_clin, funds in current_obligated_funds.items() %}
- {% set remaining_funds = (funds["obligated_funds"] - funds["expended_funds"]) %}
+ {% for JEDI_clin in current_obligated_funds | sort(attribute='name')%}
{% endmacro %}
-{% call StickyCTA(text="Funding") %}
- {% if user_can(permissions.CREATE_TASK_ORDER) %}
- Start a new task order
+{% call StickyCTA(text="common.task_orders"|translate) %}
+ {% if user_can(permissions.CREATE_TASK_ORDER) and task_orders %}
+
+ {{ "task_orders.add_new_button" | translate }}
+
{% endif %}
{% endcall %}
@@ -103,15 +77,20 @@
- {% if task_orders %}
- {{ TaskOrderList(task_orders) }}
+ {% if to_count > 0 %}
+ {% call AccordionList() %}
+ {% for status, to_list in task_orders.items() %}
+ {{ TaskOrderList(to_list, status) }}
+ {% endfor %}
+ {% endcall %}
{% else %}
{{ EmptyState(
- 'This portfolio doesn’t have any active or pending task orders.',
- action_label='Add a New Task Order',
- action_href=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id),
- icon='cloud',
- add_perms=user_can(permissions.CREATE_TASK_ORDER)
+ header="task_orders.empty_state.header"|translate,
+ message="task_orders.empty_state.message"|translate,
+ 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 %}
diff --git a/terraform/.gitignore b/terraform/.gitignore
new file mode 100644
index 00000000..3fa8c86b
--- /dev/null
+++ b/terraform/.gitignore
@@ -0,0 +1 @@
+.terraform
diff --git a/terraform/modules/k8s/main.tf b/terraform/modules/k8s/main.tf
new file mode 100644
index 00000000..93e84141
--- /dev/null
+++ b/terraform/modules/k8s/main.tf
@@ -0,0 +1,35 @@
+resource "azurerm_resource_group" "k8s" {
+ name = "${var.name}-${var.environment}-vpc"
+ location = var.region
+}
+
+resource "azurerm_kubernetes_cluster" "k8s" {
+ name = "${var.name}-${var.environment}-k8s"
+ location = azurerm_resource_group.k8s.location
+ resource_group_name = azurerm_resource_group.k8s.name
+ dns_prefix = var.k8s_dns_prefix
+
+ service_principal {
+ client_id = "f05a4457-bd5e-4c63-98e1-89aab42645d0"
+ client_secret = "19b69e2c-9f55-4850-87cb-88c67a8dc811"
+ }
+
+ default_node_pool {
+ name = "default"
+ vm_size = "Standard_D1_v2"
+ os_disk_size_gb = 30
+ vnet_subnet_id = var.vnet_subnet_id
+ node_count = 1
+ }
+
+ lifecycle {
+ ignore_changes = [
+ default_node_pool.0.node_count
+ ]
+ }
+
+ tags = {
+ environment = var.environment
+ owner = var.owner
+ }
+}
\ No newline at end of file
diff --git a/terraform/modules/k8s/outputs.tf b/terraform/modules/k8s/outputs.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/terraform/modules/k8s/variables.tf b/terraform/modules/k8s/variables.tf
new file mode 100644
index 00000000..7a3663ce
--- /dev/null
+++ b/terraform/modules/k8s/variables.tf
@@ -0,0 +1,35 @@
+variable "region" {
+ type = string
+ description = "Region this module and resources will be created in"
+}
+
+variable "name" {
+ type = string
+ description = "Unique name for the services in this module"
+}
+
+variable "environment" {
+ type = string
+ description = "Environment these resources reside (prod, dev, staging, etc)"
+}
+
+variable "owner" {
+ type = string
+ description = "Owner of the environment and resources created in this module"
+}
+
+variable "k8s_dns_prefix" {
+ type = string
+ description = "A DNS prefix"
+}
+
+variable "k8s_node_size" {
+ type = string
+ description = "The size of the instance to use in the node pools for k8s"
+ default = "Standard_A1_v2"
+}
+
+variable "vnet_subnet_id" {
+ description = "Subnet to use for the default k8s pool"
+ type = string
+}
diff --git a/terraform/modules/keyvault/main.tf b/terraform/modules/keyvault/main.tf
new file mode 100644
index 00000000..d4208e36
--- /dev/null
+++ b/terraform/modules/keyvault/main.tf
@@ -0,0 +1,40 @@
+data "azurerm_client_config" "current" {}
+
+resource "azurerm_resource_group" "keyvault" {
+ name = "${var.name}-${var.environment}-rg"
+ location = var.region
+}
+
+resource "azurerm_key_vault" "keyvault" {
+ name = "${var.name}-${var.environment}-keyvault"
+ location = azurerm_resource_group.keyvault.location
+ resource_group_name = azurerm_resource_group.keyvault.name
+ tenant_id = data.azurerm_client_config.current.tenant_id
+
+ sku_name = "premium"
+
+ tags = {
+ environment = var.environment
+ owner = var.owner
+ }
+}
+
+resource "azurerm_key_vault_access_policy" "keyvault" {
+ key_vault_id = azurerm_key_vault.keyvault.id
+
+ tenant_id = "b5ab0e1e-09f8-4258-afb7-fb17654bc5b3"
+ object_id = "2ca63d41-d058-4e06-aef6-eb517a53b631"
+
+ key_permissions = [
+ "get",
+ "list",
+ "create",
+ ]
+
+ secret_permissions = [
+ "get",
+ "list",
+ "set",
+ ]
+}
+
diff --git a/terraform/modules/keyvault/variables.tf b/terraform/modules/keyvault/variables.tf
new file mode 100644
index 00000000..f6b7b429
--- /dev/null
+++ b/terraform/modules/keyvault/variables.tf
@@ -0,0 +1,24 @@
+variable "region" {
+ type = string
+ description = "Region this module and resources will be created in"
+}
+
+variable "name" {
+ type = string
+ description = "Unique name for the services in this module"
+}
+
+variable "environment" {
+ type = string
+ description = "Environment these resources reside (prod, dev, staging, etc)"
+}
+
+variable "owner" {
+ type = string
+ description = "Owner of this environment"
+}
+
+variable "tenant_id" {
+ type = string
+ description = "The Tenant ID"
+}
diff --git a/terraform/modules/postgres/main.tf b/terraform/modules/postgres/main.tf
new file mode 100644
index 00000000..860ece56
--- /dev/null
+++ b/terraform/modules/postgres/main.tf
@@ -0,0 +1,37 @@
+resource "azurerm_resource_group" "sql" {
+ name = "${var.name}-${var.environment}-postgres"
+ location = var.region
+}
+
+resource "azurerm_postgresql_server" "sql" {
+ name = "${var.name}-${var.environment}-sql"
+ location = azurerm_resource_group.sql.location
+ resource_group_name = azurerm_resource_group.sql.name
+
+ sku {
+ name = var.sku_name
+ capacity = var.sku_capacity
+ tier = var.sku_tier
+ family = var.sku_family
+ }
+
+ storage_profile {
+ storage_mb = var.storage_mb
+ backup_retention_days = var.storage_backup_retention_days
+ geo_redundant_backup = var.storage_geo_redundant_backup
+ auto_grow = var.storage_auto_grow
+ }
+
+ administrator_login = var.administrator_login
+ administrator_login_password = var.administrator_login_password
+ version = var.postgres_version
+ ssl_enforcement = var.ssl_enforcement
+}
+
+resource "azurerm_postgresql_virtual_network_rule" "sql" {
+ name = "${var.name}-${var.environment}-rule"
+ resource_group_name = azurerm_resource_group.sql.name
+ server_name = azurerm_postgresql_server.sql.name
+ subnet_id = var.subnet_id
+ ignore_missing_vnet_service_endpoint = true
+}
\ No newline at end of file
diff --git a/terraform/modules/postgres/outputs.tf b/terraform/modules/postgres/outputs.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/terraform/modules/postgres/variables.tf b/terraform/modules/postgres/variables.tf
new file mode 100644
index 00000000..3346ff8f
--- /dev/null
+++ b/terraform/modules/postgres/variables.tf
@@ -0,0 +1,100 @@
+variable "region" {
+ type = string
+ description = "Region this module and resources will be created in"
+}
+
+variable "name" {
+ type = string
+ description = "Unique name for the services in this module"
+}
+
+variable "environment" {
+ type = string
+ description = "Environment these resources reside (prod, dev, staging, etc)"
+}
+
+variable "owner" {
+ type = string
+ description = "Owner of the environment and resources created in this module"
+}
+
+variable "subnet_id" {
+ type = string
+ description = "Subnet the SQL server should run"
+}
+
+variable "sku_name" {
+ type = string
+ description = "SKU name"
+ default = "GP_Gen5_2"
+}
+
+variable "sku_capacity" {
+ type = string
+ description = "SKU Capacity"
+ default = "2"
+}
+
+variable "sku_tier" {
+ type = string
+ description = "SKU Tier"
+ default = "GeneralPurpose"
+
+}
+
+variable "sku_family" {
+ type = string
+ description = "SKU Family"
+ default = "Gen5"
+}
+
+variable "storage_mb" {
+ type = string
+ description = "Size in MB of the storage used for the sql server"
+ default = "5120"
+}
+
+
+variable "storage_backup_retention_days" {
+ type = string
+ description = "Storage backup retention (days)"
+ default = "7"
+}
+
+variable "storage_geo_redundant_backup" {
+ type = string
+ description = "Geographic redundant backup (Enabled/Disabled)"
+ default = "Disabled"
+}
+
+variable "storage_auto_grow" {
+ type = string
+ description = "Auto Grow? (Enabled/Disabled)"
+ default = "Enabled"
+}
+
+variable "administrator_login" {
+ type = string
+ description = "Administrator login"
+ default = "sqladmindude" # FIXME - Remove with wrapper using KeyVault
+}
+
+variable "administrator_login_password" {
+ type = string
+ description = "Administrator password"
+ default = "eI0l7yswwtuhHpwzoVjwRKdAcuGNsg" # FIXME - Remove with wrapper using KeyVault
+}
+
+
+variable "postgres_version" {
+ type = string
+ description = "Postgres version to use"
+ default = "11"
+}
+
+variable "ssl_enforcement" {
+ type = string
+ description = "Enforce SSL (Enabled/Disable)"
+ default = "Enabled"
+}
+
diff --git a/terraform/modules/vpc/main.tf b/terraform/modules/vpc/main.tf
new file mode 100644
index 00000000..e614b9e4
--- /dev/null
+++ b/terraform/modules/vpc/main.tf
@@ -0,0 +1,72 @@
+resource "azurerm_resource_group" "vpc" {
+ name = "${var.name}-${var.environment}-vpc"
+ location = var.region
+
+ tags = {
+ environment = var.environment
+ owner = var.owner
+ }
+}
+
+resource "azurerm_network_ddos_protection_plan" "vpc" {
+ count = var.ddos_enabled
+ name = "${var.name}-${var.environment}-ddos"
+ location = azurerm_resource_group.vpc.location
+ resource_group_name = azurerm_resource_group.vpc.name
+}
+
+resource "azurerm_virtual_network" "vpc" {
+ name = "${var.name}-${var.environment}-network"
+ location = azurerm_resource_group.vpc.location
+ resource_group_name = azurerm_resource_group.vpc.name
+ address_space = ["${var.virtual_network}"]
+ dns_servers = var.dns_servers
+
+ tags = {
+ environment = var.environment
+ owner = var.owner
+ }
+}
+
+resource "azurerm_subnet" "subnet" {
+ for_each = var.networks
+ name = "${var.name}-${var.environment}-${each.key}"
+ resource_group_name = azurerm_resource_group.vpc.name
+ virtual_network_name = azurerm_virtual_network.vpc.name
+ address_prefix = element(split(",", each.value), 0)
+
+ # See https://github.com/terraform-providers/terraform-provider-azurerm/issues/3471
+ lifecycle {
+ ignore_changes = [route_table_id]
+ }
+ #delegation {
+ # name = "acctestdelegation"
+ #
+ # service_delegation {
+ # name = "Microsoft.ContainerInstance/containerGroups"
+ # actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
+ # }
+ #}
+}
+
+resource "azurerm_route_table" "route_table" {
+ for_each = var.route_tables
+ name = "${var.name}-${var.environment}-${each.key}"
+ location = azurerm_resource_group.vpc.location
+ resource_group_name = azurerm_resource_group.vpc.name
+}
+
+resource "azurerm_subnet_route_table_association" "route_table" {
+ for_each = var.networks
+ subnet_id = azurerm_subnet.subnet[each.key].id
+ route_table_id = azurerm_route_table.route_table[each.key].id
+}
+
+resource "azurerm_route" "route" {
+ for_each = var.route_tables
+ name = "${var.name}-${var.environment}-default"
+ resource_group_name = azurerm_resource_group.vpc.name
+ route_table_name = azurerm_route_table.route_table[each.key].name
+ address_prefix = "0.0.0.0/0"
+ next_hop_type = each.value
+}
diff --git a/terraform/modules/vpc/outputs.tf b/terraform/modules/vpc/outputs.tf
new file mode 100644
index 00000000..eedaab6c
--- /dev/null
+++ b/terraform/modules/vpc/outputs.tf
@@ -0,0 +1,3 @@
+output "subnets" {
+ value = azurerm_subnet.subnet["private"].id #FIXME - output should be a map
+}
diff --git a/terraform/modules/vpc/variables.tf b/terraform/modules/vpc/variables.tf
new file mode 100644
index 00000000..ab2aa894
--- /dev/null
+++ b/terraform/modules/vpc/variables.tf
@@ -0,0 +1,43 @@
+variable "environment" {
+ description = "Environment (Prod,Dev,etc)"
+}
+
+variable "region" {
+ description = "Region (useast2, etc)"
+
+}
+
+variable "name" {
+ description = "Name or prefix to use for all resources created by this module"
+}
+
+variable "owner" {
+ description = "Owner of these resources"
+
+}
+
+variable "ddos_enabled" {
+ description = "Enable or disable DDoS Protection (1,0)"
+ default = "0"
+}
+
+variable "virtual_network" {
+ description = "The supernet used for this VPC a.k.a Virtual Network"
+ type = string
+}
+
+variable "networks" {
+ description = "A map of lists describing the network topology"
+ type = map
+}
+
+variable "dns_servers" {
+ description = "DNS Server IPs for internal and public DNS lookups (must be on a defined subnet)"
+ type = list
+
+}
+
+variable "route_tables" {
+ type = map
+ description = "A map with the route tables to create"
+}
diff --git a/terraform/providers/dev/k8s.tf b/terraform/providers/dev/k8s.tf
new file mode 100644
index 00000000..b41df8a4
--- /dev/null
+++ b/terraform/providers/dev/k8s.tf
@@ -0,0 +1,11 @@
+module "k8s" {
+ source = "../../modules/k8s"
+ region = var.region
+ name = var.name
+ environment = var.environment
+ owner = var.owner
+ k8s_dns_prefix = var.k8s_dns_prefix
+ k8s_node_size = var.k8s_node_size
+ vnet_subnet_id = module.vpc.subnets #FIXME - output from module.vpc.subnets should be map
+}
+
diff --git a/terraform/providers/dev/keyvault.tf b/terraform/providers/dev/keyvault.tf
new file mode 100644
index 00000000..009cd93f
--- /dev/null
+++ b/terraform/providers/dev/keyvault.tf
@@ -0,0 +1,8 @@
+module "keyvault" {
+ source = "../../modules/keyvault"
+ name = var.name
+ region = var.region
+ owner = var.owner
+ environment = var.environment
+ tenant_id = var.tenant_id
+}
diff --git a/terraform/providers/dev/postgres.tf b/terraform/providers/dev/postgres.tf
new file mode 100644
index 00000000..89f06e0d
--- /dev/null
+++ b/terraform/providers/dev/postgres.tf
@@ -0,0 +1,8 @@
+module "sql" {
+ source = "../../modules/postgres"
+ name = var.name
+ owner = var.owner
+ environment = var.environment
+ region = var.region
+ subnet_id = module.vpc.subnets # FIXME - Should be a map of subnets and specify private
+}
diff --git a/terraform/providers/dev/provider.tf b/terraform/providers/dev/provider.tf
new file mode 100644
index 00000000..0d225638
--- /dev/null
+++ b/terraform/providers/dev/provider.tf
@@ -0,0 +1,17 @@
+provider "azurerm" {
+ version = "=1.38.0"
+}
+
+provider "azuread" {
+ # Whilst version is optional, we /strongly recommend/ using it to pin the version of the Provider being used
+ version = "=0.7.0"
+}
+
+terraform {
+ backend "azurerm" {
+ resource_group_name = "cloudzero-dev-tfstate"
+ storage_account_name = "cloudzerodevtfstate"
+ container_name = "tfstate"
+ key = "dev.terraform.tfstate"
+ }
+}
diff --git a/terraform/providers/dev/variables.tf b/terraform/providers/dev/variables.tf
new file mode 100644
index 00000000..7a9eea21
--- /dev/null
+++ b/terraform/providers/dev/variables.tf
@@ -0,0 +1,61 @@
+variable "environment" {
+ default = "dev"
+}
+
+variable "region" {
+ default = "eastus2"
+
+}
+
+variable "owner" {
+ default = "dev"
+}
+
+variable "name" {
+ default = "cloudzero"
+}
+
+variable "virtual_network" {
+ type = string
+ default = "10.1.0.0/16"
+}
+
+
+variable "networks" {
+ type = map
+ default = {
+ #format
+ #name = "CIDR, route table, Security Group Name"
+ public = "10.1.1.0/24,public" # LBs
+ private = "10.1.2.0/24,private" # k8s, postgres, redis, dns, ad
+ }
+}
+
+variable "route_tables" {
+ description = "Route tables and their default routes"
+ type = map
+ default = {
+ public = "Internet"
+ private = "VnetLocal"
+ }
+}
+
+variable "dns_servers" {
+ type = list
+ default = ["10.1.2.4", "10.1.2.5"]
+}
+
+variable "k8s_node_size" {
+ type = string
+ default = "Standard_A1_v2"
+}
+
+variable "k8s_dns_prefix" {
+ type = string
+ default = "atat"
+}
+
+variable "tenant_id" {
+ type = string
+ default = "b5ab0e1e-09f8-4258-afb7-fb17654bc5b3"
+}
diff --git a/terraform/providers/dev/vpc.tf b/terraform/providers/dev/vpc.tf
new file mode 100644
index 00000000..0b930a0d
--- /dev/null
+++ b/terraform/providers/dev/vpc.tf
@@ -0,0 +1,12 @@
+module "vpc" {
+ source = "../../modules/vpc/"
+ environment = var.environment
+ region = var.region
+ virtual_network = var.virtual_network
+ networks = var.networks
+ route_tables = var.route_tables
+ owner = var.owner
+ name = var.name
+ dns_servers = var.dns_servers
+}
+
diff --git a/tests/domain/cloud/reports/__init__.py b/tests/domain/cloud/reports/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/domain/cloud/reports/test_reports.py b/tests/domain/cloud/reports/test_reports.py
new file mode 100644
index 00000000..75dae24f
--- /dev/null
+++ b/tests/domain/cloud/reports/test_reports.py
@@ -0,0 +1,50 @@
+from atst.domain.csp.reports import MockReportingProvider
+
+
+def test_get_environment_monthly_totals():
+ environment = {
+ "name": "Test Environment",
+ "spending": {
+ "this_month": {"JEDI_CLIN_1": 100, "JEDI_CLIN_2": 100},
+ "last_month": {"JEDI_CLIN_1": 200, "JEDI_CLIN_2": 200},
+ "total": {"JEDI_CLIN_1": 1000, "JEDI_CLIN_2": 1000},
+ },
+ }
+ totals = MockReportingProvider._get_environment_monthly_totals(environment)
+ assert totals == {
+ "name": "Test Environment",
+ "this_month": 200,
+ "last_month": 400,
+ "total": 2000,
+ }
+
+
+def test_get_application_monthly_totals():
+ application = {
+ "name": "Test Application",
+ "environments": [
+ {
+ "name": "Z",
+ "spending": {
+ "this_month": {"JEDI_CLIN_1": 50, "JEDI_CLIN_2": 50},
+ "last_month": {"JEDI_CLIN_1": 150, "JEDI_CLIN_2": 150},
+ "total": {"JEDI_CLIN_1": 250, "JEDI_CLIN_2": 250},
+ },
+ },
+ {
+ "name": "A",
+ "spending": {
+ "this_month": {"JEDI_CLIN_1": 100, "JEDI_CLIN_2": 100},
+ "last_month": {"JEDI_CLIN_1": 200, "JEDI_CLIN_2": 200},
+ "total": {"JEDI_CLIN_1": 1000, "JEDI_CLIN_2": 1000},
+ },
+ },
+ ],
+ }
+
+ totals = MockReportingProvider._get_application_monthly_totals(application)
+ assert totals["name"] == "Test Application"
+ assert totals["this_month"] == 300
+ assert totals["last_month"] == 700
+ assert totals["total"] == 2500
+ assert [env["name"] for env in totals["environments"]] == ["A", "Z"]
diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py
index 19ad63c8..39c6655e 100644
--- a/tests/domain/cloud/test_azure_csp.py
+++ b/tests/domain/cloud/test_azure_csp.py
@@ -1,27 +1,81 @@
import pytest
+from unittest.mock import Mock
from uuid import uuid4
from atst.domain.csp.cloud import AzureCloudProvider
from tests.mock_azure import mock_azure, AUTH_CREDENTIALS
-from tests.factories import EnvironmentFactory
+from tests.factories import EnvironmentFactory, ApplicationFactory
-def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
+# TODO: Directly test create subscription, provide all args √
+# TODO: Test create environment (create management group with parent)
+# TODO: Test create application (create manageemnt group with parent)
+# Create reusable mock for mocking the management group calls for multiple services
+#
+
+
+def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create()
subscription_id = str(uuid4())
+ credentials = mock_azure._get_credential_obj(AUTH_CREDENTIALS)
+ display_name = "Test Subscription"
+ billing_profile_id = str(uuid4())
+ sku_id = str(uuid4())
+ management_group_id = (
+ environment.cloud_id # environment.csp_details.management_group_id?
+ )
+ billing_account_name = (
+ "?" # environment.application.portfilio.csp_details.billing_account.name?
+ )
+ invoice_section_name = "?" # environment.name? or something specific to billing?
+
mock_azure.sdk.subscription.SubscriptionClient.return_value.subscription_factory.create_subscription.return_value.result.return_value.subscription_link = (
f"subscriptions/{subscription_id}"
)
+ result = mock_azure._create_subscription(
+ credentials,
+ display_name,
+ billing_profile_id,
+ sku_id,
+ management_group_id,
+ billing_account_name,
+ invoice_section_name,
+ )
+
+ assert result == subscription_id
+
+
+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
+ )
+
+
+def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
+ environment = EnvironmentFactory.create()
+
+ mock_management_group_create(mock_azure, {"id": "Test Id"})
+
result = mock_azure.create_environment(
AUTH_CREDENTIALS, environment.creator, environment
)
- assert result == subscription_id
+ assert result.id == "Test Id"
+
+
+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)
+
+ assert result.id == "Test Id"
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
diff --git a/tests/domain/test_environment_roles.py b/tests/domain/test_environment_roles.py
index 486da70a..216f072e 100644
--- a/tests/domain/test_environment_roles.py
+++ b/tests/domain/test_environment_roles.py
@@ -91,6 +91,42 @@ def test_disable_completed(application_role, environment):
assert environment_role.disabled
+def test_disable_checks_env_provisioning_status(session):
+ environment = EnvironmentFactory.create()
+ assert environment.is_pending
+ env_role1 = EnvironmentRoleFactory.create(environment=environment)
+ env_role1 = EnvironmentRoles.disable(env_role1.id)
+ assert env_role1.disabled
+
+ environment.cloud_id = "cloud-id"
+ environment.root_user_info = {"credentials": "credentials"}
+ session.add(environment)
+ session.commit()
+ session.refresh(environment)
+
+ assert not environment.is_pending
+ env_role2 = EnvironmentRoleFactory.create(environment=environment)
+ env_role2 = EnvironmentRoles.disable(env_role2.id)
+ assert env_role2.disabled
+
+
+def test_disable_checks_env_role_provisioning_status():
+ environment = EnvironmentFactory.create(
+ cloud_id="cloud-id", root_user_info={"credentials": "credentials"}
+ )
+ env_role1 = EnvironmentRoleFactory.create(environment=environment)
+ assert not env_role1.csp_user_id
+ env_role1 = EnvironmentRoles.disable(env_role1.id)
+ assert env_role1.disabled
+
+ env_role2 = EnvironmentRoleFactory.create(
+ environment=environment, csp_user_id="123456"
+ )
+ assert env_role2.csp_user_id
+ env_role2 = EnvironmentRoles.disable(env_role2.id)
+ assert env_role2.disabled
+
+
def test_get_for_update(application_role, environment):
EnvironmentRoleFactory.create(
application_role=application_role, environment=environment, deleted=True
@@ -100,3 +136,28 @@ def test_get_for_update(application_role, environment):
assert role.application_role == application_role
assert role.environment == environment
assert role.deleted
+
+
+def test_for_user(application_role):
+ portfolio = application_role.application.portfolio
+ user = application_role.user
+ # create roles for 2 environments associated with application_role fixture
+ env_role_1 = EnvironmentRoleFactory.create(application_role=application_role)
+ env_role_2 = EnvironmentRoleFactory.create(application_role=application_role)
+
+ # create role for environment in a different app in same portfolio
+ application = ApplicationFactory.create(portfolio=portfolio)
+ env_role_3 = EnvironmentRoleFactory.create(
+ application_role=ApplicationRoleFactory.create(
+ application=application, user=user
+ )
+ )
+
+ # create role for environment for random user in app2
+ rando_app_role = ApplicationRoleFactory.create(application=application)
+ rando_env_role = EnvironmentRoleFactory.create(application_role=rando_app_role)
+
+ env_roles = EnvironmentRoles.for_user(user.id, portfolio.id)
+ assert len(env_roles) == 3
+ assert env_roles == [env_role_1, env_role_2, env_role_3]
+ assert not rando_env_role in env_roles
diff --git a/tests/domain/test_portfolios.py b/tests/domain/test_portfolios.py
index 80dade92..41e3fc81 100644
--- a/tests/domain/test_portfolios.py
+++ b/tests/domain/test_portfolios.py
@@ -9,6 +9,7 @@ from atst.domain.portfolios import (
)
from atst.domain.portfolio_roles import PortfolioRoles
from atst.domain.applications import Applications
+from atst.domain.application_roles import ApplicationRoles
from atst.domain.environments import Environments
from atst.domain.permission_sets import PermissionSets, PORTFOLIO_PERMISSION_SETS
from atst.models.application_role import Status as ApplicationRoleStatus
diff --git a/tests/domain/test_reports.py b/tests/domain/test_reports.py
index e11b8dee..33ac926e 100644
--- a/tests/domain/test_reports.py
+++ b/tests/domain/test_reports.py
@@ -1,22 +1,8 @@
-from atst.domain.reports import Reports
-from tests.factories import *
-
-
-# this is sketched out until we do real reporting
-def test_monthly_totals():
- pass
-
-
-# this is sketched out until we do real reporting
-def test_current_obligated_funds():
- pass
-
-
-# this is sketched out until we do real reporting
+# TODO: Implement when we get real reporting data
def test_expired_task_orders():
pass
-# this is sketched out until we do real reporting
+# TODO: Implement when we get real reporting data
def test_obligated_funds_by_JEDI_clin():
pass
diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py
index 089741d5..49b9bae6 100644
--- a/tests/domain/test_task_orders.py
+++ b/tests/domain/test_task_orders.py
@@ -2,79 +2,13 @@ import pytest
from datetime import date, timedelta
from decimal import Decimal
+from atst.domain.exceptions import AlreadyExistsError
from atst.domain.task_orders import TaskOrders
-from atst.models import Attachment, TaskOrder
+from atst.models import Attachment
+from atst.models.task_order import TaskOrder, SORT_ORDERING, Status
from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory
-def test_task_order_sorting():
- """
- Task orders should be listed first by status, and then by time_created.
- """
-
- today = date.today()
- yesterday = today - timedelta(days=1)
- future = today + timedelta(days=100)
-
- task_orders = [
- # Draft
- TaskOrderFactory.create(pdf=None),
- TaskOrderFactory.create(pdf=None),
- TaskOrderFactory.create(pdf=None),
- # Active
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
- ),
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
- ),
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
- ),
- # Upcoming
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=future, end_date=future)],
- ),
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=future, end_date=future)],
- ),
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=future, end_date=future)],
- ),
- # Expired
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
- ),
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
- ),
- TaskOrderFactory.create(
- signed_at=yesterday,
- clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
- ),
- # Unsigned
- TaskOrderFactory.create(
- clins=[CLINFactory.create(start_date=today, end_date=today)]
- ),
- TaskOrderFactory.create(
- clins=[CLINFactory.create(start_date=today, end_date=today)]
- ),
- TaskOrderFactory.create(
- clins=[CLINFactory.create(start_date=today, end_date=today)]
- ),
- ]
-
- assert TaskOrders.sort(task_orders) == task_orders
-
-
def test_create_adds_clins():
portfolio = PortfolioFactory.create()
clins = [
@@ -96,7 +30,6 @@ def test_create_adds_clins():
},
]
task_order = TaskOrders.create(
- creator=portfolio.owner,
portfolio_id=portfolio.id,
number="0123456789",
clins=clins,
@@ -127,7 +60,6 @@ def test_update_adds_clins():
},
]
task_order = TaskOrders.create(
- creator=task_order.creator,
portfolio_id=task_order.portfolio_id,
number="0000000000",
clins=clins,
@@ -179,3 +111,62 @@ def test_delete_task_order_with_clins(session):
assert not session.query(
session.query(TaskOrder).filter_by(id=task_order.id).exists()
).scalar()
+
+
+def test_task_order_sort_by_status():
+ today = date.today()
+ yesterday = today - timedelta(days=1)
+ future = today + timedelta(days=100)
+
+ initial_to_list = [
+ # Draft
+ TaskOrderFactory.create(pdf=None),
+ TaskOrderFactory.create(pdf=None),
+ TaskOrderFactory.create(pdf=None),
+ # Active
+ TaskOrderFactory.create(
+ signed_at=yesterday,
+ clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
+ ),
+ # Upcoming
+ TaskOrderFactory.create(
+ signed_at=yesterday,
+ clins=[CLINFactory.create(start_date=future, end_date=future)],
+ ),
+ # Expired
+ TaskOrderFactory.create(
+ signed_at=yesterday,
+ clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
+ ),
+ TaskOrderFactory.create(
+ signed_at=yesterday,
+ clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
+ ),
+ # Unsigned
+ TaskOrderFactory.create(
+ clins=[CLINFactory.create(start_date=today, end_date=today)]
+ ),
+ ]
+
+ sorted_by_status = TaskOrders.sort_by_status(initial_to_list)
+ assert len(sorted_by_status["Draft"]) == 3
+ assert len(sorted_by_status["Active"]) == 1
+ assert len(sorted_by_status["Upcoming"]) == 1
+ assert len(sorted_by_status["Expired"]) == 2
+ assert len(sorted_by_status["Unsigned"]) == 1
+ assert list(sorted_by_status.keys()) == [status.value for status in SORT_ORDERING]
+
+
+def test_create_enforces_unique_number():
+ portfolio = PortfolioFactory.create()
+ number = "1234567890123"
+ assert TaskOrders.create(portfolio.id, number, [], None)
+ with pytest.raises(AlreadyExistsError):
+ TaskOrders.create(portfolio.id, number, [], None)
+
+
+def test_update_enforces_unique_number():
+ task_order = TaskOrderFactory.create()
+ dupe_task_order = TaskOrderFactory.create()
+ with pytest.raises(AlreadyExistsError):
+ TaskOrders.update(dupe_task_order.id, task_order.number, [], None)
diff --git a/tests/domain/test_users.py b/tests/domain/test_users.py
index b5a24058..20bd8266 100644
--- a/tests/domain/test_users.py
+++ b/tests/domain/test_users.py
@@ -4,36 +4,41 @@ from uuid import uuid4
from atst.domain.users import Users
from atst.domain.exceptions import NotFoundError, AlreadyExistsError, UnauthorizedError
+from atst.utils import pick
from tests.factories import UserFactory
DOD_ID = "my_dod_id"
+REQUIRED_KWARGS = {"first_name": "Luke", "last_name": "Skywalker"}
def test_create_user():
- user = Users.create(DOD_ID)
+ user = Users.create(DOD_ID, **REQUIRED_KWARGS)
assert user.dod_id == DOD_ID
def test_create_user_with_existing_email():
- Users.create(DOD_ID, email="thisusersemail@usersRus.com")
+ Users.create(DOD_ID, email="thisusersemail@usersRus.com", **REQUIRED_KWARGS)
with pytest.raises(AlreadyExistsError):
Users.create(DOD_ID, email="thisusersemail@usersRus.com")
def test_create_user_with_nonexistent_permission_set():
with pytest.raises(NotFoundError):
- Users.create(DOD_ID, permission_sets=["nonexistent"])
+ Users.create(DOD_ID, permission_sets=["nonexistent"], **REQUIRED_KWARGS)
def test_get_or_create_nonexistent_user():
- user = Users.get_or_create_by_dod_id(DOD_ID)
+ user = Users.get_or_create_by_dod_id(DOD_ID, **REQUIRED_KWARGS)
assert user.dod_id == DOD_ID
def test_get_or_create_existing_user():
fact_user = UserFactory.create()
- user = Users.get_or_create_by_dod_id(fact_user.dod_id)
+ user = Users.get_or_create_by_dod_id(
+ fact_user.dod_id,
+ **pick(["first_name", "last_name"], fact_user.to_dictionary()),
+ )
assert user == fact_user
diff --git a/tests/factories.py b/tests/factories.py
index efb6fb82..ee4159f6 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -105,13 +105,7 @@ class PortfolioFactory(Base):
name = factory.Faker("domain_word")
defense_component = factory.LazyFunction(random_service_branch)
-
- app_migration = random_choice(data.APP_MIGRATION)
- complexity = [random_choice(data.APPLICATION_COMPLEXITY)]
description = factory.Faker("sentence")
- dev_team = [random_choice(data.DEV_TEAM)]
- native_apps = random.choice(["yes", "no", "not_sure"])
- team_experience = random_choice(data.TEAM_EXPERIENCE)
@classmethod
def _create(cls, model_class, *args, **kwargs):
@@ -236,9 +230,17 @@ class ApplicationRoleFactory(Base):
@classmethod
def _create(cls, model_class, *args, **kwargs):
with_invite = kwargs.pop("invite", True)
- app_role = super()._create(model_class, *args, **kwargs)
+ app_role = model_class(*args, **kwargs)
- if with_invite:
+ if with_invite and app_role.user:
+ ApplicationInvitationFactory.create(
+ role=app_role,
+ dod_id=app_role.user.dod_id,
+ first_name=app_role.user.first_name,
+ last_name=app_role.user.last_name,
+ email=app_role.user.email,
+ )
+ elif with_invite:
ApplicationInvitationFactory.create(role=app_role)
return app_role
@@ -260,6 +262,14 @@ class PortfolioInvitationFactory(Base):
email = factory.Faker("email")
status = InvitationStatus.PENDING
expiration_time = PortfolioInvitations.current_expiration_time()
+ dod_id = factory.LazyFunction(random_dod_id)
+ first_name = factory.Faker("first_name")
+ last_name = factory.Faker("last_name")
+
+ @classmethod
+ def _create(cls, model_class, *args, **kwargs):
+ inviter_id = kwargs.pop("inviter_id", UserFactory.create().id)
+ return super()._create(model_class, inviter_id=inviter_id, *args, **kwargs)
class ApplicationInvitationFactory(Base):
@@ -270,6 +280,14 @@ class ApplicationInvitationFactory(Base):
status = InvitationStatus.PENDING
expiration_time = PortfolioInvitations.current_expiration_time()
role = factory.SubFactory(ApplicationRoleFactory, invite=False)
+ dod_id = factory.LazyFunction(random_dod_id)
+ first_name = factory.Faker("first_name")
+ last_name = factory.Faker("last_name")
+
+ @classmethod
+ def _create(cls, model_class, *args, **kwargs):
+ inviter_id = kwargs.pop("inviter_id", UserFactory.create().id)
+ return super()._create(model_class, inviter_id=inviter_id, *args, **kwargs)
class AttachmentFactory(Base):
@@ -284,11 +302,8 @@ class TaskOrderFactory(Base):
class Meta:
model = TaskOrder
- portfolio = factory.SubFactory(
- PortfolioFactory, owner=factory.SelfAttribute("..creator")
- )
+ portfolio = factory.SubFactory(PortfolioFactory)
number = factory.LazyFunction(random_task_order_number)
- creator = factory.SubFactory(UserFactory)
signed_at = None
_pdf = factory.SubFactory(AttachmentFactory)
diff --git a/tests/forms/test_task_order.py b/tests/forms/test_task_order.py
index de95e555..97759c81 100644
--- a/tests/forms/test_task_order.py
+++ b/tests/forms/test_task_order.py
@@ -2,7 +2,7 @@ import datetime
from dateutil.relativedelta import relativedelta
from flask import current_app as app
-from atst.forms.task_order import CLINForm
+from atst.forms.task_order import CLINForm, TaskOrderForm
from atst.models import JEDICLINType
from atst.utils.localization import translate
@@ -106,3 +106,9 @@ def test_clin_form_dollar_amounts_out_of_range():
assert (
translate("forms.task_order.clin_funding_errors.funding_range_error")
) in invalid_clin_form.obligated_amount.errors
+
+
+def test_no_number():
+ http_request_form_data = {}
+ form = TaskOrderForm(http_request_form_data)
+ assert form.data["number"] is None
diff --git a/tests/mock_azure.py b/tests/mock_azure.py
index 34d0aa53..a360df64 100644
--- a/tests/mock_azure.py
+++ b/tests/mock_azure.py
@@ -10,9 +10,9 @@ AZURE_CONFIG = {
}
AUTH_CREDENTIALS = {
- "CLIENT_ID": AZURE_CONFIG["AZURE_CLIENT_ID"],
- "SECRET_KEY": AZURE_CONFIG["AZURE_SECRET_KEY"],
- "TENANT_ID": AZURE_CONFIG["AZURE_TENANT_ID"],
+ "client_id": AZURE_CONFIG["AZURE_CLIENT_ID"],
+ "secret_key": AZURE_CONFIG["AZURE_SECRET_KEY"],
+ "tenant_id": AZURE_CONFIG["AZURE_TENANT_ID"],
}
@@ -28,6 +28,12 @@ def mock_authorization():
return Mock(spec=authorization)
+def mock_managementgroups():
+ from azure.mgmt import managementgroups
+
+ return Mock(spec=managementgroups)
+
+
def mock_graphrbac():
import azure.graphrbac as graphrbac
@@ -46,6 +52,7 @@ class MockAzureSDK(object):
self.subscription = mock_subscription()
self.authorization = mock_authorization()
+ self.managementgroups = mock_managementgroups()
self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials()
# may change to a JEDI cloud
diff --git a/tests/models/test_task_order.py b/tests/models/test_task_order.py
index cb1ca7de..91fee15b 100644
--- a/tests/models/test_task_order.py
+++ b/tests/models/test_task_order.py
@@ -76,7 +76,7 @@ class TestTaskOrderStatus:
@patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock)
def test_draft_status(self, is_signed, is_completed):
# Given that I have a TO that is neither completed nor signed
- to = TaskOrder()
+ to = TaskOrderFactory.create()
is_signed.return_value = False
is_completed.return_value = False
@@ -89,7 +89,7 @@ class TestTaskOrderStatus:
def test_active_status(self, is_signed, is_completed, start_date, end_date):
# Given that I have a signed TO and today is within its start_date and end_date
today = pendulum.today().date()
- to = TaskOrder()
+ to = TaskOrderFactory.create()
start_date.return_value = today.subtract(days=1)
end_date.return_value = today.add(days=1)
@@ -105,7 +105,7 @@ class TestTaskOrderStatus:
@patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock)
def test_upcoming_status(self, is_signed, is_completed, start_date, end_date):
# Given that I have a signed TO and today is before its start_date
- to = TaskOrder()
+ to = TaskOrderFactory.create()
start_date.return_value = pendulum.today().add(days=1).date()
end_date.return_value = pendulum.today().add(days=2).date()
is_signed.return_value = True
@@ -120,7 +120,7 @@ class TestTaskOrderStatus:
@patch("atst.models.TaskOrder.is_signed", new_callable=PropertyMock)
def test_expired_status(self, is_signed, is_completed, end_date, start_date):
# Given that I have a signed TO and today is after its expiration date
- to = TaskOrder()
+ to = TaskOrderFactory.create()
end_date.return_value = pendulum.today().subtract(days=1).date()
start_date.return_value = pendulum.today().subtract(days=2).date()
is_signed.return_value = True
@@ -143,7 +143,7 @@ class TestTaskOrderStatus:
class TestBudget:
def test_total_contract_amount(self):
- to = TaskOrder()
+ to = TaskOrderFactory.create()
assert to.total_contract_amount == 0
clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1)
@@ -156,7 +156,7 @@ class TestBudget:
)
def test_total_obligated_funds(self):
- to = TaskOrder()
+ to = TaskOrderFactory.create()
assert to.total_obligated_funds == 0
clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1)
diff --git a/tests/render_vue_component.py b/tests/render_vue_component.py
index c4a7c135..62106a67 100644
--- a/tests/render_vue_component.py
+++ b/tests/render_vue_component.py
@@ -1,10 +1,11 @@
import pytest
from bs4 import BeautifulSoup
+from flask import Markup
from wtforms import Form, FormField
from wtforms.fields import StringField
from wtforms.validators import InputRequired
-from wtforms.widgets import CheckboxInput
+from wtforms.widgets import ListWidget, CheckboxInput
from atst.forms.task_order import CLINForm
from atst.forms.task_order import TaskOrderForm
@@ -56,6 +57,12 @@ def checkbox_input_macro(env):
return getattr(checkbox_template.module, "CheckboxInput")
+@pytest.fixture
+def multi_checkbox_input_macro(env):
+ multi_checkbox_template = env.get_template("components/multi_checkbox_input.html")
+ return getattr(multi_checkbox_template.module, "MultiCheckboxInput")
+
+
@pytest.fixture
def initial_value_form(scope="function"):
return InitialValueForm()
@@ -82,6 +89,20 @@ def test_make_checkbox_input_template(checkbox_input_macro, initial_value_form):
write_template(rendered_checkbox_macro, "checkbox_input_template.html")
+def test_make_multi_checkbox_input_template(
+ multi_checkbox_input_macro, initial_value_form
+):
+ initial_value_form.datafield.widget = ListWidget()
+ initial_value_form.datafield.option_widget = CheckboxInput()
+ initial_value_form.datafield.choices = [("a", "A"), ("b", "B")]
+ rendered_multi_checkbox_input_macro = multi_checkbox_input_macro(
+ initial_value_form.datafield, optional=Markup("'optional'")
+ )
+ write_template(
+ rendered_multi_checkbox_input_macro, "multi_checkbox_input_template.html"
+ )
+
+
def test_make_upload_input_template(upload_input_macro, task_order_form):
rendered_upload_macro = upload_input_macro(task_order_form.pdf)
write_template(rendered_upload_macro, "upload_input_template.html")
diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py
index 4961065e..08c979ad 100644
--- a/tests/routes/applications/test_settings.py
+++ b/tests/routes/applications/test_settings.py
@@ -12,6 +12,7 @@ from atst.domain.application_roles import ApplicationRoles
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.invitations import ApplicationInvitations
from atst.domain.common import Paginator
+from atst.domain.csp.cloud import GeneralCSPException
from atst.domain.permission_sets import PermissionSets
from atst.models.application_role import Status as ApplicationRoleStatus
from atst.models.environment_role import CSPRole, EnvironmentRole
@@ -748,3 +749,41 @@ def test_handle_update_member(set_g):
assert len(application.roles) == 1
assert len(app_role.environment_roles) == 1
assert app_role.environment_roles[0].environment == env
+
+
+def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger):
+ exception = "An error occurred."
+
+ def _raise_csp_exception(*args, **kwargs):
+ raise GeneralCSPException(exception)
+
+ monkeypatch.setattr(
+ "atst.domain.environments.Environments.update_env_role", _raise_csp_exception
+ )
+
+ user = UserFactory.create()
+ application = ApplicationFactory.create(
+ environments=[{"name": "Naboo"}, {"name": "Endor"}]
+ )
+ (env, env_1) = application.environments
+ app_role = ApplicationRoleFactory(application=application)
+ set_g("current_user", application.portfolio.owner)
+ set_g("portfolio", application.portfolio)
+ set_g("application", application)
+
+ form_data = ImmutableMultiDict(
+ {
+ "environment_roles-0-environment_id": env.id,
+ "environment_roles-0-role": "Basic Access",
+ "environment_roles-0-environment_name": env.name,
+ "environment_roles-1-environment_id": env_1.id,
+ "environment_roles-1-role": NO_ACCESS,
+ "environment_roles-1-environment_name": env_1.name,
+ "perms_env_mgmt": True,
+ "perms_team_mgmt": True,
+ "perms_del_env": True,
+ }
+ )
+ handle_update_member(application.id, app_role.id, form_data)
+
+ assert mock_logger.messages[-1] == exception
diff --git a/tests/routes/portfolios/test_index.py b/tests/routes/portfolios/test_index.py
index ef368c3d..745430f0 100644
--- a/tests/routes/portfolios/test_index.py
+++ b/tests/routes/portfolios/test_index.py
@@ -18,7 +18,7 @@ def test_new_portfolio(client, user_session):
user = UserFactory.create()
user_session(user)
- response = client.get(url_for("portfolios.new_portfolio"))
+ response = client.get(url_for("portfolios.new_portfolio_step_1"))
assert response.status_code == 200
@@ -34,7 +34,7 @@ def test_create_portfolio_success(client, user_session):
data={
"name": "My project name",
"description": "My project description",
- "defense_component": "Air Force, Department of the",
+ "defense_component": "army",
},
)
@@ -65,32 +65,6 @@ def test_create_portfolio_failure(client, user_session):
assert len(PortfoliosQuery.get_all()) == original_portfolio_count
-def test_portfolio_index_with_existing_portfolios(client, user_session):
- portfolio = PortfolioFactory.create()
- user_session(portfolio.owner)
-
- response = client.get(url_for("portfolios.portfolios"))
-
- assert response.status_code == 200
- assert portfolio.name.encode("utf8") in response.data
- assert (
- translate("portfolios.index.empty.start_button").encode("utf8")
- not in response.data
- )
-
-
-def test_portfolio_index_without_existing_portfolios(client, user_session):
- user = UserFactory.create()
- user_session(user)
-
- response = client.get(url_for("portfolios.portfolios"))
-
- assert response.status_code == 200
- assert (
- translate("portfolios.index.empty.start_button").encode("utf8") in response.data
- )
-
-
def test_portfolio_reports(client, user_session):
portfolio = PortfolioFactory.create(
applications=[
diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py
index c800ce25..48d8bd6b 100644
--- a/tests/routes/task_orders/test_index.py
+++ b/tests/routes/task_orders/test_index.py
@@ -29,8 +29,10 @@ def task_order():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
attachment = Attachment(filename="sample_attachment", object_name="sample")
+ task_order = TaskOrderFactory.create(portfolio=portfolio)
+ CLINFactory.create(task_order=task_order)
- return TaskOrderFactory.create(creator=user, portfolio=portfolio)
+ return task_order
def test_review_task_order_not_draft(client, user_session, task_order):
diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py
index 61a97b82..a7f5a991 100644
--- a/tests/routes/task_orders/test_new.py
+++ b/tests/routes/task_orders/test_new.py
@@ -19,15 +19,24 @@ def build_pdf_form_data(filename="sample.pdf", object_name=None):
def task_order():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
+ task_order = TaskOrderFactory.create(portfolio=portfolio)
+ CLINFactory.create(task_order=task_order)
- return TaskOrderFactory.create(creator=user, portfolio=portfolio)
+ return task_order
+
+
+@pytest.fixture
+def incomplete_to():
+ user = UserFactory.create()
+ portfolio = PortfolioFactory.create(owner=user)
+
+ return TaskOrderFactory.create(portfolio=portfolio)
@pytest.fixture
def completed_task_order():
portfolio = PortfolioFactory.create()
task_order = TaskOrderFactory.create(
- creator=portfolio.owner,
portfolio=portfolio,
create_clins=[{"number": "1234567890123456789012345678901234567890123"}],
)
@@ -68,7 +77,7 @@ def test_task_orders_submit_form_step_one_add_pdf(client, user_session, portfoli
def test_task_orders_form_step_one_add_pdf_existing_to(
client, user_session, task_order
):
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
response = client.get(
url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order.id)
)
@@ -77,7 +86,7 @@ def test_task_orders_form_step_one_add_pdf_existing_to(
def test_task_orders_submit_form_step_one_add_pdf_existing_to(client, user_session):
task_order = TaskOrderFactory.create()
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
response = client.post(
url_for(
"task_orders.submit_form_step_one_add_pdf", task_order_id=task_order.id
@@ -140,7 +149,7 @@ def test_task_orders_submit_form_step_one_validates_object_name(
def test_task_orders_form_step_two_add_number(client, user_session, task_order):
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
response = client.get(
url_for("task_orders.form_step_two_add_number", task_order_id=task_order.id)
)
@@ -148,7 +157,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.creator)
+ user_session(task_order.portfolio.owner)
form_data = {"number": "1234567890"}
response = client.post(
url_for(
@@ -161,10 +170,30 @@ def test_task_orders_submit_form_step_two_add_number(client, user_session, task_
assert task_order.number == "1234567890"
+def test_task_orders_submit_form_step_two_enforces_unique_number(
+ client, user_session, task_order, session
+):
+ number = "1234567890123"
+ dupe_task_order = TaskOrderFactory.create(number=number)
+ portfolio = task_order.portfolio
+ user_session(task_order.portfolio.owner)
+ form_data = {"number": number}
+ session.begin_nested()
+ response = client.post(
+ url_for(
+ "task_orders.submit_form_step_two_add_number", task_order_id=task_order.id
+ ),
+ data=form_data,
+ )
+ session.rollback()
+
+ assert response.status_code == 400
+
+
def test_task_orders_submit_form_step_two_add_number_existing_to(
client, user_session, task_order
):
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
form_data = {"number": "0000000000"}
original_number = task_order.number
response = client.post(
@@ -179,7 +208,7 @@ def test_task_orders_submit_form_step_two_add_number_existing_to(
def test_task_orders_form_step_three_add_clins(client, user_session, task_order):
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
response = client.get(
url_for("task_orders.form_step_three_add_clins", task_order_id=task_order.id)
)
@@ -187,7 +216,7 @@ def test_task_orders_form_step_three_add_clins(client, user_session, task_order)
def test_task_orders_submit_form_step_three_add_clins(client, user_session, task_order):
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312",
@@ -235,9 +264,9 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
},
]
TaskOrders.create_clins(task_order.id, clin_list)
- assert len(task_order.clins) == 2
+ assert len(task_order.clins) == 3
- user_session(task_order.creator)
+ user_session(task_order.portfolio.owner)
form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-clin_number": "12312",
@@ -258,7 +287,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
def test_task_orders_form_step_four_review(client, user_session, completed_task_order):
- user_session(completed_task_order.creator)
+ user_session(completed_task_order.portfolio.owner)
response = client.get(
url_for(
"task_orders.form_step_four_review", task_order_id=completed_task_order.id
@@ -268,11 +297,11 @@ def test_task_orders_form_step_four_review(client, user_session, completed_task_
def test_task_orders_form_step_four_review_incomplete_to(
- client, user_session, task_order
+ client, user_session, incomplete_to
):
- user_session(task_order.creator)
+ user_session(incomplete_to.portfolio.owner)
response = client.get(
- url_for("task_orders.form_step_four_review", task_order_id=task_order.id)
+ url_for("task_orders.form_step_four_review", task_order_id=incomplete_to.id)
)
assert response.status_code == 404
@@ -280,7 +309,7 @@ def test_task_orders_form_step_four_review_incomplete_to(
def test_task_orders_form_step_five_confirm_signature(
client, user_session, completed_task_order
):
- user_session(completed_task_order.creator)
+ user_session(completed_task_order.portfolio.owner)
response = client.get(
url_for(
"task_orders.form_step_five_confirm_signature",
@@ -291,12 +320,13 @@ def test_task_orders_form_step_five_confirm_signature(
def test_task_orders_form_step_five_confirm_signature_incomplete_to(
- client, user_session, task_order
+ client, user_session, incomplete_to
):
- user_session(task_order.creator)
+ user_session(incomplete_to.portfolio.owner)
response = client.get(
url_for(
- "task_orders.form_step_five_confirm_signature", task_order_id=task_order.id
+ "task_orders.form_step_five_confirm_signature",
+ task_order_id=incomplete_to.id,
)
)
assert response.status_code == 404
@@ -340,9 +370,7 @@ def test_task_orders_submit_task_order(client, user_session, task_order):
def test_task_orders_edit_redirects_to_latest_incomplete_step(
client, user_session, portfolio, to_factory_args, expected_step
):
- task_order = TaskOrderFactory.create(
- portfolio=portfolio, creator=portfolio.owner, **to_factory_args
- )
+ task_order = TaskOrderFactory.create(portfolio=portfolio, **to_factory_args)
user_session(portfolio.owner)
response = client.get(url_for("task_orders.edit", task_order_id=task_order.id))
@@ -414,8 +442,7 @@ def test_task_orders_update_invalid_data(client, user_session, portfolio):
@pytest.mark.skip(reason="Update after implementing errors on TO form")
def test_task_order_form_shows_errors(client, user_session, task_order):
- creator = task_order.creator
- user_session(creator)
+ user_session(task_order.portfolio.owner)
task_order_data = TaskOrderFactory.dictionary()
funding_data = slice_data_for_section(task_order_data, "funding")
diff --git a/tests/test_access.py b/tests/test_access.py
index 8f3d201f..19aaf749 100644
--- a/tests/test_access.py
+++ b/tests/test_access.py
@@ -25,8 +25,7 @@ _NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [
"dev.test_email", # dev tool
"portfolios.accept_invitation", # available to all users; access control is built into invitation logic
"portfolios.create_portfolio", # create a portfolio
- "portfolios.new_portfolio", # all users can create a portfolio
- "portfolios.portfolios", # the portfolios list is scoped to the user separately
+ "portfolios.new_portfolio_step_1", # all users can create a portfolio
"task_orders.get_started", # all users can start a new TO
"users.update_user", # available to all users
"users.user", # available to all users
@@ -487,7 +486,9 @@ def test_portfolios_resend_invitation_access(post_url_assert_status):
portfolio = PortfolioFactory.create(owner=owner)
prr = PortfolioRoleFactory.create(user=invitee, portfolio=portfolio)
- invite = PortfolioInvitationFactory.create(user=UserFactory.create(), role=prr)
+ invite = PortfolioInvitationFactory.create(
+ user=UserFactory.create(), role=prr, inviter_id=owner.id
+ )
url = url_for(
"portfolios.resend_invitation",
@@ -651,7 +652,6 @@ def test_task_orders_new_get_routes(get_url_assert_status):
portfolio = PortfolioFactory.create(owner=owner)
task_order = TaskOrderFactory.create(
- creator=owner,
portfolio=portfolio,
create_clins=[{"number": "1234567890123456789012345678901234567890123"}],
)
@@ -689,7 +689,7 @@ def test_task_orders_new_post_routes(post_url_assert_status):
rando = user_with()
portfolio = PortfolioFactory.create(owner=owner)
- task_order = TaskOrderFactory.create(portfolio=portfolio, creator=owner)
+ task_order = TaskOrderFactory.create(portfolio=portfolio)
for route, data in post_routes:
url = url_for(route, task_order_id=task_order.id)
diff --git a/tests/test_app.py b/tests/test_app.py
index 222f4a4f..937a15e2 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1,8 +1,13 @@
import os
+from configparser import ConfigParser
import pytest
-from atst.app import make_crl_validator
+from atst.app import (
+ make_crl_validator,
+ apply_config_from_directory,
+ apply_config_from_environment,
+)
@pytest.fixture
@@ -22,3 +27,43 @@ def test_make_crl_validator_creates_crl_dir(app, tmpdir, replace_crl_dir_config)
replace_crl_dir_config(crl_dir)
make_crl_validator(app)
assert os.path.isdir(crl_dir)
+
+
+@pytest.fixture
+def config_object():
+ config = ConfigParser()
+ config.optionxform = str
+ config.read_string("[default]\nFOO=BALONEY")
+ return config
+
+
+def test_apply_config_from_directory(tmpdir, config_object):
+ config_setting = tmpdir.join("FOO")
+ with open(config_setting, "w") as conf_file:
+ conf_file.write("MAYO")
+
+ apply_config_from_directory(tmpdir, config_object)
+ assert config_object.get("default", "FOO") == "MAYO"
+
+
+def test_apply_config_from_directory_skips_unknown_settings(tmpdir, config_object):
+ config_setting = tmpdir.join("FLARF")
+ with open(config_setting, "w") as conf_file:
+ conf_file.write("MAYO")
+
+ apply_config_from_directory(tmpdir, config_object)
+ assert "FLARF" not in config_object.options("default")
+
+
+def test_apply_config_from_environment(monkeypatch, config_object):
+ monkeypatch.setenv("FOO", "MAYO")
+ apply_config_from_environment(config_object)
+ assert config_object.get("default", "FOO") == "MAYO"
+
+
+def test_apply_config_from_environment_skips_unknown_settings(
+ monkeypatch, config_object
+):
+ monkeypatch.setenv("FLARF", "MAYO")
+ apply_config_from_environment(config_object)
+ assert "FLARF" not in config_object.options("default")
diff --git a/tests/test_auth.py b/tests/test_auth.py
index d6160491..23dc46d2 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -105,13 +105,6 @@ def test_protected_routes_redirect_to_login(client, app):
assert server_name in resp.headers["Location"]
-def test_get_protected_route_encodes_redirect(client):
- portfolio_index = url_for("portfolios.portfolios")
- response = client.get(portfolio_index)
- redirect = url_for("atst.root", next=portfolio_index)
- assert redirect in response.headers["Location"]
-
-
def test_unprotected_routes_set_user_if_logged_in(client, app, user_session):
user = UserFactory.create()
diff --git a/tests/utils.py b/tests/utils.py
index 152c347a..66bf2b18 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -40,6 +40,9 @@ class FakeLogger:
def error(self, msg, *args, **kwargs):
self._log("error", msg, *args, **kwargs)
+ def exception(self, msg, *args, **kwargs):
+ self._log("exception", msg, *args, **kwargs)
+
def _log(self, _lvl, msg, *args, **kwargs):
self.messages.append(msg)
if "extra" in kwargs:
diff --git a/translations.yaml b/translations.yaml
index 387aecec..1d876e97 100644
--- a/translations.yaml
+++ b/translations.yaml
@@ -44,6 +44,7 @@ ccpo:
alert_message: "Confirm removing CCPO superuser access from {user_name}"
remove_button: Remove Access
common:
+ applications: Applications
cancel: Cancel
close: Close
confirm: Confirm
@@ -65,6 +66,7 @@ common:
response_label: Response required
save: Save
save_changes: Save Changes
+ task_orders: Task Orders
undo: Undo
view: View
resource_names:
@@ -121,6 +123,8 @@ flash:
new_ppoc_message: 'You have successfully added {ppoc_name} as the primary point of contact. You are no longer the PPoC.'
new_ppoc_title: Primary point of contact updated
success: Success!
+ task_order_number_error:
+ message: 'The TO number has already been entered for a JEDI task order #{to_number}. Please double-check the TO number you are entering. If you believe this is in error, please contact support@cloud.mil.'
new_application_member:
title: "{user_name}'s invitation has been sent"
message: "{user_name}'s access to this Application is pending until they sign in for the first time."
@@ -164,8 +168,50 @@ forms:
portfolio_mgmt: Portfolio management
reporting: Reporting
portfolio:
- name_label: Portfolio name
- name_length_validation_message: Portfolio names can be between 4-100 characters
+ name:
+ label: Portfolio Name
+ length_validation_message: Portfolio names can be between 4-100 characters
+ help_text: |
+
+
+ Naming can be difficult. Choose a name that is descriptive enough for users to identify the Portfolio. You may consider naming based on your organization.
+
+ Add a brief one to two sentence description of your Portfolio. Consider this your statement of work.
+
+
+ Writer's Block? A description example includes:
+
+
Build security applications for FOB Clark
+
+
+
+ defense_component:
+ label: "Select DoD component(s) funding your Portfolio:"
+ choices:
+ air_force: Air Force
+ army: Army
+ marine_corps: Marine Corps
+ navy: Navy
+ other: Other
+ validation_message: You must select at least one defense component.
+ help_text: |
+
+ Select the DOD component(s) that will fund all Applications within this Portfolio.
+ In JEDI, multiple DoD organizations can fund the same Portfolio.
+ Select all that apply.
+
attachment:
object_name:
length_error: Object name may be no longer than 40 characters.
@@ -174,41 +220,8 @@ forms:
task_order:
upload_error: There was an error uploading your file. Please try again. If you encounter repeated problems uploading this file, please contact CCPO.
size_error: The file you have selected is too large. Please choose a file no larger than 64MB.
- app_migration:
- both: 'Yes, migrating from both an on-premise data center and another cloud provider'
- cloud: 'Yes, migrating from another cloud provider'
- description: Do you plan to migrate one or more existing application(s) to the cloud?
- label: App migration
- none: Not planning to migrate any applications
- not_sure: Not sure
- on_premise: 'Yes, migrating from an on-premise data center'
- complexity:
- conus: CONUS access
- data_analytics: Data analytics
- description: Which of these describes how complex your team's use of the cloud will be? Select all that apply.
- label: Project complexity
- not_sure: Not sure
- oconus: OCONUS access
- other: Other
- storage: Storage
- tactical_edge: Tactical edge access
- complexity_other_label: Project Complexity Other
- defense_component_label: Department of Defense component
- dev_team:
- civilians: Government civilians
- contractor: Contractor
- description: Who is building your cloud application(s)? Select all that apply.
- label: Development team
- military: Military
- other: Other (E.g. University or other partner)
- dev_team_other_label: Development Team Other
+ defense_component_label: Select DoD component(s) funding your Portfolio
file_format_not_allowed: Only PDF or PNG files can be uploaded.
- native_apps:
- description: Do you plan to develop any applications in the cloud?
- label: Native apps
- 'no': 'No, not planning to develop natively in the cloud'
- not_sure: 'Not sure, unsure if planning to develop natively in the cloud'
- not_sure_help: Not sure? Talk to your technical lead about where and how they plan on developing your application.
number_description: Task order number (13 digits)
pop_errors:
date_order: PoP start date must be before end date.
@@ -217,8 +230,6 @@ forms:
end_pre_contract: PoP end date must be after or on {date}.
start_past_contract: PoP start date must be before or on {date}.
start_pre_contract: PoP start date must be on or after {date}.
- scope_description: 'What do you plan to do on the cloud? Some examples might include migrating an existing application or creating a prototype. You don’t need to include a detailed plan of execution, but should list key requirements. This section will be reviewed by your contracting officer, but won’t be sent to the CCPO.
Not sure how to describe your scope? Read some examples to get some inspiration.
'
- scope_label: Cloud project scope
clin_funding_errors:
obligated_amount_error: Obligated amount must be less than or equal to total amount
funding_range_error: Dollar amount must be from $0.00 to $1,000,000,000.00
@@ -308,6 +319,12 @@ portfolios:
add_member: Add Team Member
add_another_environment: Add another environment
app_settings_text: App settings
+ 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"
@@ -455,6 +472,7 @@ portfolios:
action_label: 'Add a new application'
task_orders:
+ add_new_button: Add New Task Order
review:
pdf_title: Approved Task Order
review_your_funding: Review your funding
@@ -507,6 +525,11 @@ task_orders:
alert_message: All task orders require a Contracting Officer signature.
next_button: 'Confirm & Submit'
sticky_header_text: 'Add Task Order (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.
new:
form_help_text: Before you can begin work in the cloud, you'll need to complete the information below and upload your approved task order for reference by the CCPO.
app_info:
@@ -515,6 +538,8 @@ task_orders:
team_title: Your team
sign:
digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature.
+ status_empty_state: 'This Portfolio has no {status} Task Orders.'
+ status_list_title: '{status} Task Orders'
JEDICLINType:
JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS'
JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS'
diff --git a/uitests/Add_CCPO_User.html b/uitests/Add_CCPO_User.html
index bab35aa4..df3ad34d 100644
--- a/uitests/Add_CCPO_User.html
+++ b/uitests/Add_CCPO_User.html
@@ -74,15 +74,14 @@
@@ -103,7 +102,8 @@ Imported from: AT-AT CI - Login Brandon
Imported from: AT-AT CI - New App Step 3
Imported from: AT-AT CI - New App Step 2
Imported from: AT-AT CI - New App Step 1
-Imported from: AT-AT CI - New Portfolio-->
+Imported from: AT-AT CI - New Portfolio
+Imported from: AT-AT CI - login-->
open
/login-dev
@@ -118,6 +118,27 @@ Imported from: AT-AT CI - New Portfolio-->
Imported from: AT-AT CI - New App Step 3
Imported from: AT-AT CI - New App Step 2
Imported from: AT-AT CI - New App Step 1
+Imported from: AT-AT CI - New Portfolio
+Imported from: AT-AT CI - login-->
+
+
waitForElementPresent
+
css=.about-cloud > h1
+
+
+
+
assertText
+
css=.about-cloud > h1
+
About Cloud Services
+
+
+
waitForPageToLoad
+
+
+
+
waitForElementPresent
@@ -201,26 +222,6 @@ Imported from: AT-AT CI - New App Step 1
Imported from: AT-AT CI - New Portfolio-->
waitForElementPresent
-
css=#defense_component > option:nth-of-type(14)
-
-
-
-
click
-
css=#defense_component > option:nth-of-type(14)
-
-
-
-
waitForPageToLoad
-
-
-
-
-
-
waitForElementPresent
css=#description
@@ -501,13 +502,13 @@ Imported from: AT-AT CI - New App Step 1
Imported from: AT-AT CI - New Portfolio-->
waitForElementPresent
-
css=.empty-state__message
+
css=.empty-state h3
assertText
-
css=.empty-state__message
-
*This portfolio doesn’t have any applications*
+
css=.empty-state h3
+
*You don't have any Applications yet*
waitForPageToLoad
@@ -520,12 +521,12 @@ Imported from: AT-AT CI - New App Step 2
Imported from: AT-AT CI - New App Step 1-->
waitForElementPresent
-
css=a.usa-button.usa-button-big
+
css=a.usa-button.usa-button-primary
click
-
css=a.usa-button.usa-button-big
+
css=a.usa-button.usa-button-primary
@@ -991,23 +992,6 @@ Imported from: AT-AT CI - New App Step 3-->
Imported from: AT-AT CI - New App Step 3-->