From 4e0af139ccc10a3f21c3535acf3d1663d11139bf Mon Sep 17 00:00:00 2001 From: graham-dds Date: Wed, 27 Nov 2019 15:25:17 -0500 Subject: [PATCH 001/111] Style and refactor components for expired funding - modify Accordion component to be able to customize classes and tags that it uses to build markup - modify the shape of the object that the mock CSP returns for data that builds this section. - Also ensures that the CLINs TOs and CLINS are sorted - add appropriate css for styling --- atst/domain/csp/reports.py | 41 +++++++---- styles/sections/_reports.scss | 10 +++ templates/components/accordion.html | 51 ++++++++------ .../reports/expired_task_orders.html | 69 ++++++++++++------- 4 files changed, 111 insertions(+), 60 deletions(-) diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index 2d42fede..4b69e904 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -308,20 +308,37 @@ class MockReportingProvider(ReportingInterface): return {} def get_expired_task_orders(self, portfolio): - return [ - { - "id": task_order.id, - "number": task_order.number, + def sorted_task_orders(to_list): + return sorted(to_list, key=lambda to: to["number"]) + + def sorted_clins(clin_list): + return sorted(clin_list, key=lambda clin: clin["number"]) + + def serialize_clin(clin): + return { + "number": clin.number, + "jedi_clin_type": clin.jedi_clin_type, "period_of_performance": { - "start_date": task_order.start_date, - "end_date": task_order.end_date, + "start_date": clin.start_date, + "end_date": clin.end_date, }, - "total_obligated_funds": task_order.total_obligated_funds, + "total_value": clin.total_amount, + "total_obligated_funds": clin.obligated_amount, "expended_funds": ( - task_order.total_obligated_funds - * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) + clin.obligated_amount * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) ), } - for task_order in portfolio.task_orders - if task_order.is_expired - ] + + return sorted_task_orders( + [ + { + "id": task_order.id, + "number": task_order.number, + "clins": sorted_clins( + [serialize_clin(clin) for clin in task_order.clins] + ), + } + for task_order in portfolio.task_orders + if task_order.is_expired + ] + ) diff --git a/styles/sections/_reports.scss b/styles/sections/_reports.scss index f80a7750..05295185 100644 --- a/styles/sections/_reports.scss +++ b/styles/sections/_reports.scss @@ -87,4 +87,14 @@ font-size: $lead-font-size; } } + + .reporting-expended-funding { + &__header { + margin: 0; + } + &__content { + padding: 0; + border-top: 1px solid $color-gray-lighter; + } + } } diff --git a/templates/components/accordion.html b/templates/components/accordion.html index 8e508321..3fd38e01 100644 --- a/templates/components/accordion.html +++ b/templates/components/accordion.html @@ -1,23 +1,30 @@ -{% macro Accordion(title, id, heading_level="h2") %} - -
- <{{heading_level}}> - - -
- {{ caller() }} -
-
-
+{% macro Accordion( + title, + id, + wrapper_tag="div", + wrapper_classes="", + heading_tag="h2", + heading_classes="", + content_tag="div", + content_classes="") %} + + <{{wrapper_tag}} class="{{ wrapper_classes }}"> + <{{heading_tag}} class="{{ heading_classes }}"> + + + <{{content_tag}} + id="{{ id }}" + class="usa-accordion-content {{ content_classes }}" + v-bind:aria-hidden="isVisible ? 'false' : 'true'"> + {{ caller() }} + + + {% endmacro %} \ No newline at end of file diff --git a/templates/portfolios/reports/expired_task_orders.html b/templates/portfolios/reports/expired_task_orders.html index d7dd843a..4d8da3f6 100644 --- a/templates/portfolios/reports/expired_task_orders.html +++ b/templates/portfolios/reports/expired_task_orders.html @@ -1,34 +1,51 @@ {% from "components/accordion.html" import Accordion %} +{% from "components/icon.html" import Icon %}
- {% call Accordion("Expired Task Orders", "expired_task_orders", "h3") %} - {% for task_order in expired_task_orders %} - - Task Order {{ task_order["number"] }} - -
-

Period of Performance

-

- {{ task_order["period_of_performance"].start_date | formattedDate(formatter="%B %d, %Y") }} - - - {{ task_order["period_of_performance"].end_date | formattedDate(formatter="%B %d, %Y") }} -

-
-
-

Total Obligated

-

{{ task_order["total_obligated_funds"] | dollars }}

-
-
-

Total Expended

-

{{ task_order["expended_funds"] | dollars }}

-
-
-

Total Unused

-

{{ (task_order["total_obligated_funds"] - task_order["expended_funds"]) | dollars }}

-
- {% endfor %} + {% call Accordion( + "Expired funding", + "expired_funding", + heading_classes="reporting-expended-funding__header", + content_tag="table", + content_classes="atat-table reporting-expended-funding__content") %} + + + TO CLIN + PoP + CLIN Value + Amount Obligated + Amount Unspent + + + + {% for task_order in expired_task_orders %} + + + Task Order + {{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--blue" ) }} + + + + {% for clin in task_order.clins %} + + +
{{ clin.number }}
+
{{ ("{}".format(clin.jedi_clin_type) | translate)[15:] }}
+ + + {{ clin.period_of_performance.start_date | formattedDate(formatter="%b %d, %Y") }} + - + {{ clin.period_of_performance.end_date | formattedDate(formatter="%b %d, %Y") }} + + {{ clin.total_value | dollars }} + {{ clin.total_obligated_funds | dollars }} + {{ (clin.total_obligated_funds - clin.expended_funds) | dollars }} + + {% endfor %} + {% endfor %} + {% endcall %}
From 00b79e2c7b16ba7b0a5d65eb6e18a724426f3358 Mon Sep 17 00:00:00 2001 From: graham-dds Date: Wed, 27 Nov 2019 15:28:26 -0500 Subject: [PATCH 002/111] Modify styling for app / env spend table --- styles/sections/_reports.scss | 18 ++++++++++++++++++ .../reports/application_and_env_spending.html | 8 ++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/styles/sections/_reports.scss b/styles/sections/_reports.scss index 05295185..0cd737c8 100644 --- a/styles/sections/_reports.scss +++ b/styles/sections/_reports.scss @@ -97,4 +97,22 @@ border-top: 1px solid $color-gray-lighter; } } + + .reporting-spend-table { + &__env-row { + &-label { + margin-left: $gap * 5; + } + &--last { + & > td { + border-bottom: 1px solid black; + } + &:last-of-type { + & > td { + border-bottom: none; + } + } + } + } + } } diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index d969a767..e43ebf0c 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -59,9 +59,13 @@ - + - + From da1633515ba26bf46c886cefcf3253f10d0273ff Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 15:55:24 -0500 Subject: [PATCH 003/111] New test checks that optional fields really are optional --- .../New_Portfolio_-_No_optional_fields.html | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 uitests/New_Portfolio_-_No_optional_fields.html diff --git a/uitests/New_Portfolio_-_No_optional_fields.html b/uitests/New_Portfolio_-_No_optional_fields.html new file mode 100644 index 00000000..eae79a14 --- /dev/null +++ b/uitests/New_Portfolio_-_No_optional_fields.html @@ -0,0 +1,179 @@ + + + + + + +New Portfolio - No optional fields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
New Portfolio - No optional fields
waitForPageToLoad
open/login-dev
waitForPageToLoad
waitForElementPresentcss=.about-cloud > h1
assertTextcss=.about-cloud > h1About Cloud Services
waitForPageToLoad
waitForElementPresentcss=a[href="/portfolios/new"]
clickcss=a[href="/portfolios/new"]
waitForPageToLoad
waitForElementPresentcss=main.usa-section > h1
assertTextcss=main.usa-section > h1*New Portfolio Form*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameTatooine Energy Maintenance Systems
waitForPageToLoad
waitForElementPresentcss=#defense_component
typecss=#defense_componentDefense Logistics Agency
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
assertTextcss=input[type="submit"]Save
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.empty-state__message
assertTextcss=.empty-state__message*This portfolio doesn’t have any applications*
+ + \ No newline at end of file From c3878b4cce406e959c71d43c220977b45dc33753 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 15:56:01 -0500 Subject: [PATCH 004/111] Test modifications for week of 11/25/2019 --- uitests/Add_CCPO_User.html | 5 ++--- uitests/Application_Settings.html | 5 ++--- uitests/Edit_App_Member.html | 5 ++--- uitests/Edit_Portfolio_Member.html | 5 ++--- uitests/Login_Brandon.html | 3 +-- uitests/New_App_Step_3.html | 5 ++--- uitests/New_Portfolio_Member.html | 5 ++--- uitests/Remove_Portfolio_Member.html | 5 ++--- uitests/Resend_App_User_Invite.html | 5 ++--- uitests/Revoke_App_Member_Invite.html | 5 ++--- 10 files changed, 19 insertions(+), 29 deletions(-) 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 @@ - + waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Application_Settings.html b/uitests/Application_Settings.html index 963ed1e3..08ab153d 100644 --- a/uitests/Application_Settings.html +++ b/uitests/Application_Settings.html @@ -78,15 +78,14 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - Login Brandon--> waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Edit_App_Member.html b/uitests/Edit_App_Member.html index ee2af0b5..7e5070a1 100644 --- a/uitests/Edit_App_Member.html +++ b/uitests/Edit_App_Member.html @@ -82,15 +82,14 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - Login Brandon--> waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Edit_Portfolio_Member.html b/uitests/Edit_Portfolio_Member.html index d50e14fe..b2f179e4 100644 --- a/uitests/Edit_Portfolio_Member.html +++ b/uitests/Edit_Portfolio_Member.html @@ -78,15 +78,14 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - Login Brandon--> waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Login_Brandon.html b/uitests/Login_Brandon.html index 75543571..b5a2a58a 100644 --- a/uitests/Login_Brandon.html +++ b/uitests/Login_Brandon.html @@ -71,14 +71,13 @@ - waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/New_App_Step_3.html b/uitests/New_App_Step_3.html index cd8a47f5..0aab34c0 100644 --- a/uitests/New_App_Step_3.html +++ b/uitests/New_App_Step_3.html @@ -74,15 +74,14 @@ - + waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/New_Portfolio_Member.html b/uitests/New_Portfolio_Member.html index e2dbbe82..090ed9cc 100644 --- a/uitests/New_Portfolio_Member.html +++ b/uitests/New_Portfolio_Member.html @@ -74,15 +74,14 @@ - + waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Remove_Portfolio_Member.html b/uitests/Remove_Portfolio_Member.html index 99801981..47d5b49d 100644 --- a/uitests/Remove_Portfolio_Member.html +++ b/uitests/Remove_Portfolio_Member.html @@ -78,15 +78,14 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - Login Brandon--> waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Resend_App_User_Invite.html b/uitests/Resend_App_User_Invite.html index 6f93e334..f1b01f4e 100644 --- a/uitests/Resend_App_User_Invite.html +++ b/uitests/Resend_App_User_Invite.html @@ -82,15 +82,14 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - Login Brandon--> waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* diff --git a/uitests/Revoke_App_Member_Invite.html b/uitests/Revoke_App_Member_Invite.html index 48ca1e42..1fd8dec6 100644 --- a/uitests/Revoke_App_Member_Invite.html +++ b/uitests/Revoke_App_Member_Invite.html @@ -82,15 +82,14 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - Login Brandon--> waitForElementPresent css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading -verifyText +assertText css=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading *Logged out* From 36c76a62b8e3515d3ede8fb4c2cb67a4b19837e5 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 16:08:42 -0500 Subject: [PATCH 005/111] Remove Ghost Inspector details from base README --- README.md | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d18177d4..44749862 100644 --- a/README.md +++ b/README.md @@ -270,33 +270,8 @@ execute UI tests than vanilla Selenium. Ghost Inspector tests and steps can be exported to files that the Selenium IDE can import. We export these tests/steps regularly and archive them with the AT-AT codebase in the `uitests` directory. -To run the Ghost Inspector tests against a local instance of AT-AT, -you will need the following: - -- [docker](https://docs.docker.com/v17.12/install/) -- [circleci CLI tool](https://circleci.com/docs/2.0/local-cli/#installation) -- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/) - -The version of our CircleCI config (2.1) is incompatible with the -`circleci` tool. First run: - -``` -circleci config process .circleci/config.yml > local-ci.yml -``` - -Then run the job: - -``` -circleci local execute -e GI_SUITE= -e GI_API_KEY= -e NGROK_TOKEN= --job integration-tests -c local-ci.yml -``` - -If the job fails and you want to re-run it, you may receive errors -about running docker containers or the network already existing. -Some version of the following should reset your local docker state: - -``` -docker container stop redis postgres test-atat; docker container rm redis postgres test-atat ; docker network rm atat -``` +For further information about Ghost Inspector and its use in AT-AT, check out [its README](./uitests/README.md) +in the `uitests` directory. ## Notes From a016cfae289731a74e486c53d942989701abcfdc Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 16:09:24 -0500 Subject: [PATCH 006/111] Start Ghost Inspector README with details extracted from main README --- uitests/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 uitests/README.md diff --git a/uitests/README.md b/uitests/README.md new file mode 100644 index 00000000..18d92351 --- /dev/null +++ b/uitests/README.md @@ -0,0 +1,29 @@ +# Ghost Inspector Readme + +To run the Ghost Inspector tests against a local instance of AT-AT, +you will need the following: + +- [docker](https://docs.docker.com/v17.12/install/) +- [circleci CLI tool](https://circleci.com/docs/2.0/local-cli/#installation) +- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/) + +The version of our CircleCI config (2.1) is incompatible with the +`circleci` tool. First run: + +``` +circleci config process .circleci/config.yml > local-ci.yml +``` + +Then run the job: + +``` +circleci local execute -e GI_SUITE= -e GI_API_KEY= -e NGROK_TOKEN= --job integration-tests -c local-ci.yml +``` + +If the job fails and you want to re-run it, you may receive errors +about running docker containers or the network already existing. +Some version of the following should reset your local docker state: + +``` +docker container stop redis postgres test-atat; docker container rm redis postgres test-atat ; docker network rm atat +``` From 75aa82f9386df4273546912b0f107e817f6c833b Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 16:30:02 -0500 Subject: [PATCH 007/111] Added introductory information --- uitests/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/uitests/README.md b/uitests/README.md index 18d92351..986cc1d6 100644 --- a/uitests/README.md +++ b/uitests/README.md @@ -1,5 +1,12 @@ # Ghost Inspector Readme +The suite of Ghost Inspector tests that runs as part of the CI/CD workflow +can be found [here](https://app.ghostinspector.com/suites/5d9e3d303ab5d56633c11598). Its current status is https://api.ghostinspector.com/v1/suites/5d9e3d303ab5d56633c11598/status-badge + +[Another suite](https://app.ghostinspector.com/suites/5d9603f1af31210914da04ca) of Ghost Inspector tests runs once daily against the Staging site. Status: https://api.ghostinspector.com/v1/suites/5d9603f1af31210914da04ca/status-badge + +## Running Ghost Inspector tests locally + To run the Ghost Inspector tests against a local instance of AT-AT, you will need the following: From 83d90007609d98ab345a3b2a310c05a8c1d7a0d2 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 16:40:38 -0500 Subject: [PATCH 008/111] Add Testing Philosophy section --- uitests/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/uitests/README.md b/uitests/README.md index 986cc1d6..1ab5777a 100644 --- a/uitests/README.md +++ b/uitests/README.md @@ -5,6 +5,18 @@ can be found [here](https://app.ghostinspector.com/suites/5d9e3d303ab5d56633c115 [Another suite](https://app.ghostinspector.com/suites/5d9603f1af31210914da04ca) of Ghost Inspector tests runs once daily against the Staging site. Status: https://api.ghostinspector.com/v1/suites/5d9603f1af31210914da04ca/status-badge +## Testing philosophy + +The tests have been created to traverse the most common user flows in AT-AT. There are a few tests (e.g. "New Portfolio - no optional fields") +that check for regressions. Others (e.g. "Remove Portfolio Member") check less-common, "negative path" flows. Tests are added as necessary +to ensure fairly thorough checking of AT-AT. + +The tests are constructed in a "stepwise" fashion; that is, no individual test depends upon another, and each test checks one step in complex +user flows. As an example, there are six tests for creating a new Task Order, one corresponding to each screen in the process plus one +for adding a second CLIN to a TO. These tests rely on Ghost Inspector's "Import steps from Test X" functionality to perform all the +necessary setup for the current test. This also ensures that tests can be run in any sequence because Ghost Inspector launches up to four +tests simultaneously. + ## Running Ghost Inspector tests locally To run the Ghost Inspector tests against a local instance of AT-AT, From e7cdcd349712a7ca757616a153a56edcae4b2b30 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 16:58:21 -0500 Subject: [PATCH 009/111] Add section on dealing with changes to the UI and failing tests --- uitests/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/uitests/README.md b/uitests/README.md index 1ab5777a..5a119302 100644 --- a/uitests/README.md +++ b/uitests/README.md @@ -1,6 +1,6 @@ # Ghost Inspector Readme -The suite of Ghost Inspector tests that runs as part of the CI/CD workflow +The suite of Ghost Inspector tests that runs as part of the CI workflow can be found [here](https://app.ghostinspector.com/suites/5d9e3d303ab5d56633c11598). Its current status is https://api.ghostinspector.com/v1/suites/5d9e3d303ab5d56633c11598/status-badge [Another suite](https://app.ghostinspector.com/suites/5d9603f1af31210914da04ca) of Ghost Inspector tests runs once daily against the Staging site. Status: https://api.ghostinspector.com/v1/suites/5d9603f1af31210914da04ca/status-badge @@ -17,6 +17,21 @@ for adding a second CLIN to a TO. These tests rely on Ghost Inspector's "Import necessary setup for the current test. This also ensures that tests can be run in any sequence because Ghost Inspector launches up to four tests simultaneously. +## Handling UI changes + +As with any UI-testing system, Ghost Inspector tests will fail because of changes to the UI. This can be problematic since failures +cause errors in the CI workflow, which could cause the Pull Request not to be deployed. To mitigate this issue, we have utilized +the following strategies: + +1. Front-end developers notify the project QA lead of changes that will potentially cause Ghost Inspector failures --OR-- the +QA lead monitors all failures on the CI suite and determines that such a change has occurred. + +2. If only one step in a test is affected (e.g. a visual but not functional change), that step can be marked as "optional" in +the Ghost Inspector UI. + +3. If the (potentially) failing test will need to be reworked to account for functional changes, the test is moved from the CI +suite to the "Holding" suite until the PR is merged. Then the test can be edited and returned to the CI suite. + ## Running Ghost Inspector tests locally To run the Ghost Inspector tests against a local instance of AT-AT, From c23d01bc3dc1cce4fb44e827d238f5cfd70841be Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Wed, 27 Nov 2019 17:06:49 -0500 Subject: [PATCH 010/111] Minor edits --- uitests/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uitests/README.md b/uitests/README.md index 5a119302..0f3a940b 100644 --- a/uitests/README.md +++ b/uitests/README.md @@ -13,9 +13,9 @@ to ensure fairly thorough checking of AT-AT. The tests are constructed in a "stepwise" fashion; that is, no individual test depends upon another, and each test checks one step in complex user flows. As an example, there are six tests for creating a new Task Order, one corresponding to each screen in the process plus one -for adding a second CLIN to a TO. These tests rely on Ghost Inspector's "Import steps from Test X" functionality to perform all the -necessary setup for the current test. This also ensures that tests can be run in any sequence because Ghost Inspector launches up to four -tests simultaneously. +for adding a second CLIN to a TO. Each test relies on Ghost Inspector's "Import steps from Test X" functionality to perform all the +necessary setup before running its own check. This also ensures that tests can be run in any sequence since Ghost Inspector launches up to +four tests simultaneously. ## Handling UI changes From 2f5ad6b38b9acc7e6acf021992d0e8167b10eb34 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 27 Nov 2019 15:43:41 -0500 Subject: [PATCH 011/111] Move event emitter before async function call so it will hopefully trigger the change event on the parent component earlier. Update this.changed inside if statement because it should only be updated if the file is successfully uploaded. --- js/components/upload_input.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/components/upload_input.js b/js/components/upload_input.js index 52cd8bf8..cdd9b15e 100644 --- a/js/components/upload_input.js +++ b/js/components/upload_input.js @@ -58,18 +58,18 @@ export default { this.$refs.attachmentFilename.value = file.name this.$refs.attachmentObjectName.value = response.objectName this.$refs.attachmentInput.disabled = true + emitFieldChange(this) + this.changed = true this.downloadLink = await this.getDownloadLink( file.name, response.objectName ) } else { + emitFieldChange(this) + this.changed = true this.uploadError = true } - - this.changed = true - - emitFieldChange(this) }, removeAttachment: function(e) { e.preventDefault() From 614514d6a2040ed9217f0ddb4f27c3ee50ff0ab2 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 25 Nov 2019 14:16:15 -0500 Subject: [PATCH 012/111] Update tables to match business logic --- .secrets.baseline | 4 +- ...9_update_schema_based_on_business_logic.py | 198 ++++++++++++++++++ atst/domain/task_orders.py | 6 +- atst/models/application_invitation.py | 5 +- atst/models/application_role.py | 4 +- atst/models/clin.py | 12 +- atst/models/environment_role.py | 4 +- atst/models/mixins/invites.py | 20 +- atst/models/portfolio.py | 6 +- atst/models/portfolio_invitation.py | 2 +- atst/models/portfolio_role.py | 4 +- atst/models/task_order.py | 5 +- atst/models/user.py | 4 +- atst/routes/task_orders/new.py | 6 +- script/seed_sample.py | 21 +- tests/domain/test_portfolios.py | 1 + tests/domain/test_task_orders.py | 2 - tests/domain/test_users.py | 15 +- tests/factories.py | 33 ++- tests/models/test_task_order.py | 12 +- tests/routes/task_orders/test_index.py | 2 +- tests/routes/task_orders/test_new.py | 34 ++- tests/test_access.py | 7 +- 23 files changed, 328 insertions(+), 79 deletions(-) create mode 100644 alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py diff --git a/.secrets.baseline b/.secrets.baseline index 07353d5a..7a81d3cb 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -161,7 +161,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 32, + "line_number": 31, "type": "Hex High Entropy String" } ], @@ -170,7 +170,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 656, + "line_number": 657, "type": "Hex High Entropy String" } ] diff --git a/alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py b/alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py new file mode 100644 index 00000000..06fd5d40 --- /dev/null +++ b/alembic/versions/67a2151d6269_update_schema_based_on_business_logic.py @@ -0,0 +1,198 @@ +"""update schema based on business logic + +Revision ID: 67a2151d6269 +Revises: 687fd43489d6 +Create Date: 2019-12-02 14:16:24.902108 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '67a2151d6269' # pragma: allowlist secret +down_revision = '687fd43489d6' # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('application_invitations', 'application_role_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('application_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=False) + op.alter_column('application_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('application_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('application_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=False) + op.alter_column('clins', 'end_date', + existing_type=sa.DATE(), + nullable=False) + op.alter_column('clins', 'jedi_clin_type', + existing_type=sa.VARCHAR(length=11), + nullable=False) + op.alter_column('clins', 'number', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('clins', 'obligated_amount', + existing_type=sa.NUMERIC(), + nullable=False) + op.alter_column('clins', 'start_date', + existing_type=sa.DATE(), + nullable=False) + op.alter_column('clins', 'total_amount', + existing_type=sa.NUMERIC(), + nullable=False) + op.alter_column('environment_roles', 'status', + existing_type=sa.VARCHAR(length=9), + nullable=False) + op.alter_column('portfolio_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=False) + op.alter_column('portfolio_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('portfolio_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_invitations', 'portfolio_role_id', + existing_type=postgresql.UUID(), + nullable=False) + op.alter_column('portfolio_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolio_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=False) + op.alter_column('portfolios', 'defense_component', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('portfolios', 'name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('task_orders', 'portfolio_id', + existing_type=postgresql.UUID(), + nullable=False) + op.drop_constraint('task_orders_user_id_fkey', 'task_orders', type_='foreignkey') + op.drop_column('task_orders', 'user_id') + op.alter_column('users', 'first_name', + existing_type=sa.VARCHAR(), + nullable=False) + op.alter_column('users', 'last_name', + existing_type=sa.VARCHAR(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'last_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('users', 'first_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.add_column('task_orders', sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=True)) + op.create_foreign_key('task_orders_user_id_fkey', 'task_orders', 'users', ['user_id'], ['id']) + op.alter_column('task_orders', 'portfolio_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('portfolios', 'name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolios', 'defense_component', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=True) + op.alter_column('portfolio_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_invitations', 'portfolio_role_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('portfolio_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('portfolio_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('portfolio_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=True) + op.alter_column('portfolio_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('environment_roles', 'status', + existing_type=sa.VARCHAR(length=9), + nullable=True) + op.alter_column('clins', 'total_amount', + existing_type=sa.NUMERIC(), + nullable=True) + op.alter_column('clins', 'start_date', + existing_type=sa.DATE(), + nullable=True) + op.alter_column('clins', 'obligated_amount', + existing_type=sa.NUMERIC(), + nullable=True) + op.alter_column('clins', 'number', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('clins', 'jedi_clin_type', + existing_type=sa.VARCHAR(length=11), + nullable=True) + op.alter_column('clins', 'end_date', + existing_type=sa.DATE(), + nullable=True) + op.alter_column('application_roles', 'status', + existing_type=sa.VARCHAR(length=8), + nullable=True) + op.alter_column('application_invitations', 'token', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'last_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'inviter_id', + existing_type=postgresql.UUID(), + nullable=True) + op.alter_column('application_invitations', 'first_name', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'expiration_time', + existing_type=postgresql.TIMESTAMP(timezone=True), + nullable=True) + op.alter_column('application_invitations', 'dod_id', + existing_type=sa.VARCHAR(), + nullable=True) + op.alter_column('application_invitations', 'application_role_id', + existing_type=postgresql.UUID(), + nullable=True) + # ### end Alembic commands ### diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 93521843..0c67e1d4 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -11,10 +11,8 @@ class TaskOrders(BaseDomainClass): resource_name = "task_order" @classmethod - def create(cls, creator, portfolio_id, number, clins, pdf): - task_order = TaskOrder( - portfolio_id=portfolio_id, creator=creator, number=number, pdf=pdf - ) + def create(cls, portfolio_id, number, clins, pdf): + task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf) db.session.add(task_order) db.session.commit() diff --git a/atst/models/application_invitation.py b/atst/models/application_invitation.py index d24cc54d..02be5e14 100644 --- a/atst/models/application_invitation.py +++ b/atst/models/application_invitation.py @@ -12,7 +12,10 @@ class ApplicationInvitation( __tablename__ = "application_invitations" application_role_id = Column( - UUID(as_uuid=True), ForeignKey("application_roles.id"), index=True + UUID(as_uuid=True), + ForeignKey("application_roles.id"), + index=True, + nullable=False, ) role = relationship( "ApplicationRole", diff --git a/atst/models/application_role.py b/atst/models/application_role.py index f8f7f201..d65ceac7 100644 --- a/atst/models/application_role.py +++ b/atst/models/application_role.py @@ -46,7 +46,9 @@ class ApplicationRole( UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True ) - status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) + status = Column( + SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False + ) permission_sets = relationship( "PermissionSet", secondary=application_roles_permission_sets diff --git a/atst/models/clin.py b/atst/models/clin.py index 2802e292..0624d985 100644 --- a/atst/models/clin.py +++ b/atst/models/clin.py @@ -23,12 +23,12 @@ class CLIN(Base, mixins.TimestampsMixin): task_order_id = Column(ForeignKey("task_orders.id"), nullable=False) task_order = relationship("TaskOrder") - number = Column(String, nullable=True) - start_date = Column(Date, nullable=True) - end_date = Column(Date, nullable=True) - total_amount = Column(Numeric(scale=2), nullable=True) - obligated_amount = Column(Numeric(scale=2), nullable=True) - jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True) + number = Column(String, nullable=False) + start_date = Column(Date, nullable=False) + end_date = Column(Date, nullable=False) + total_amount = Column(Numeric(scale=2), nullable=False) + obligated_amount = Column(Numeric(scale=2), nullable=False) + jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False) # # NOTE: For now obligated CLINS are CLIN 1 + CLIN 3 diff --git a/atst/models/environment_role.py b/atst/models/environment_role.py index 541b6d40..5b3a2c27 100644 --- a/atst/models/environment_role.py +++ b/atst/models/environment_role.py @@ -43,7 +43,9 @@ class EnvironmentRole( COMPLETED = "completed" DISABLED = "disabled" - status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) + status = Column( + SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False + ) def __repr__(self): return "".format( diff --git a/atst/models/mixins/invites.py b/atst/models/mixins/invites.py index 69e016f8..18916dc4 100644 --- a/atst/models/mixins/invites.py +++ b/atst/models/mixins/invites.py @@ -31,23 +31,29 @@ class InvitesMixin(object): @declared_attr def inviter_id(cls): - return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) + return Column( + UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False + ) @declared_attr def inviter(cls): return relationship("User", foreign_keys=[cls.inviter_id]) - status = Column(SQLAEnum(Status, native_enum=False, default=Status.PENDING)) + status = Column( + SQLAEnum(Status, native_enum=False, default=Status.PENDING, nullable=False) + ) - expiration_time = Column(TIMESTAMP(timezone=True)) + expiration_time = Column(TIMESTAMP(timezone=True), nullable=False) - token = Column(String, index=True, default=lambda: secrets.token_urlsafe()) + token = Column( + String, index=True, default=lambda: secrets.token_urlsafe(), nullable=False + ) email = Column(String, nullable=False) - dod_id = Column(String) - first_name = Column(String) - last_name = Column(String) + dod_id = Column(String, nullable=False) + first_name = Column(String, nullable=False) + last_name = Column(String, nullable=False) phone_number = Column(String) phone_ext = Column(String) diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index 08a65f1c..ed26b4d4 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -18,8 +18,10 @@ class Portfolio( __tablename__ = "portfolios" id = types.Id() - name = Column(String) - defense_component = Column(String) # Department of Defense Component + name = Column(String, nullable=False) + defense_component = Column( + String, nullable=False + ) # Department of Defense Component app_migration = Column(String) # App Migration complexity = Column(ARRAY(String)) # Application Complexity diff --git a/atst/models/portfolio_invitation.py b/atst/models/portfolio_invitation.py index 55d895c6..4ab9088d 100644 --- a/atst/models/portfolio_invitation.py +++ b/atst/models/portfolio_invitation.py @@ -12,7 +12,7 @@ class PortfolioInvitation( __tablename__ = "portfolio_invitations" portfolio_role_id = Column( - UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True + UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True, nullable=False ) role = relationship( "PortfolioRole", diff --git a/atst/models/portfolio_role.py b/atst/models/portfolio_role.py index 500a0ddd..53204e82 100644 --- a/atst/models/portfolio_role.py +++ b/atst/models/portfolio_role.py @@ -52,7 +52,9 @@ class PortfolioRole( UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True ) - status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING) + status = Column( + SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False + ) permission_sets = relationship( "PermissionSet", secondary=portfolio_roles_permission_sets diff --git a/atst/models/task_order.py b/atst/models/task_order.py index c79d3b83..85bf363a 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -33,12 +33,9 @@ class TaskOrder(Base, mixins.TimestampsMixin): id = types.Id() - portfolio_id = Column(ForeignKey("portfolios.id")) + portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False) portfolio = relationship("Portfolio") - user_id = Column(ForeignKey("users.id")) - creator = relationship("User", foreign_keys="TaskOrder.user_id") - pdf_attachment_id = Column(ForeignKey("attachments.id")) _pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id]) number = Column(String) # Task Order Number diff --git a/atst/models/user.py b/atst/models/user.py index 4ba23895..29b377d6 100644 --- a/atst/models/user.py +++ b/atst/models/user.py @@ -56,8 +56,8 @@ class User( email = Column(String) dod_id = Column(String, unique=True, nullable=False) - first_name = Column(String) - last_name = Column(String) + first_name = Column(String, nullable=False) + last_name = Column(String, nullable=False) phone_number = Column(String) phone_ext = Column(String) service_branch = Column(String) diff --git a/atst/routes/task_orders/new.py b/atst/routes/task_orders/new.py index 24460204..dca751e3 100644 --- a/atst/routes/task_orders/new.py +++ b/atst/routes/task_orders/new.py @@ -66,7 +66,7 @@ def update_task_order( task_order = TaskOrders.update(task_order_id, **form.data) portfolio_id = task_order.portfolio_id else: - task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data) + task_order = TaskOrders.create(portfolio_id, **form.data) return redirect(url_for(next_page, task_order_id=task_order.id)) else: @@ -181,9 +181,7 @@ def cancel_edit(task_order_id=None, portfolio_id=None): if task_order_id: task_order = TaskOrders.update(task_order_id, **form.data) else: - task_order = TaskOrders.create( - g.current_user, portfolio_id, **form.data - ) + task_order = TaskOrders.create(portfolio_id, **form.data) elif not save and task_order_id: TaskOrders.delete(task_order_id) diff --git a/script/seed_sample.py b/script/seed_sample.py index 11a45530..a9cec40b 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -30,6 +30,8 @@ from atst.domain.users import Users from atst.routes.dev import _DEV_USERS as DEV_USERS +from atst.utils import pick + from tests.factories import ( random_service_branch, TaskOrderFactory, @@ -238,6 +240,7 @@ def add_applications_to_portfolio(portfolio): None, first_name=user_data["first_name"], last_name=user_data["last_name"], + email=user_data["email"], ) app_role = ApplicationRoles.create( @@ -263,7 +266,23 @@ def add_applications_to_portfolio(portfolio): def create_demo_portfolio(name, data): try: - portfolio_owner = Users.get_or_create_by_dod_id("2345678901") # Amanda + portfolio_owner = Users.get_or_create_by_dod_id( + "2345678901", + **pick( + [ + "permission_sets", + "first_name", + "last_name", + "email", + "service_branch", + "phone_number", + "citizenship", + "designation", + "date_latest_training", + ], + DEV_USERS["amanda"], + ), + ) # Amanda # auditor = Users.get_by_dod_id("3453453453") # Sally except NotFoundError: print( 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_task_orders.py b/tests/domain/test_task_orders.py index 089741d5..8b1eb724 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -96,7 +96,6 @@ def test_create_adds_clins(): }, ] task_order = TaskOrders.create( - creator=portfolio.owner, portfolio_id=portfolio.id, number="0123456789", clins=clins, @@ -127,7 +126,6 @@ def test_update_adds_clins(): }, ] task_order = TaskOrders.create( - creator=task_order.creator, portfolio_id=task_order.portfolio_id, number="0000000000", clins=clins, 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..9c8f8c02 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -236,9 +236,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 +268,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 +286,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 +308,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/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/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index c800ce25..708b2bdc 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -30,7 +30,7 @@ def task_order(): portfolio = PortfolioFactory.create(owner=user) attachment = Attachment(filename="sample_attachment", object_name="sample") - return TaskOrderFactory.create(creator=user, portfolio=portfolio) + return TaskOrderFactory.create(portfolio=portfolio) 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..4d2f5fdb 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -20,14 +20,13 @@ def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) - return TaskOrderFactory.create(creator=user, portfolio=portfolio) + 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 +67,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 +76,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 +139,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 +147,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( @@ -164,7 +163,7 @@ def test_task_orders_submit_form_step_two_add_number(client, user_session, task_ 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 +178,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 +186,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", @@ -237,7 +236,7 @@ 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 - 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 +257,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 @@ -270,7 +269,7 @@ 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 ): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for("task_orders.form_step_four_review", task_order_id=task_order.id) ) @@ -280,7 +279,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", @@ -293,7 +292,7 @@ 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 ): - user_session(task_order.creator) + user_session(task_order.portfolio.owner) response = client.get( url_for( "task_orders.form_step_five_confirm_signature", task_order_id=task_order.id @@ -340,9 +339,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 +411,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..24948ec2 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -487,7 +487,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 +653,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 +690,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) From 57b00715d3c5d2040d44453991143df6a9983cf7 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Mon, 2 Dec 2019 13:53:44 -0500 Subject: [PATCH 013/111] Remove optional validator from CLINField number because it is a required column --- atst/forms/task_order.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/atst/forms/task_order.py b/atst/forms/task_order.py index 3ac5640b..2a1b8ad2 100644 --- a/atst/forms/task_order.py +++ b/atst/forms/task_order.py @@ -7,7 +7,7 @@ from wtforms.fields import ( HiddenField, ) from wtforms.fields.html5 import DateField -from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError +from wtforms.validators import Required, Length, NumberRange, ValidationError from flask_wtf import FlaskForm from numbers import Number @@ -61,9 +61,7 @@ class CLINForm(FlaskForm): coerce=coerce_enum, ) - number = StringField( - label=translate("task_orders.form.clin_number_label"), validators=[Optional()] - ) + number = StringField(label=translate("task_orders.form.clin_number_label")) start_date = DateField( translate("task_orders.form.pop_start"), description=translate("task_orders.form.pop_example"), From 26c5b5ea7f3972b51e061466aba0066eefa2cd79 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 14 Nov 2019 13:25:30 -0500 Subject: [PATCH 014/111] Add JSON logging back for NGINX container. This configures the NGINX container to log in JSON. It also updates the K8s config so that we mount all of the key/value pairs available in the atst-nginx ConfigMap as files in "/etc/nginx/conf.d" inside the container. This simplifies the config a little. --- deploy/azure/atst-nginx-configmap.yml | 26 ++++++++++++++++++++++++-- deploy/azure/azure.yml | 6 +----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/deploy/azure/atst-nginx-configmap.yml b/deploy/azure/atst-nginx-configmap.yml index e19c6d54..22d6d97e 100644 --- a/deploy/azure/atst-nginx-configmap.yml +++ b/deploy/azure/atst-nginx-configmap.yml @@ -5,8 +5,10 @@ metadata: name: atst-nginx namespace: atat data: - nginx-config: |- + atst.conf: |- server { + access_log /var/log/nginx/access.log json; + listen ${PORT_PREFIX}342; server_name ${MAIN_DOMAIN}; root /usr/share/nginx/html; @@ -18,6 +20,8 @@ data: } } server { + access_log /var/log/nginx/access.log json; + listen ${PORT_PREFIX}343; server_name ${AUTH_DOMAIN}; root /usr/share/nginx/html; @@ -29,6 +33,8 @@ data: } } server { + access_log /var/log/nginx/access.log json; + server_name ${MAIN_DOMAIN}; # access_log /var/log/nginx/access.log json; listen ${PORT_PREFIX}442 ssl; @@ -58,7 +64,8 @@ data: } } server { - # access_log /var/log/nginx/access.log json; + access_log /var/log/nginx/access.log json; + server_name ${AUTH_DOMAIN}; listen ${PORT_PREFIX}443 ssl; listen [::]:${PORT_PREFIX}443 ssl ipv6only=on; @@ -88,3 +95,18 @@ data: uwsgi_param HTTP_X_REQUEST_ID $request_id; } } + 00json_log.conf: |- + log_format json escape=json + '{' + '"timestamp":"$time_iso8601",' + '"msec":"$msec",' + '"request_id":"$request_id",' + '"remote_addr":"$remote_addr",' + '"remote_user":"$remote_user",' + '"request":"$request",' + '"status":$status,' + '"body_bytes_sent":$body_bytes_sent,' + '"referer":"$http_referer",' + '"user_agent":"$http_user_agent",' + '"http_x_forwarded_for":"$http_x_forwarded_for"' + '}'; diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 8d46fa4b..3ed3ea61 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -62,8 +62,7 @@ spec: name: auth volumeMounts: - name: nginx-config - mountPath: "/etc/nginx/conf.d/atst.conf" - subPath: atst.conf + mountPath: "/etc/nginx/conf.d/" - name: uwsgi-socket-dir mountPath: "/var/run/uwsgi" - name: nginx-htpasswd @@ -90,9 +89,6 @@ spec: - name: nginx-config configMap: name: atst-nginx - items: - - key: nginx-config - path: atst.conf - name: uwsgi-socket-dir emptyDir: medium: Memory From a3aa3e69352eadec8d2db976858461843495a4f5 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 14 Nov 2019 14:20:17 -0500 Subject: [PATCH 015/111] Config for NGINX SSL/TLS. This adds additional SSL/TLS config to specify the acceptable TLS version, cipher suites, session cache, etc. Values are currently based on the Mozilla Foundation's recommendations for intermediate compatibility: https://wiki.mozilla.org/Security/Server_Side_TLS We will manage NGINX configuration snippets as a K8s ConfigMap so that they can be included in server blocks as-needed. --- deploy/azure/atst-nginx-configmap.yml | 8 +- deploy/azure/azure.yml | 143 ++++++++++++++------------ deploy/azure/kustomization.yaml | 1 + deploy/azure/nginx-snippets.yml | 24 +++++ 4 files changed, 106 insertions(+), 70 deletions(-) create mode 100644 deploy/azure/nginx-snippets.yml diff --git a/deploy/azure/atst-nginx-configmap.yml b/deploy/azure/atst-nginx-configmap.yml index 22d6d97e..b702924c 100644 --- a/deploy/azure/atst-nginx-configmap.yml +++ b/deploy/azure/atst-nginx-configmap.yml @@ -41,6 +41,9 @@ data: listen [::]:${PORT_PREFIX}442 ssl ipv6only=on; ssl_certificate /etc/ssl/private/atat.crt; ssl_certificate_key /etc/ssl/private/atat.key; + # additional SSL/TLS settings + include /etc/nginx/snippets/ssl.conf + location /login-redirect { return 301 https://auth-azure.atat.code.mil$request_uri; } @@ -75,8 +78,9 @@ data: ssl_verify_client on; ssl_verify_depth 10; ssl_client_certificate /etc/ssl/client-ca-bundle.pem; - # Guard against HTTPS -> HTTP downgrade - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + # additional SSL/TLS settings + include /etc/nginx/snippets/ssl.conf + location / { return 301 https://azure.atat.code.mil$request_uri; } diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 3ed3ea61..4ed180fb 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -30,8 +30,8 @@ spec: - name: atst image: $CONTAINER_IMAGE envFrom: - - configMapRef: - name: atst-envvars + - configMapRef: + name: atst-envvars volumeMounts: - name: atst-config mountPath: "/opt/atat/atst/atst-overrides.ini" @@ -74,14 +74,16 @@ spec: mountPath: "/etc/ssl/" - name: acme mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/" + - name: snippets + mountPath: "/etc/nginx/snippets/" volumes: - name: atst-config secret: secretName: atst-config-ini items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 + - key: override.ini + path: atst-overrides.ini + mode: 0644 - name: nginx-client-ca-bundle configMap: name: nginx-client-ca-bundle @@ -96,19 +98,19 @@ spec: secret: secretName: atst-nginx-htpasswd items: - - key: htpasswd - path: .htpasswd - mode: 0640 + - key: htpasswd + path: .htpasswd + mode: 0640 - name: tls secret: secretName: azure-atat-code-mil-tls items: - - key: tls.crt - path: atat.crt - mode: 0644 - - key: tls.key - path: atat.key - mode: 0640 + - key: tls.crt + path: atat.crt + mode: 0644 + - key: tls.key + path: atat.key + mode: 0640 - name: crls-vol persistentVolumeClaim: claimName: crls-vol-claim @@ -116,9 +118,9 @@ spec: configMap: name: pgsslrootcert items: - - key: cert - path: pgsslrootcert.crt - mode: 0666 + - key: cert + path: pgsslrootcert.crt + mode: 0666 - name: acme configMap: name: acme-challenges @@ -128,9 +130,12 @@ spec: name: uwsgi-config defaultMode: 0666 items: - - key: uwsgi.ini - path: uwsgi.ini - mode: 0644 + - key: uwsgi.ini + path: uwsgi.ini + mode: 0644 + - name: snippets + configMap: + name: nginx-snippets --- apiVersion: extensions/v1beta1 kind: Deployment @@ -157,19 +162,20 @@ spec: containers: - name: atst-worker image: $CONTAINER_IMAGE - args: [ - "/opt/atat/atst/.venv/bin/python", - "/opt/atat/atst/.venv/bin/celery", - "-A", - "celery_worker.celery", - "worker", - "--loglevel=info" - ] + args: + [ + "/opt/atat/atst/.venv/bin/python", + "/opt/atat/atst/.venv/bin/celery", + "-A", + "celery_worker.celery", + "worker", + "--loglevel=info", + ] envFrom: - - configMapRef: - name: atst-envvars - - configMapRef: - name: atst-worker-envvars + - configMapRef: + name: atst-envvars + - configMapRef: + name: atst-worker-envvars volumeMounts: - name: atst-config mountPath: "/opt/atat/atst/atst-overrides.ini" @@ -182,16 +188,16 @@ spec: secret: secretName: atst-config-ini items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 + - key: override.ini + path: atst-overrides.ini + mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert items: - - key: cert - path: pgsslrootcert.crt - mode: 0666 + - key: cert + path: pgsslrootcert.crt + mode: 0666 --- apiVersion: extensions/v1beta1 kind: Deployment @@ -218,19 +224,20 @@ spec: containers: - name: atst-beat image: $CONTAINER_IMAGE - args: [ - "/opt/atat/atst/.venv/bin/python", - "/opt/atat/atst/.venv/bin/celery", - "-A", - "celery_worker.celery", - "beat", - "--loglevel=info" - ] + args: + [ + "/opt/atat/atst/.venv/bin/python", + "/opt/atat/atst/.venv/bin/celery", + "-A", + "celery_worker.celery", + "beat", + "--loglevel=info", + ] envFrom: - - configMapRef: - name: atst-envvars - - configMapRef: - name: atst-worker-envvars + - configMapRef: + name: atst-envvars + - configMapRef: + name: atst-worker-envvars volumeMounts: - name: atst-config mountPath: "/opt/atat/atst/atst-overrides.ini" @@ -243,16 +250,16 @@ spec: secret: secretName: atst-config-ini items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 + - key: override.ini + path: atst-overrides.ini + mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert items: - - key: cert - path: pgsslrootcert.crt - mode: 0666 + - key: cert + path: pgsslrootcert.crt + mode: 0666 --- apiVersion: v1 kind: Service @@ -264,12 +271,12 @@ metadata: spec: loadBalancerIP: 13.92.235.6 ports: - - port: 80 - targetPort: 8342 - name: http - - port: 443 - targetPort: 8442 - name: https + - port: 80 + targetPort: 8342 + name: http + - port: 443 + targetPort: 8442 + name: https selector: role: web type: LoadBalancer @@ -284,12 +291,12 @@ metadata: spec: loadBalancerIP: 23.100.24.41 ports: - - port: 80 - targetPort: 8343 - name: http - - port: 443 - targetPort: 8443 - name: https + - port: 80 + targetPort: 8343 + name: http + - port: 443 + targetPort: 8443 + name: https selector: role: web type: LoadBalancer diff --git a/deploy/azure/kustomization.yaml b/deploy/azure/kustomization.yaml index 43e6f813..9dee809c 100644 --- a/deploy/azure/kustomization.yaml +++ b/deploy/azure/kustomization.yaml @@ -11,3 +11,4 @@ resources: - nginx-client-ca-bundle.yml - acme-challenges.yml - aadpodidentity.yml + - nginx-snippets.yml diff --git a/deploy/azure/nginx-snippets.yml b/deploy/azure/nginx-snippets.yml new file mode 100644 index 00000000..fc38751f --- /dev/null +++ b/deploy/azure/nginx-snippets.yml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-snippets + namespace: atat +data: + ssl.conf: |- + # Guard against HTTPS -> HTTP downgrade + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + # Set SSL protocols, ciphers, and related options + ssl_protocols TLSv1.3 TLSv1.2; + ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384' + ssl_prefer_server_ciphers on; + ssl_ecdh_curve X25519:prime256v1:secp384r1; + ssl_dhparam /etc/ssl/dhparam.pem; + # SSL session options + ssl_session_timeout 4h; + ssl_session_cache shared:SSL:10m; # 1mb = ~4000 sessions + ssl_session_tickets off; + # OCSP Stapling + ssl_stapling on; + ssl_stapling_verify on; + resolver 8.8.8.8 8.8.4.4; From 6acc085a771803353af9f75882afd983cd6f178b Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 25 Nov 2019 14:03:41 -0500 Subject: [PATCH 016/111] Use dhparam.pem from AZ Key Vault --- deploy/azure/azure.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 4ed180fb..58491c9c 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -23,6 +23,7 @@ spec: labels: app: atst role: web + aadpodidbinding: atat-kv-id-binding spec: securityContext: fsGroup: 101 @@ -76,6 +77,9 @@ spec: mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/" - name: snippets mountPath: "/etc/nginx/snippets/" + - name: nginx-dhparam-secret + mountPath: "/etc/ssl/" + readOnly: true volumes: - name: atst-config secret: @@ -136,6 +140,16 @@ spec: - name: snippets configMap: name: nginx-snippets + - name: nginx-dhparam-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "dhparam4096" + keyvaultobjectaliases: "dhparam.pem" + keyvaultobjecttypes: secret + tenantid: "b5ab0e1e-09f8-4258-afb7-fb17654bc5b3" --- apiVersion: extensions/v1beta1 kind: Deployment From 949ffa294dd8f0bba586967548fe03aa1d4279d5 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 25 Nov 2019 14:38:56 -0500 Subject: [PATCH 017/111] Use a single FlexVolume for nginx secrets Just a name update for now, but we'll use the one flex volume to mount all the nginx related secrets going forward. --- deploy/azure/azure.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 58491c9c..0f0033c5 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -77,7 +77,7 @@ spec: mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/" - name: snippets mountPath: "/etc/nginx/snippets/" - - name: nginx-dhparam-secret + - name: nginx-secret mountPath: "/etc/ssl/" readOnly: true volumes: @@ -140,7 +140,7 @@ spec: - name: snippets configMap: name: nginx-snippets - - name: nginx-dhparam-secret + - name: nginx-secret flexVolume: driver: "azure/kv" options: From 9469d1ff1b99ba64b89a94d373aa3ad508ec607f Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 25 Nov 2019 14:40:10 -0500 Subject: [PATCH 018/111] Introduce TEMPLATE_ID variable for FlexVolume FlexVolume requires you specify the tenant id of the key vault instance, so this will need to be templated in for future enviroments --- deploy/README.md | 1 + deploy/azure/azure.yml | 2 +- script/k8s_config | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/README.md b/deploy/README.md index be66290d..731d07d7 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -14,6 +14,7 @@ The production configuration (azure.atat.code.mil, currently) is reflected in th - AUTH_DOMAIN: The host domain for the authentication endpoint for the environment. - KV_MI_ID: the fully qualified id (path) of the managed identity for the key vault (instructions on retrieving this are down in section on [Setting up FlexVol](#configuring-the-identity)). Example: /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/RESOURCE_GROUP_NAME/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MANAGED_IDENTITY_NAME - KV_MI_CLIENT_ID: The client id of the managed identity for the key vault. This is a GUID. +- TENANT_ID: The id of the active directory tenant in which the cluster and it's associated users exist. This is a GUID. We use envsubst to substitute values for these variables. There is a wrapper script (script/k8s_config) that will output the compiled configuration, using a combination of kustomize and envsubst. diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 0f0033c5..ddbbfe18 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -149,7 +149,7 @@ spec: keyvaultobjectnames: "dhparam4096" keyvaultobjectaliases: "dhparam.pem" keyvaultobjecttypes: secret - tenantid: "b5ab0e1e-09f8-4258-afb7-fb17654bc5b3" + tenantid: $TENANT_ID --- apiVersion: extensions/v1beta1 kind: Deployment diff --git a/script/k8s_config b/script/k8s_config index ee3c9878..b489c942 100755 --- a/script/k8s_config +++ b/script/k8s_config @@ -13,6 +13,7 @@ SETTINGS=( AUTH_DOMAIN KV_MI_ID KV_MI_CLIENT_ID + TENANT_ID ) # Loop all expected settings. Track ones that are missing and build From 1c4e00e9142468d18391ba11cb4abe3ab7ff8c88 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 25 Nov 2019 15:01:13 -0500 Subject: [PATCH 019/111] Update Deploy Readme for FlexVol consumption Explain via example how you can use FlexVol to mount secrets in our containers. --- deploy/README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/deploy/README.md b/deploy/README.md index 731d07d7..6f8c4cf1 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -218,3 +218,45 @@ Example values: 5. The file `deploy/azure/aadpodidentity.yml` is templated via Kustomize, so you'll need to include clientId (as `KV_MI_CLIENT_ID`) and id (as `KV_MI_ID`) of the managed identity as part of the call to Kustomize. +## Using the FlexVol + +There are 3 steps to using the FlexVol to access secrets from KeyVault + +1. For the resource in which you would like to mount a FlexVol, add a metadata label with the selector from `aadpodidentity.yml` + ``` + metadata: + labels: + app: atst + role: web + aadpodidbinding: atat-kv-id-binding + ``` + +2. Register the FlexVol as a mount and specifiy which secrets you want to mount, along with the file name they should have. The `keyvaultobjectnames`, `keyvaultobjectaliases`, and `keyvaultobjecttypes` correspond to one another, positionally. They are passed as semicolon delimited strings, examples below. + + ``` + - name: volume-of-secrets + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "" + keyvaultobjectnames: "mysecret;mykey;mycert" + keyvaultobjectaliases: "mysecret.pem;mykey.txt;mycert.crt" + keyvaultobjecttypes: "secret;key;cert" + tenantid: $TENANT_ID + ``` + +3. Tell the resource where to mount your new volume, using the same name that you specified for the volume above. + ``` + - name: nginx-secret + mountPath: "/usr/secrets/" + readOnly: true + ``` + +4. Once applied, the directory specified in the `mountPath` argument will contain the files you specified in the flexVolume. In our case, you would be able to do this: + ``` + $ kubectl exec -it CONTAINER_NAME -c atst ls /usr/secrets + mycert.crt + mykey.txt + mysecret.pem + ``` From 9b8d5e36626f989c11b468eb38b7f321be1a7923 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 26 Nov 2019 11:03:53 -0500 Subject: [PATCH 020/111] Document generation and updating of dhparams. --- deploy/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deploy/README.md b/deploy/README.md index 6f8c4cf1..25380293 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -170,6 +170,12 @@ Then: kubectl -n atat create secret tls azure-atat-code-mil-tls --key="[path to the private key]" --cert="[path to the full chain]" ``` +### Create the Diffie-Hellman parameters + +Diffie-Hellman parameters allow per-session encryption of SSL traffic to help improve security. We currently store our parameters in KeyVault, the value can be updated using the following command. Note: Generating the new paramter can take over 10 minutes and there won't be any output while it's running. +``` +az keyvault secret set --vault-name --name --value "$(openssl genpkey -genparam -algorithm DH -outform pem -pkeyopt dh_paramgen_prime_len:4096 2> /dev/null)" +``` --- # Setting Up FlexVol for Secrets From 26bb2f46141a5c25a83b0fe92561bc891f4bb4db Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 26 Nov 2019 17:01:16 -0500 Subject: [PATCH 021/111] Use mounted all-in-one cert for nginx ssl Mount the combined key and cert for nginx ssl using flexvol and point the necessary nginx config at it. --- deploy/azure/atst-nginx-configmap.yml | 8 ++++---- deploy/azure/azure.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/azure/atst-nginx-configmap.yml b/deploy/azure/atst-nginx-configmap.yml index b702924c..77de51f9 100644 --- a/deploy/azure/atst-nginx-configmap.yml +++ b/deploy/azure/atst-nginx-configmap.yml @@ -39,8 +39,8 @@ data: # access_log /var/log/nginx/access.log json; listen ${PORT_PREFIX}442 ssl; listen [::]:${PORT_PREFIX}442 ssl ipv6only=on; - ssl_certificate /etc/ssl/private/atat.crt; - ssl_certificate_key /etc/ssl/private/atat.key; + ssl_certificate /etc/ssl/atat.crt; + ssl_certificate_key /etc/ssl/atat.crt; # additional SSL/TLS settings include /etc/nginx/snippets/ssl.conf @@ -72,8 +72,8 @@ data: server_name ${AUTH_DOMAIN}; listen ${PORT_PREFIX}443 ssl; listen [::]:${PORT_PREFIX}443 ssl ipv6only=on; - ssl_certificate /etc/ssl/private/atat.crt; - ssl_certificate_key /etc/ssl/private/atat.key; + ssl_certificate /etc/ssl/atat.crt; + ssl_certificate_key /etc/ssl/atat.crt; # Request and validate client certificate ssl_verify_client on; ssl_verify_depth 10; diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index ddbbfe18..e391fc54 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -146,8 +146,8 @@ spec: options: usepodidentity: "true" keyvaultname: "atat-vault-test" - keyvaultobjectnames: "dhparam4096" - keyvaultobjectaliases: "dhparam.pem" + keyvaultobjectnames: "dhparam4096;staging-cert" + keyvaultobjectaliases: "dhparam.pem;atat.crt" keyvaultobjecttypes: secret tenantid: $TENANT_ID --- From 221e9ab26b77c39cb4e03fbf2a9371ca879ad1c1 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 26 Nov 2019 17:02:04 -0500 Subject: [PATCH 022/111] Add a staging overlay for the key vault name Currently we're just using the test vault, but in the future we want to be able to prescribe vault names for different environments via overlay. --- deploy/overlays/staging/kustomization.yaml | 1 + deploy/overlays/staging/vault_name.yml | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 deploy/overlays/staging/vault_name.yml diff --git a/deploy/overlays/staging/kustomization.yaml b/deploy/overlays/staging/kustomization.yaml index 83450cf5..51d34b15 100644 --- a/deploy/overlays/staging/kustomization.yaml +++ b/deploy/overlays/staging/kustomization.yaml @@ -7,6 +7,7 @@ patchesStrategicMerge: - replica_count.yml - ports.yml - envvars.yml + - vault_name.yml patchesJson6902: - target: group: extensions diff --git a/deploy/overlays/staging/vault_name.yml b/deploy/overlays/staging/vault_name.yml new file mode 100644 index 00000000..fa364270 --- /dev/null +++ b/deploy/overlays/staging/vault_name.yml @@ -0,0 +1,12 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: atst +spec: + template: + spec: + volumes: + - name: nginx-secret + flexVolume: + options: + keyvaultname: "atat-vault-test" From 36406372e3d464ccb7acbbd7a2268203e3d701e4 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 11:41:56 -0500 Subject: [PATCH 023/111] Remove unused secret volume for tls key and cert --- deploy/azure/azure.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index e391fc54..99eb847d 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -105,16 +105,6 @@ spec: - key: htpasswd path: .htpasswd mode: 0640 - - name: tls - secret: - secretName: azure-atat-code-mil-tls - items: - - key: tls.crt - path: atat.crt - mode: 0644 - - key: tls.key - path: atat.key - mode: 0640 - name: crls-vol persistentVolumeClaim: claimName: crls-vol-claim From 253ddaa49edcebe5ec2d3225ad67643e395418e8 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 11:42:36 -0500 Subject: [PATCH 024/111] Properly register key vault object types --- deploy/azure/azure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 99eb847d..5859d94a 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -138,7 +138,7 @@ spec: keyvaultname: "atat-vault-test" keyvaultobjectnames: "dhparam4096;staging-cert" keyvaultobjectaliases: "dhparam.pem;atat.crt" - keyvaultobjecttypes: secret + keyvaultobjecttypes: "secret;cert" tenantid: $TENANT_ID --- apiVersion: extensions/v1beta1 From 33ce02d0458b090c9cd6f0da7ea8cbec127a47fd Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 11:43:26 -0500 Subject: [PATCH 025/111] Better differentiate between master and staging vault config via overlay --- deploy/azure/azure.yml | 2 +- deploy/overlays/staging/{vault_name.yml => flex_vol.yml} | 1 + deploy/overlays/staging/kustomization.yaml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename deploy/overlays/staging/{vault_name.yml => flex_vol.yml} (78%) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 5859d94a..c8c498c4 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -136,7 +136,7 @@ spec: options: usepodidentity: "true" keyvaultname: "atat-vault-test" - keyvaultobjectnames: "dhparam4096;staging-cert" + keyvaultobjectnames: "dhparam4096;master-cert" keyvaultobjectaliases: "dhparam.pem;atat.crt" keyvaultobjecttypes: "secret;cert" tenantid: $TENANT_ID diff --git a/deploy/overlays/staging/vault_name.yml b/deploy/overlays/staging/flex_vol.yml similarity index 78% rename from deploy/overlays/staging/vault_name.yml rename to deploy/overlays/staging/flex_vol.yml index fa364270..ef8ca168 100644 --- a/deploy/overlays/staging/vault_name.yml +++ b/deploy/overlays/staging/flex_vol.yml @@ -10,3 +10,4 @@ spec: flexVolume: options: keyvaultname: "atat-vault-test" + keyvaultobjectnames: "dhparam4096;staging-cert" diff --git a/deploy/overlays/staging/kustomization.yaml b/deploy/overlays/staging/kustomization.yaml index 51d34b15..38251002 100644 --- a/deploy/overlays/staging/kustomization.yaml +++ b/deploy/overlays/staging/kustomization.yaml @@ -7,7 +7,7 @@ patchesStrategicMerge: - replica_count.yml - ports.yml - envvars.yml - - vault_name.yml + - flex_vol.yml patchesJson6902: - target: group: extensions From 5006945cfe73d8880a874b3650dfff018a7d8678 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 14:01:35 -0500 Subject: [PATCH 026/111] Remove tls volumeMount --- deploy/azure/azure.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index c8c498c4..e8a9eacb 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -69,8 +69,6 @@ spec: - name: nginx-htpasswd mountPath: "/etc/nginx/.htpasswd" subPath: .htpasswd - - name: tls - mountPath: "/etc/ssl/private" - name: nginx-client-ca-bundle mountPath: "/etc/ssl/" - name: acme From 9ac52493080d84ac7b18261a1305907d08bd95bd Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 15:38:15 -0500 Subject: [PATCH 027/111] Add .env files to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 19f4acc5..d8e2290d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ static/buildinfo.* log/* config/dev.ini +.env* # CRLs /crl From df6ab4a01626af341bf2e7df3ade1d0b39a8528b Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 15:40:25 -0500 Subject: [PATCH 028/111] Fix some formatting problems in nginx configs --- deploy/azure/atst-nginx-configmap.yml | 4 ++-- deploy/azure/nginx-snippets.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/azure/atst-nginx-configmap.yml b/deploy/azure/atst-nginx-configmap.yml index 77de51f9..b7959845 100644 --- a/deploy/azure/atst-nginx-configmap.yml +++ b/deploy/azure/atst-nginx-configmap.yml @@ -42,7 +42,7 @@ data: ssl_certificate /etc/ssl/atat.crt; ssl_certificate_key /etc/ssl/atat.crt; # additional SSL/TLS settings - include /etc/nginx/snippets/ssl.conf + include /etc/nginx/snippets/ssl.conf; location /login-redirect { return 301 https://auth-azure.atat.code.mil$request_uri; @@ -79,7 +79,7 @@ data: ssl_verify_depth 10; ssl_client_certificate /etc/ssl/client-ca-bundle.pem; # additional SSL/TLS settings - include /etc/nginx/snippets/ssl.conf + include /etc/nginx/snippets/ssl.conf; location / { return 301 https://azure.atat.code.mil$request_uri; diff --git a/deploy/azure/nginx-snippets.yml b/deploy/azure/nginx-snippets.yml index fc38751f..916d9524 100644 --- a/deploy/azure/nginx-snippets.yml +++ b/deploy/azure/nginx-snippets.yml @@ -10,7 +10,7 @@ data: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; # Set SSL protocols, ciphers, and related options ssl_protocols TLSv1.3 TLSv1.2; - ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384' + ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on; ssl_ecdh_curve X25519:prime256v1:secp384r1; ssl_dhparam /etc/ssl/dhparam.pem; From 728bb5713f2d1d814f4f1dc5d2ab61b375f51ebb Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 2 Dec 2019 15:41:46 -0500 Subject: [PATCH 029/111] Fix flexVol serving of nginx certificates FlexVol requires that you specify certificates as secrets in order to get both the certificate and private key in the appropriate format for nginx to consume. Additionally, flexvol shouldn't interfer with other secrets mounted in it's host directory. --- deploy/azure/atst-nginx-configmap.yml | 4 ++-- deploy/azure/azure.yml | 15 +++++++++------ deploy/overlays/staging/flex_vol.yml | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/deploy/azure/atst-nginx-configmap.yml b/deploy/azure/atst-nginx-configmap.yml index b7959845..5f51c7d6 100644 --- a/deploy/azure/atst-nginx-configmap.yml +++ b/deploy/azure/atst-nginx-configmap.yml @@ -40,7 +40,7 @@ data: listen ${PORT_PREFIX}442 ssl; listen [::]:${PORT_PREFIX}442 ssl ipv6only=on; ssl_certificate /etc/ssl/atat.crt; - ssl_certificate_key /etc/ssl/atat.crt; + ssl_certificate_key /etc/ssl/atat.key; # additional SSL/TLS settings include /etc/nginx/snippets/ssl.conf; @@ -73,7 +73,7 @@ data: listen ${PORT_PREFIX}443 ssl; listen [::]:${PORT_PREFIX}443 ssl ipv6only=on; ssl_certificate /etc/ssl/atat.crt; - ssl_certificate_key /etc/ssl/atat.crt; + ssl_certificate_key /etc/ssl/atat.key; # Request and validate client certificate ssl_verify_client on; ssl_verify_depth 10; diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index e8a9eacb..02952029 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -70,14 +70,14 @@ spec: mountPath: "/etc/nginx/.htpasswd" subPath: .htpasswd - name: nginx-client-ca-bundle - mountPath: "/etc/ssl/" + mountPath: "/etc/ssl/client-ca-bundle.pem" + subPath: "client-ca-bundle.pem" - name: acme mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/" - name: snippets mountPath: "/etc/nginx/snippets/" - name: nginx-secret mountPath: "/etc/ssl/" - readOnly: true volumes: - name: atst-config secret: @@ -89,7 +89,10 @@ spec: - name: nginx-client-ca-bundle configMap: name: nginx-client-ca-bundle - defaultMode: 0666 + defaultMode: 0444 + items: + - key: "client-ca-bundle.pem" + path: "client-ca-bundle.pem" - name: nginx-config configMap: name: atst-nginx @@ -134,9 +137,9 @@ spec: options: usepodidentity: "true" keyvaultname: "atat-vault-test" - keyvaultobjectnames: "dhparam4096;master-cert" - keyvaultobjectaliases: "dhparam.pem;atat.crt" - keyvaultobjecttypes: "secret;cert" + keyvaultobjectnames: "dhparam4096;master-cert;master-cert" + keyvaultobjectaliases: "dhparam.pem;atat.key;atat.crt" + keyvaultobjecttypes: "secret;secret;secret" tenantid: $TENANT_ID --- apiVersion: extensions/v1beta1 diff --git a/deploy/overlays/staging/flex_vol.yml b/deploy/overlays/staging/flex_vol.yml index ef8ca168..0ebeea84 100644 --- a/deploy/overlays/staging/flex_vol.yml +++ b/deploy/overlays/staging/flex_vol.yml @@ -10,4 +10,4 @@ spec: flexVolume: options: keyvaultname: "atat-vault-test" - keyvaultobjectnames: "dhparam4096;staging-cert" + keyvaultobjectnames: "dhparam4096;staging-cert;staging-cert" From b1814db50be192c631e433961db2fe26f8a33a28 Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 3 Dec 2019 09:27:10 -0500 Subject: [PATCH 030/111] Limit integration tests to only run on protected branches. Because of limitations with CircleCI, we should limit Ghost Inspector tests to only run on merge commits to our protected branches. This will allow us to build every commit in CI without exhausting our monthly allotment of Ghost Inspector test runs. Once that setting has been enabled in CircleCI ("build every commit") we will not longer have to worry about what our default branch in the Github repo is. --- .circleci/config.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index af450fad..082ff825 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -293,6 +293,11 @@ workflows: - integration-tests: requires: - docker-build + filters: + branches: + only: + - staging + - master - deploy-staging: requires: - test From f4ffde89d0205100417bac119c96c6918f051139 Mon Sep 17 00:00:00 2001 From: dandds Date: Tue, 3 Dec 2019 09:19:44 -0500 Subject: [PATCH 031/111] Add more restrictions to K8s CRL CronJob. The K8s CronJob that manages CRL syncing often leaves pods hanging around for days at a time. This appears to happen when the download of a particular CRL from DISA hangs for whatever reason. This updates the configuration so that a running cronjob is automatically replaced by its successor, rather than the two running concurrently. (The CRL CronJob runs every hour, and it one has taken that long then it's hanging and needs to be replace.) Similarly, this updates the config to only retain one successful CRL pod, rather than the default of three. --- deploy/azure/crls-sync.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/azure/crls-sync.yaml b/deploy/azure/crls-sync.yaml index f5bcdcf8..5e95e331 100644 --- a/deploy/azure/crls-sync.yaml +++ b/deploy/azure/crls-sync.yaml @@ -5,6 +5,8 @@ metadata: namespace: atat spec: schedule: "0 * * * *" + concurrencyPolicy: Replace + successfulJobsHistoryLimit: 1 jobTemplate: spec: template: From d1b6e7833dbc6393bd26d67a16d6d0fe3a7d5cfe Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 13:21:46 -0500 Subject: [PATCH 032/111] Delete styling that removed focus styling --- styles/atat.scss | 8 -------- 1 file changed, 8 deletions(-) diff --git a/styles/atat.scss b/styles/atat.scss index c3fd1a55..4c8aa263 100644 --- a/styles/atat.scss +++ b/styles/atat.scss @@ -45,11 +45,3 @@ @import "sections/application_edit"; @import "sections/reports"; @import "sections/task_order"; - -// -// IE likes to display an outline when focusing on an element. This -// fix removes that unwanted outline on focus. -// -*:focus { - outline: 0; -} From 9ef4f693ebc9d4d26752e150e355806dff325f03 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 10:29:50 -0500 Subject: [PATCH 033/111] Sticky CTA styling fixes: 1. Remove gap between sidenav and sticky cta 2. Update z-index of sticky cta so it is behind the sidenav --- styles/components/_sticky_cta.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/components/_sticky_cta.scss b/styles/components/_sticky_cta.scss index c5946951..ce05144f 100644 --- a/styles/components/_sticky_cta.scss +++ b/styles/components/_sticky_cta.scss @@ -1,7 +1,7 @@ .sticky-cta { - margin-left: -$gap * 4; + margin-left: -$gap * 5; margin-right: -$gap * 5; - z-index: 10; + z-index: 1; background-color: $color-gray-lightest; border-top: 1px solid $color-gray-lighter; border-bottom: 1px solid $color-gray-lighter; From 74ff581570fe54f940ef82269952357f13d83cbc Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 14:47:55 -0500 Subject: [PATCH 034/111] Remove route for /portfolios --- .secrets.baseline | 4 ++-- atst/routes/portfolios/index.py | 10 ---------- templates/portfolios/blank_slate.html | 15 --------------- tests/routes/portfolios/test_index.py | 26 -------------------------- tests/test_access.py | 1 - tests/test_auth.py | 7 ------- 6 files changed, 2 insertions(+), 61 deletions(-) delete mode 100644 templates/portfolios/blank_slate.html diff --git a/.secrets.baseline b/.secrets.baseline index 7a81d3cb..49a70104 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "lines": null }, - "generated_at": "2019-11-26T21:33:43Z", + "generated_at": "2019-12-03T19:44:47Z", "plugins_used": [ { "base64_limit": 4.5, @@ -170,7 +170,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 657, + "line_number": 656, "type": "Hex High Entropy String" } ] diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index 06a4e783..a34d9057 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -11,16 +11,6 @@ from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.utils.flash import formatted_flash as flash -@portfolios_bp.route("/portfolios") -def portfolios(): - portfolios = Portfolios.for_user(g.current_user) - - if portfolios: - return render_template("portfolios/index.html", page=5, portfolios=portfolios) - else: - return render_template("portfolios/blank_slate.html") - - @portfolios_bp.route("/portfolios/new") def new_portfolio(): form = PortfolioCreationForm() diff --git a/templates/portfolios/blank_slate.html b/templates/portfolios/blank_slate.html deleted file mode 100644 index 62c59aa5..00000000 --- a/templates/portfolios/blank_slate.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} - -{% from "components/empty_state.html" import EmptyState %} -{% from "components/tooltip.html" import Tooltip %} - -{% block content %} - {{ - EmptyState( - action_href="#", - action_label=("portfolios.index.empty.start_button" | translate), - icon="cloud", - message=("portfolios.index.empty.title" | translate), - ) - }} -{% endblock %} diff --git a/tests/routes/portfolios/test_index.py b/tests/routes/portfolios/test_index.py index ef368c3d..dbee544a 100644 --- a/tests/routes/portfolios/test_index.py +++ b/tests/routes/portfolios/test_index.py @@ -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/test_access.py b/tests/test_access.py index 24948ec2..668d66d9 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -26,7 +26,6 @@ _NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [ "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 "task_orders.get_started", # all users can start a new TO "users.update_user", # available to all users "users.user", # available to all users 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() From 8e25adb1c3852a9f93319c224bfaa3127bb142b1 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 15:03:14 -0500 Subject: [PATCH 035/111] Update EmptyState macro to align with new designs --- styles/components/_empty_state.scss | 30 +++++++---------- templates/applications/index.html | 11 +++---- templates/components/empty_state.html | 32 ++++++++----------- .../reports/application_and_env_spending.html | 12 +++---- templates/task_orders/index.html | 10 +++--- 5 files changed, 40 insertions(+), 55 deletions(-) diff --git a/styles/components/_empty_state.scss b/styles/components/_empty_state.scss index 0e102a4d..12405e05 100644 --- a/styles/components/_empty_state.scss +++ b/styles/components/_empty_state.scss @@ -1,28 +1,20 @@ .empty-state { - text-align: center; - padding: 5rem ($gap * 2) 2rem; - display: flex; - flex-direction: column; - align-items: center; + padding: $gap * 3; max-width: 100%; + background-color: $color-gray-lightest; + margin-top: $gap * 5; - > .icon { - @include icon-size(50); - @include icon-color($color-gray-light); + hr { + margin-left: -$gap * 3; + margin-right: -$gap * 3; } - &__message { - font-weight: $font-bold; - } + &__footer { + text-align: center; - &__sub-message { - @include h4; - - color: $color-gray; - max-width: 100%; - - @include media($large-screen) { - @include h3; + a.usa-button { + width: 60%; + display: inline-block; } } } diff --git a/templates/applications/index.html b/templates/applications/index.html index bc7ae431..2d3e7d42 100644 --- a/templates/applications/index.html +++ b/templates/applications/index.html @@ -24,12 +24,11 @@ {% 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="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", + button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id), + view_only_text="Contact your portfolio administrator to add an application." ) }} {% else %} diff --git a/templates/components/empty_state.html b/templates/components/empty_state.html index a9d9ff5e..9989e4f8 100644 --- a/templates/components/empty_state.html +++ b/templates/components/empty_state.html @@ -1,20 +1,14 @@ -{% from "components/icon.html" import Icon %} - -{% macro EmptyState(message, action_label, action_href, icon=None, sub_message=None, add_perms=True) -%} -
-

{{ message }}

- - {% if icon %} - {{ Icon(icon) }} - {% endif %} - - {% if sub_message %} -

{{ sub_message }}

- {% endif %} - - {% if add_perms and (action_href and action_label) %} - {{ action_label }} - {% endif %} - +{% macro EmptyState(header, message, button_text, button_link, view_only_text, user_can_create=True) %} +
+

{{ header }}

+

{{ message }}

+
+
-{%- endmacro %} +{% endmacro %} diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index e43ebf0c..8accd0e6 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -15,13 +15,13 @@ %} {{ EmptyState( - ('portfolios.reports.empty_state.message' | translate), - action_label= ('portfolios.reports.empty_state.action_label' | translate) 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='chart', - sub_message=message, - add_perms=can_create_applications + header='portfolios.reports.empty_state.message' | translate, + message=message, + button_text="Add a new application", + button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id), + view_only_text="Contact your portfolio administrator to create an application." ) }} + {% else %} From 5ebcaa3950d5d285c0cecd3fbd377e26f207eb78 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 15:03:55 -0500 Subject: [PATCH 036/111] Add sticky CTA to applications index page and update copy in TO index sticky CTA --- styles/components/_portfolio_layout.scss | 2 ++ templates/applications/index.html | 18 +++++++----------- templates/task_orders/index.html | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 0e018809..0da98f17 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -383,6 +383,8 @@ } .portfolio-applications { + margin-top: $gap * 5; + &__header { &--title { @include subheading; diff --git a/templates/applications/index.html b/templates/applications/index.html index 2d3e7d42..ce6b6fde 100644 --- a/templates/applications/index.html +++ b/templates/applications/index.html @@ -1,5 +1,6 @@ {% from "components/icon.html" import Icon %} {% from "components/empty_state.html" import EmptyState %} +{% from "components/sticky_cta.html" import StickyCTA %} {% extends "portfolios/base.html" %} @@ -7,19 +8,14 @@ {% block portfolio_content %} +{% call StickyCTA(text="Applications") %} + {% if can_create_applications and portfolio.applications %} + Create Application + {% endif %} +{% endcall %} +
{% include "fragments/flash.html" %} - {% if not portfolio.applications %} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index dae2715f..c1e78c81 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -93,9 +93,9 @@ {% endmacro %} -{% call StickyCTA(text="Funding") %} - {% if user_can(permissions.CREATE_TASK_ORDER) %} - Start a new task order +{% call StickyCTA(text="Task Orders") %} + {% if user_can(permissions.CREATE_TASK_ORDER) and task_orders %} + Add New Task Order {% endif %} {% endcall %} From 31d9f3461578054d6720457427e99422226f7218 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 15:07:58 -0500 Subject: [PATCH 037/111] Add in check for edit perms before showing add/create button in empty state macro --- templates/applications/index.html | 3 ++- templates/portfolios/reports/application_and_env_spending.html | 3 ++- templates/task_orders/index.html | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/templates/applications/index.html b/templates/applications/index.html index ce6b6fde..028eb6b7 100644 --- a/templates/applications/index.html +++ b/templates/applications/index.html @@ -24,7 +24,8 @@ 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", button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id), - view_only_text="Contact your portfolio administrator to add an application." + view_only_text="Contact your portfolio administrator to add an application.", + user_can_create=can_create_applications, ) }} {% else %} diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index 8accd0e6..7edfb3f5 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -19,7 +19,8 @@ message=message, button_text="Add a new application", button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id), - view_only_text="Contact your portfolio administrator to create an application." + view_only_text="Contact your portfolio administrator to create an application.", + user_can_create=can_create_applications, ) }} {% else %} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index c1e78c81..2bd699cb 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -111,7 +111,8 @@ 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_link=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id), button_text="Add Task Order", - view_only_text="Contact your portfolio administrator to add a Task Order." + view_only_text="Contact your portfolio administrator to add a Task Order.", + user_can_create=user_can(permissions.CREATE_TASK_ORDER), ) }} {% endif %}
From 6dabd0bf353b04a58835e9da016fd18a83003e96 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 15:20:50 -0500 Subject: [PATCH 038/111] Move copy into translations file --- templates/applications/index.html | 14 ++++++++------ .../reports/application_and_env_spending.html | 4 ++-- templates/task_orders/index.html | 14 ++++++++------ translations.yaml | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/templates/applications/index.html b/templates/applications/index.html index 028eb6b7..4ab303e4 100644 --- a/templates/applications/index.html +++ b/templates/applications/index.html @@ -8,9 +8,11 @@ {% block portfolio_content %} -{% call StickyCTA(text="Applications") %} +{% call StickyCTA(text="common.applications"|translate) %} {% if can_create_applications and portfolio.applications %} - Create Application + + {{ "portfolios.applications.create_button"|translate }} + {% endif %} {% endcall %} @@ -20,11 +22,11 @@ {% if not portfolio.applications %} {{ EmptyState( - 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", + 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="Contact your portfolio administrator to add an application.", + view_only_text="portfolios.applications.empty_state.view_only_text"|translate, user_can_create=can_create_applications, ) }} diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index 7edfb3f5..0af72112 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -17,9 +17,9 @@ {{ EmptyState( header='portfolios.reports.empty_state.message' | translate, message=message, - button_text="Add a new application", + 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="Contact your portfolio administrator to create an application.", + view_only_text="portfolios.applications.empty_state.view_only_text"|translate, user_can_create=can_create_applications, ) }} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index 2bd699cb..b737f3b9 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -93,9 +93,11 @@ {% endmacro %} -{% call StickyCTA(text="Task Orders") %} +{% call StickyCTA(text="common.task_orders"|translate) %} {% if user_can(permissions.CREATE_TASK_ORDER) and task_orders %} - Add New Task Order + + {{ "task_orders.add_new_button" | translate }} + {% endif %} {% endcall %} @@ -107,11 +109,11 @@ {{ TaskOrderList(task_orders) }} {% else %} {{ EmptyState( - 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.", + 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="Add Task Order", - view_only_text="Contact your portfolio administrator to add a Task Order.", + 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/translations.yaml b/translations.yaml index 387aecec..d63b1a5e 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: @@ -308,6 +310,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 +463,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 +516,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: From 20c7e943c8c1fb31e070196a4de35435a3d010f8 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 4 Dec 2019 06:14:19 -0500 Subject: [PATCH 039/111] Compose REDIS_URI from component parts. This updates the configuration handling for the Redis connection string. The motivation is so that the Redis password can be managed separately via Azure Key Vault and eventually be rotated independently of the rest of the connection URI. This also tweaks the method we use to build the DATABASE_URI and removes some stale config from the CI config file. --- .circleci/config.yml | 4 ++-- atst/app.py | 26 ++++++++++++++----------- config/base.ini | 5 ++++- config/ci.ini | 8 +++----- deploy/azure/atst-envvars-configmap.yml | 4 +++- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 082ff825..8898a30d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ commands: default: atat_test container_env: type: string - default: -e PGHOST=postgres -e REDIS_URI=redis://redis:6379 + default: -e PGHOST=postgres -e REDIS_HOST=redis:6379 steps: - run: name: Set up temporary docker network @@ -172,7 +172,7 @@ jobs: command: | docker run \ -e PGHOST=postgres \ - -e REDIS_URI=redis://redis:6379 \ + -e REDIS_HOST=redis:6379 \ --network atat \ atat:builder \ /bin/sh -c "pipenv install --dev && /bin/sh script/cibuild" diff --git a/atst/app.py b/atst/app.py index 39eab6ec..1b60f64c 100644 --- a/atst/app.py +++ b/atst/app.py @@ -223,20 +223,24 @@ def make_config(direct_config=None): config.read_dict({"default": direct_config}) # Assemble DATABASE_URI value - database_uri = ( - "postgres://" - + config.get("default", "PGUSER") - + ":" - + config.get("default", "PGPASSWORD") - + "@" - + config.get("default", "PGHOST") - + ":" - + config.get("default", "PGPORT") - + "/" - + config.get("default", "PGDATABASE") + database_uri = "postgres://{}:{}@{}:{}/{}".format( # pragma: allowlist secret + config.get("default", "PGUSER"), + config.get("default", "PGPASSWORD"), + config.get("default", "PGHOST"), + config.get("default", "PGPORT"), + config.get("default", "PGDATABASE"), ) config.set("default", "DATABASE_URI", database_uri) + # Assemble REDIS_URI value + redis_uri = "redis{}://{}:{}@{}".format( # pragma: allowlist secret + ("s" if config["default"].getboolean("REDIS_TLS") else ""), + (config.get("default", "REDIS_USER") or ""), + (config.get("default", "REDIS_PASSWORD") or ""), + config.get("default", "REDIS_HOST"), + ) + config.set("default", "REDIS_URI", redis_uri) + return map_config(config) diff --git a/config/base.ini b/config/base.ini index 257059e4..ade3abe1 100644 --- a/config/base.ini +++ b/config/base.ini @@ -24,7 +24,10 @@ PGSSLMODE = prefer PGSSLROOTCERT PGUSER = postgres PORT=8000 -REDIS_URI = redis://localhost:6379 +REDIS_HOST=localhost:6379 +REDIS_PASSWORD +REDIS_TLS=False +REDIS_USER SECRET_KEY = change_me_into_something_secret SERVER_NAME SESSION_COOKIE_NAME=atat diff --git a/config/ci.ini b/config/ci.ini index 34682fcc..0a6af8c2 100644 --- a/config/ci.ini +++ b/config/ci.ini @@ -1,8 +1,6 @@ [default] -DEBUG = true -PGHOST = postgreshost -PGDATABASE = atat_test -REDIS_URI = redis://redishost:6379 CRL_STORAGE_CONTAINER = tests/fixtures/crl -WTF_CSRF_ENABLED = false CSP=mock-test +DEBUG = true +PGDATABASE = atat_test +WTF_CSRF_ENABLED = false diff --git a/deploy/azure/atst-envvars-configmap.yml b/deploy/azure/atst-envvars-configmap.yml index 8dd80237..d6bd60ef 100644 --- a/deploy/azure/atst-envvars-configmap.yml +++ b/deploy/azure/atst-envvars-configmap.yml @@ -7,14 +7,16 @@ metadata: data: ASSETS_URL: https://atat-cdn.azureedge.net/ BLOB_STORAGE_URL: https://atat.blob.core.windows.net/ - CELERY_DEFAULT_QUEUE: celery-master CDN_ORIGIN: https://azure.atat.code.mil + CELERY_DEFAULT_QUEUE: celery-master CSP: azure FLASK_ENV: master LOG_JSON: "true" OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt + REDIS_HOST: atat.redis.cache.windows.net:6380 + REDIS_TLS: "true" STATIC_URL: https://atat-cdn.azureedge.net/static/ TZ: UTC UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini From 4ba652868de9efc5e7dcc393a9d78cd3d6ebb084 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 4 Dec 2019 14:15:51 -0500 Subject: [PATCH 040/111] Fix Redis config in CI. Missed a few instances of the old connection string parameter in the CircleCI config. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8898a30d..1bb49bb3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -195,7 +195,7 @@ jobs: docker run -d \ -e DISABLE_CRL_CHECK=true \ -e PGHOST=postgres \ - -e REDIS_URI=redis://redis:6379 \ + -e REDIS_HOST=redis:6379 \ -p 8000:8000 \ --network atat \ --name test-atat \ @@ -253,7 +253,7 @@ jobs: command: | docker run \ -e PGHOST=postgres \ - -e REDIS_URI=redis://redis:6379 \ + -e REDIS_HOST=redis:6379 \ --network atat \ atat:builder \ /bin/sh -c "pipenv install --dev && /bin/sh script/sync-crls && pipenv run pytest --no-cov tests/check_crl_parse.py" From 0851e425729252b143c948a33433608da721bd9e Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 5 Dec 2019 10:57:03 -0500 Subject: [PATCH 041/111] Add script for running Ghost Inspector tests locally. Eventually, this should replace the CircleCI config for running the integration tests to avoid duplication. In the interest of time so that I don't have to debug broken builds, I'm only adding it as a utility script. --- script/integration_tests | 94 ++++++++++++++++++++++++++++++++++++++++ uitests/README.md | 28 ++++++------ 2 files changed, 108 insertions(+), 14 deletions(-) create mode 100755 script/integration_tests diff --git a/script/integration_tests b/script/integration_tests new file mode 100755 index 00000000..e84b103a --- /dev/null +++ b/script/integration_tests @@ -0,0 +1,94 @@ +#!/bin/bash + +# script/integration_tests: Run the integration tests via docker. +set -e + +if [ -z "${CONTAINER_TIMEOUT+is_set}" ]; then + CONTAINER_TIMEOUT=200 +fi + +# Expected settings. Script will error if these are not provided. +SETTINGS=( + CONTAINER_IMAGE + NGROK_TOKEN + GI_API_KEY + GI_SUITE +) + +# Loop all expected settings. Track ones that are missing. If any +# are missing, exit. +MISSING_SETTINGS=() +for envvar in "${SETTINGS[@]}"; do + if [ -z "${!envvar}" ]; then + MISSING_SETTINGS+=(${envvar}) + fi +done + +if [[ ${#MISSING_SETTINGS[@]} > 0 ]]; then + >&2 echo "The following variables need to be set:" + for missing in "${MISSING_SETTINGS[@]}"; do + >&2 echo $missing + done + exit 1 +fi + +# Remove any existing container and network instances +docker container stop redis postgres test-atat || true && docker container rm redis postgres test-atat || true +docker network rm atat || true + +# Create network +docker network create atat + +# Start Redis and Postgres +docker run -d --network atat --link redis:redis -p 6379:6379 --name redis circleci/redis:4-alpine3.8 +docker run -d --network atat --link postgres:postgres -p 5432:5432 --name postgres circleci/postgres:10-alpine-ram +# Wait for datastores to be available +sleep 3 + +# Create database and run migrations +docker exec postgres createdb -U postgres atat +docker run --network atat -e PGDATABASE=atat -e PGHOST=postgres -e REDIS_HOST=redis:6379 $CONTAINER_IMAGE .venv/bin/python .venv/bin/alembic upgrade head +docker run --network atat -e PGDATABASE=atat -e PGHOST=postgres -e REDIS_HOST=redis:6379 $CONTAINER_IMAGE .venv/bin/python script/seed_roles.py + +# Start application container +docker run -d \ +-e DISABLE_CRL_CHECK=true \ +-e PGHOST=postgres \ +-e REDIS_HOST=redis:6379 \ +-p 8000:8000 \ +--network atat \ +--name test-atat \ +$CONTAINER_IMAGE \ +/bin/sh -c " + echo CLOUD_PROVIDER=mock > .env &&\ + yarn build &&\ + uwsgi \ + --callable app \ + --module app \ + --plugin python3 \ + --virtualenv /install/.venv \ + --http-socket :8000 +" + +# Use curl to wait for application container to become available +docker pull curlimages/curl:latest +docker run --network atat \ + curlimages/curl:latest \ + curl --connect-timeout 3 \ + --max-time 5 \ + --retry $CONTAINER_TIMEOUT \ + --retry-connrefused \ + --retry-delay 1 \ + --retry-max-time $CONTAINER_TIMEOUT \ + test-atat:8000 + +# Run Ghost Inspector tests +docker pull ghostinspector/test-runner-standalone:latest +docker run \ + -e NGROK_TOKEN=$NGROK_TOKEN \ + -e GI_API_KEY=$GI_API_KEY \ + -e GI_SUITE=$GI_SUITE \ + -e GI_PARAMS_JSON='{}' \ + -e APP_PORT="test-atat:8000" \ + --network atat \ + ghostinspector/test-runner-standalone:latest diff --git a/uitests/README.md b/uitests/README.md index 0f3a940b..aa1dfd93 100644 --- a/uitests/README.md +++ b/uitests/README.md @@ -30,7 +30,7 @@ QA lead monitors all failures on the CI suite and determines that such a change the Ghost Inspector UI. 3. If the (potentially) failing test will need to be reworked to account for functional changes, the test is moved from the CI -suite to the "Holding" suite until the PR is merged. Then the test can be edited and returned to the CI suite. +suite to the "Holding" suite until the PR is merged. Then the test can be edited and returned to the CI suite. ## Running Ghost Inspector tests locally @@ -38,26 +38,26 @@ To run the Ghost Inspector tests against a local instance of AT-AT, you will need the following: - [docker](https://docs.docker.com/v17.12/install/) -- [circleci CLI tool](https://circleci.com/docs/2.0/local-cli/#installation) -- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/) +- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/): NGROK_TOKEN, GI_API_KEY, GI_SUITE -The version of our CircleCI config (2.1) is incompatible with the -`circleci` tool. First run: +First you will need to build a copy of the container: ``` -circleci config process .circleci/config.yml > local-ci.yml +docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:builder --target builder ``` -Then run the job: +This builds the first stage of the docker container, which is the one we need to run integration tests. You can tag the container whatever you want; in the example we've tagged it "atat:builder". + +Then you can run the integration tests script. You will need four environment variables set: the three mentioned previously and CONTAINER_IMAGE. You can either export them or set them inline in the command you use to run the script. In the example we'll set them inline: ``` -circleci local execute -e GI_SUITE= -e GI_API_KEY= -e NGROK_TOKEN= --job integration-tests -c local-ci.yml +NGROK_TOKEN= GI_API_KEY= GI_SUITE= CONTAINER_IMAGE=atat:builder ./script/integration_tests ``` -If the job fails and you want to re-run it, you may receive errors -about running docker containers or the network already existing. -Some version of the following should reset your local docker state: +### Troubleshooting -``` -docker container stop redis postgres test-atat; docker container rm redis postgres test-atat ; docker network rm atat -``` +- If you get errors regarding ports being in use, make sure you don't have instances of the Flask app, Postgres, or Redis running locally using those ports. +- If the curl command used to wait for the application container times out and fails, you can increase the timeout by setting a CONTAINER_TIMEOUT environment variable. It defaults to 200 in the script. +- The curl command will print errors until it successfully connects to the application container. These are normal and expected. When it finally connects, it will print the ATAT home page HTML to STDOUT. +- You may see errors like "No such container". The script attempts to clean up any previous incarnations of the containers before it starts, and it may print errors when it doesn't find them. This is fine. +- The script is, for the most part, a series of docker commands, so try running the commands individually and debugging that way. From 7e86825988b5fa878f807f4b5f4b4bfde355974e Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Fri, 6 Dec 2019 09:53:33 -0500 Subject: [PATCH 042/111] New tests created this week --- uitests/Delete_Portfolio_Member.html | 968 ++++++++++++ uitests/Report_Summary.html | 1296 ++++++++++++++++ uitests/Reports_-_Follow_Add_App_Button.html | 1409 +++++++++++++++++ uitests/Reports_-_Follow_TO_link.html | 1439 ++++++++++++++++++ uitests/Resend_App_Member_Invite.html | 1322 ++++++++++++++++ 5 files changed, 6434 insertions(+) create mode 100644 uitests/Delete_Portfolio_Member.html create mode 100644 uitests/Report_Summary.html create mode 100644 uitests/Reports_-_Follow_Add_App_Button.html create mode 100644 uitests/Reports_-_Follow_TO_link.html create mode 100644 uitests/Resend_App_Member_Invite.html diff --git a/uitests/Delete_Portfolio_Member.html b/uitests/Delete_Portfolio_Member.html new file mode 100644 index 00000000..4557fcfa --- /dev/null +++ b/uitests/Delete_Portfolio_Member.html @@ -0,0 +1,968 @@ + + + + + + +Delete Portfolio Member + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Delete Portfolio Member
waitForPageToLoad
open/login-dev?username=brandon
waitForPageToLoad
waitForElementPresentcss=a[href="/user"] > .topbar__link-label
assertTextcss=a[href="/user"] > .topbar__link-label*Brandon Buchannan*
waitForPageToLoad
waitForElementPresentcss=a[href="/logout"] > .topbar__link-label
clickcss=a[href="/logout"] > .topbar__link-label
waitForPageToLoad
waitForElementPresentcss=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading
assertTextcss=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading*Logged out*
waitForPageToLoad
open/login-dev
waitForPageToLoad
waitForElementPresentcss=.about-cloud > h1
assertTextcss=.about-cloud > h1About Cloud Services
waitForPageToLoad
waitForElementPresentcss=a[href="/portfolios/new"]
clickcss=a[href="/portfolios/new"]
waitForPageToLoad
waitForElementPresentcss=main.usa-section > h1
assertTextcss=main.usa-section > h1*New Portfolio Form*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameTatooine Energy Maintenance Systems
waitForPageToLoad
waitForElementPresentcss=#defense_component
typecss=#defense_componentDefense Logistics Agency
waitForPageToLoad
waitForElementPresentcss=#description
typecss=#descriptionMaintenance and monitoring of the energy grid
waitForPageToLoad
waitForElementPresentcss=#app_migration > li:nth-of-type(3) > label
clickcss=#app_migration > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#native_apps > li:nth-of-type(1) > label
clickcss=#native_apps > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#team_experience > li:nth-of-type(5) > label
clickcss=#team_experience > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
assertTextcss=input[type="submit"]Save
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*You don't have any Applications yet*
waitForPageToLoad
waitForElementPresentcss=.icon.icon--cog > svg
clickcss=.icon.icon--cog > svg
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.panel__content > p:nth-of-type(2)
assertElementPresentcss=.panel__content > p:nth-of-type(2)
waitForPageToLoad
waitForElementPresentcss=td.name
assertElementPresentcss=td.name
waitForPageToLoad
waitForElementPresentcss=button.usa-button.usa-button-primary.usa-button-big
assertTextcss=button.usa-button.usa-button-primary.usa-button-bigSave
waitForPageToLoad
waitForElementPresentcss=button.usa-button.usa-button-primary
assertTextcss=button.usa-button.usa-button-primary*Update*
waitForPageToLoad
waitForElementPresentcss=input.usa-button.usa-button-primary
assertTextcss=input.usa-button.usa-button-primarySave
waitForPageToLoad
waitForElementPresentcss=a.icon-link.modal-link
clickcss=a.icon-link.modal-link
waitForPageToLoad
waitForElementPresentcss=#add-port-mem > div > div:nth-of-type(1) > h1
assertTextcss=#add-port-mem > div > div:nth-of-type(1) > h1*Invite new portfolio member*
waitForPageToLoad
waitForElementPresentcss=#user_data-first_name
typecss=#user_data-first_nameBrandon
waitForPageToLoad
waitForElementPresentcss=#user_data-last_name
typecss=#user_data-last_nameBuchannan
waitForPageToLoad
waitForElementPresentcss=#user_data-email
typecss=#user_data-emailjay+brandon@promptworks.com
waitForPageToLoad
waitForElementPresentcss=#user_data-dod_id
typecss=#user_data-dod_id3456789012
waitForPageToLoad
waitForElementPresentcss=input[type="button"]
clickcss=input[type="button"]
waitForPageToLoad
waitForElementPresentcss=#add-port-mem > div > div:nth-of-type(2) > h1
assertTextcss=#add-port-mem > div > div:nth-of-type(2) > h1*Assign member permissions*
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_app_mgmt
clickcss=#permission_sets-perms_app_mgmt
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_app_mgmt > option:nth-of-type(1)
clickcss=#permission_sets-perms_app_mgmt > option:nth-of-type(1)
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_funding
clickcss=#permission_sets-perms_funding
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_funding > option:nth-of-type(1)
clickcss=#permission_sets-perms_funding > option:nth-of-type(1)
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_reporting
clickcss=#permission_sets-perms_reporting
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_reporting > option:nth-of-type(1)
clickcss=#permission_sets-perms_reporting > option:nth-of-type(1)
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_portfolio_mgmt
typecss=#permission_sets-perms_portfolio_mgmtedit_portfolio_admin
waitForPageToLoad
waitForElementPresentcss=#permission_sets-perms_portfolio_mgmt > option:nth-of-type(2)
clickcss=#permission_sets-perms_portfolio_mgmt > option:nth-of-type(2)
waitForPageToLoad
waitForElementPresentcss=input[type="submit"].action-group__action
clickcss=input[type="submit"].action-group__action
waitForPageToLoad
waitForElementPresentcss=table.atat-table > tbody > tr:nth-of-type(2) > td.name
assertElementPresentcss=table.atat-table > tbody > tr:nth-of-type(2) > td.name
waitForPageToLoad
waitForElementPresentcss=.usa-alert-body > p:nth-of-type(2)
assertTextcss=.usa-alert-body > p:nth-of-type(2)*You have successfully invited Brandon Buchannan to the portfolio.*
waitForPageToLoad
waitForElementPresentcss=a.usa-button.button-danger-outline
clickcss=a.usa-button.button-danger-outline
waitForPageToLoad
waitForElementPresentcss=.responsive-table-wrapper > div:nth-of-type(3) > .modal > .modal__container > .modal__dialog > .modal__body > h1
assertTextcss=.responsive-table-wrapper > div:nth-of-type(3) > .modal > .modal__container > .modal__dialog > .modal__body > h1*Are you sure you want to delete this member?*
waitForPageToLoad
waitForElementPresentcss=form[action] > button.usa-button.usa-button-danger
clickcss=form[action] > button.usa-button.usa-button-danger
waitForPageToLoad
waitForElementPresentcss=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading
assertTextcss=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading*Portfolio member deleted*
waitForPageToLoad
waitForElementPresentcss=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text
assertTextcss=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text*You have successfully deleted Brandon Buchannan from the portfolio.*
+ + \ No newline at end of file diff --git a/uitests/Report_Summary.html b/uitests/Report_Summary.html new file mode 100644 index 00000000..6e92127c --- /dev/null +++ b/uitests/Report_Summary.html @@ -0,0 +1,1296 @@ + + + + + + +Report Summary + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Report Summary
waitForPageToLoad
open/login-dev
waitForPageToLoad
waitForElementPresentcss=.about-cloud > h1
assertTextcss=.about-cloud > h1About Cloud Services
waitForPageToLoad
waitForElementPresentcss=a[href="/portfolios/new"]
clickcss=a[href="/portfolios/new"]
waitForPageToLoad
waitForElementPresentcss=main.usa-section > h1
assertTextcss=main.usa-section > h1*New Portfolio Form*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameTatooine Energy Maintenance Systems
waitForPageToLoad
waitForElementPresentcss=#defense_component
typecss=#defense_componentDefense Logistics Agency
waitForPageToLoad
waitForElementPresentcss=#description
typecss=#descriptionMaintenance and monitoring of the energy grid
waitForPageToLoad
waitForElementPresentcss=#app_migration > li:nth-of-type(3) > label
clickcss=#app_migration > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#native_apps > li:nth-of-type(1) > label
clickcss=#native_apps > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#team_experience > li:nth-of-type(5) > label
clickcss=#team_experience > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
assertTextcss=input[type="submit"]Save
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*You don't have any Applications yet*
waitForPageToLoad
waitForElementPresentcss=.icon--funding
clickcss=.icon--funding
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*Add approved task orders*
waitForPageToLoad
waitForElementPresentcss=.usa-button.usa-button-primary
clickcss=.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 1 of 5)*
waitForPageToLoad
waitForElementPresentcss=.upload-button
clickcss=.upload-button
waitForPageToLoad
waitForElementPresentcss=#pdf
typecss=#pdfhttps://ghostinspector-prod.s3.amazonaws.com/uploads/726e1c95-2d16-4793-85d8-b4d3ae9cd35d.pdf
waitForPageToLoad
assertElementNotPresentcss=input[type="submit"][disabled="disabled"]
waitForPageToLoad
waitForElementPresentcss=a[href="#"].uploaded-file__remove
assertTextcss=a[href="#"].uploaded-file__remove*Remove*
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 2 of 5)*
waitForPageToLoad
waitForElementPresentcss=#number
typecss=#number1234567890123
waitForPageToLoad
waitForElementPresentcss=svg.svg-inline--fa.fa-check-circle > path
assertElementPresentcss=svg.svg-inline--fa.fa-check-circle > path
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 3 of 5)*
waitForPageToLoad
waitForElementPresentcss=#clins-0-number
typecss=#clins-0-number0002
waitForPageToLoad
waitForElementPresentcss=.card__title > .h4
assertTextcss=.card__title > .h4*CLIN 0002*
waitForPageToLoad
waitForElementPresentcss=#clins-0-jedi_clin_type
typecss=#clins-0-jedi_clin_typeJEDI_CLIN_2
waitForPageToLoad
waitForElementPresentcss=#percent-obligated
assertTextcss=#percent-obligated*0%*
waitForPageToLoad
waitForElementPresentcss=#clins-0-total_amount
typecss=#clins-0-total_amount$800,000
waitForPageToLoad
waitForElementPresentcss=#clins-0-obligated_amount
typecss=#clins-0-obligated_amount$100,000
waitForPageToLoad
waitForElementPresentcss=#percent-obligated
assertTextcss=#percent-obligated*13%*
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
clickcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]10
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]01
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]2019
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]06
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]30
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]2020
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
clickcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > div:nth-of-type(4) > .usa-form-group-date-ok > .icon.icon--ok.icon--green > svg.svg-inline--fa.fa-check-circle.fa-w-16
assertElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > div:nth-of-type(4) > .usa-form-group-date-ok > .icon.icon--ok.icon--green > svg.svg-inline--fa.fa-check-circle.fa-w-16
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 4 of 5)*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(2)
assertTextcss=.totals-box > .h3:nth-of-type(2)*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(4)
assertTextcss=.totals-box > .h3:nth-of-type(4)*$800,000.00*
waitForPageToLoad
waitForElementPresentcss=.col.task-order__details > div:nth-of-type(2)
assertTextcss=.col.task-order__details > div:nth-of-type(2)*1234567890123*
waitForPageToLoad
waitForElementPresentcss=table.fixed-table-wrapper > thead > tr > th.task-order__amount:nth-of-type(6)
assertTextcss=table.fixed-table-wrapper > thead > tr > th.task-order__amount:nth-of-type(6)*Amount Obligated*
waitForPageToLoad
waitForElementPresentcss=a.usa-button.usa-button-primary
clickcss=a.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.h2
assertTextcss=.h2*Confirm Signature*
waitForPageToLoad
waitForElementPresentcss=fieldset.usa-input__choices > legend > label
clickcss=fieldset.usa-input__choices > legend > label
waitForPageToLoad
captureEntirePageScreenshot
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.usa-alert-text
assertTextcss=.usa-alert-text*Your task order form for Tatooine Energy Maintenance Systems has been submitted.*
waitForPageToLoad
waitForElementPresentcss=.icon.icon--chart-pie > svg
clickcss=.icon.icon--chart-pie > svg
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Reports*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value*October 01, 2019 + - + June 30, 2020*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text*Days Remaining*
+ + \ No newline at end of file diff --git a/uitests/Reports_-_Follow_Add_App_Button.html b/uitests/Reports_-_Follow_Add_App_Button.html new file mode 100644 index 00000000..e556ef71 --- /dev/null +++ b/uitests/Reports_-_Follow_Add_App_Button.html @@ -0,0 +1,1409 @@ + + + + + + +Reports - Follow Add App Button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Reports - Follow Add App Button
waitForPageToLoad
open/login-dev
waitForPageToLoad
waitForElementPresentcss=.about-cloud > h1
assertTextcss=.about-cloud > h1About Cloud Services
waitForPageToLoad
waitForElementPresentcss=a[href="/portfolios/new"]
clickcss=a[href="/portfolios/new"]
waitForPageToLoad
waitForElementPresentcss=main.usa-section > h1
assertTextcss=main.usa-section > h1*New Portfolio Form*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameTatooine Energy Maintenance Systems
waitForPageToLoad
waitForElementPresentcss=#defense_component
typecss=#defense_componentDefense Logistics Agency
waitForPageToLoad
waitForElementPresentcss=#description
typecss=#descriptionMaintenance and monitoring of the energy grid
waitForPageToLoad
waitForElementPresentcss=#app_migration > li:nth-of-type(3) > label
clickcss=#app_migration > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#native_apps > li:nth-of-type(1) > label
clickcss=#native_apps > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#team_experience > li:nth-of-type(5) > label
clickcss=#team_experience > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
assertTextcss=input[type="submit"]Save
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*You don't have any Applications yet*
waitForPageToLoad
waitForElementPresentcss=.icon--funding
clickcss=.icon--funding
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*Add approved task orders*
waitForPageToLoad
waitForElementPresentcss=.usa-button.usa-button-primary
clickcss=.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 1 of 5)*
waitForPageToLoad
waitForElementPresentcss=.upload-button
clickcss=.upload-button
waitForPageToLoad
waitForElementPresentcss=#pdf
typecss=#pdfhttps://ghostinspector-prod.s3.amazonaws.com/uploads/726e1c95-2d16-4793-85d8-b4d3ae9cd35d.pdf
waitForPageToLoad
assertElementNotPresentcss=input[type="submit"][disabled="disabled"]
waitForPageToLoad
waitForElementPresentcss=a[href="#"].uploaded-file__remove
assertTextcss=a[href="#"].uploaded-file__remove*Remove*
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 2 of 5)*
waitForPageToLoad
waitForElementPresentcss=#number
typecss=#number1234567890123
waitForPageToLoad
waitForElementPresentcss=svg.svg-inline--fa.fa-check-circle > path
assertElementPresentcss=svg.svg-inline--fa.fa-check-circle > path
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 3 of 5)*
waitForPageToLoad
waitForElementPresentcss=#clins-0-number
typecss=#clins-0-number0002
waitForPageToLoad
waitForElementPresentcss=.card__title > .h4
assertTextcss=.card__title > .h4*CLIN 0002*
waitForPageToLoad
waitForElementPresentcss=#clins-0-jedi_clin_type
typecss=#clins-0-jedi_clin_typeJEDI_CLIN_2
waitForPageToLoad
waitForElementPresentcss=#percent-obligated
assertTextcss=#percent-obligated*0%*
waitForPageToLoad
waitForElementPresentcss=#clins-0-total_amount
typecss=#clins-0-total_amount$800,000
waitForPageToLoad
waitForElementPresentcss=#clins-0-obligated_amount
typecss=#clins-0-obligated_amount$100,000
waitForPageToLoad
waitForElementPresentcss=#percent-obligated
assertTextcss=#percent-obligated*13%*
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
clickcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]10
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]01
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]2019
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]06
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]30
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]2020
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
clickcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > div:nth-of-type(4) > .usa-form-group-date-ok > .icon.icon--ok.icon--green > svg.svg-inline--fa.fa-check-circle.fa-w-16
assertElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > div:nth-of-type(4) > .usa-form-group-date-ok > .icon.icon--ok.icon--green > svg.svg-inline--fa.fa-check-circle.fa-w-16
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 4 of 5)*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(2)
assertTextcss=.totals-box > .h3:nth-of-type(2)*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(4)
assertTextcss=.totals-box > .h3:nth-of-type(4)*$800,000.00*
waitForPageToLoad
waitForElementPresentcss=.col.task-order__details > div:nth-of-type(2)
assertTextcss=.col.task-order__details > div:nth-of-type(2)*1234567890123*
waitForPageToLoad
waitForElementPresentcss=table.fixed-table-wrapper > thead > tr > th.task-order__amount:nth-of-type(6)
assertTextcss=table.fixed-table-wrapper > thead > tr > th.task-order__amount:nth-of-type(6)*Amount Obligated*
waitForPageToLoad
waitForElementPresentcss=a.usa-button.usa-button-primary
clickcss=a.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.h2
assertTextcss=.h2*Confirm Signature*
waitForPageToLoad
waitForElementPresentcss=fieldset.usa-input__choices > legend > label
clickcss=fieldset.usa-input__choices > legend > label
waitForPageToLoad
captureEntirePageScreenshot
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.usa-alert-text
assertTextcss=.usa-alert-text*Your task order form for Tatooine Energy Maintenance Systems has been submitted.*
waitForPageToLoad
waitForElementPresentcss=.icon.icon--chart-pie > svg
clickcss=.icon.icon--chart-pie > svg
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Reports*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value*October 01, 2019 + - + June 30, 2020*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text*Days Remaining*
waitForPageToLoad
waitForElementPresentcss=div.empty-state__footer > a
clickcss=div.empty-state__footer > a
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Name and Describe New Application*
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-context
assertTextcss=.sticky-cta-context*Step 1 of 3*
+ + \ No newline at end of file diff --git a/uitests/Reports_-_Follow_TO_link.html b/uitests/Reports_-_Follow_TO_link.html new file mode 100644 index 00000000..fa3662db --- /dev/null +++ b/uitests/Reports_-_Follow_TO_link.html @@ -0,0 +1,1439 @@ + + + + + + +Reports - Follow TO link + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Reports - Follow TO link
waitForPageToLoad
open/login-dev
waitForPageToLoad
waitForElementPresentcss=.about-cloud > h1
assertTextcss=.about-cloud > h1About Cloud Services
waitForPageToLoad
waitForElementPresentcss=a[href="/portfolios/new"]
clickcss=a[href="/portfolios/new"]
waitForPageToLoad
waitForElementPresentcss=main.usa-section > h1
assertTextcss=main.usa-section > h1*New Portfolio Form*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameTatooine Energy Maintenance Systems
waitForPageToLoad
waitForElementPresentcss=#defense_component
typecss=#defense_componentDefense Logistics Agency
waitForPageToLoad
waitForElementPresentcss=#description
typecss=#descriptionMaintenance and monitoring of the energy grid
waitForPageToLoad
waitForElementPresentcss=#app_migration > li:nth-of-type(3) > label
clickcss=#app_migration > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#native_apps > li:nth-of-type(1) > label
clickcss=#native_apps > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#team_experience > li:nth-of-type(5) > label
clickcss=#team_experience > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
assertTextcss=input[type="submit"]Save
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*You don't have any Applications yet*
waitForPageToLoad
waitForElementPresentcss=.icon--funding
clickcss=.icon--funding
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*Add approved task orders*
waitForPageToLoad
waitForElementPresentcss=.usa-button.usa-button-primary
clickcss=.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 1 of 5)*
waitForPageToLoad
waitForElementPresentcss=.upload-button
clickcss=.upload-button
waitForPageToLoad
waitForElementPresentcss=#pdf
typecss=#pdfhttps://ghostinspector-prod.s3.amazonaws.com/uploads/726e1c95-2d16-4793-85d8-b4d3ae9cd35d.pdf
waitForPageToLoad
assertElementNotPresentcss=input[type="submit"][disabled="disabled"]
waitForPageToLoad
waitForElementPresentcss=a[href="#"].uploaded-file__remove
assertTextcss=a[href="#"].uploaded-file__remove*Remove*
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 2 of 5)*
waitForPageToLoad
waitForElementPresentcss=#number
typecss=#number1234567890123
waitForPageToLoad
waitForElementPresentcss=svg.svg-inline--fa.fa-check-circle > path
assertElementPresentcss=svg.svg-inline--fa.fa-check-circle > path
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 3 of 5)*
waitForPageToLoad
waitForElementPresentcss=#clins-0-number
typecss=#clins-0-number0002
waitForPageToLoad
waitForElementPresentcss=.card__title > .h4
assertTextcss=.card__title > .h4*CLIN 0002*
waitForPageToLoad
waitForElementPresentcss=#clins-0-jedi_clin_type
typecss=#clins-0-jedi_clin_typeJEDI_CLIN_2
waitForPageToLoad
waitForElementPresentcss=#percent-obligated
assertTextcss=#percent-obligated*0%*
waitForPageToLoad
waitForElementPresentcss=#clins-0-total_amount
typecss=#clins-0-total_amount$800,000
waitForPageToLoad
waitForElementPresentcss=#clins-0-obligated_amount
typecss=#clins-0-obligated_amount$100,000
waitForPageToLoad
waitForElementPresentcss=#percent-obligated
assertTextcss=#percent-obligated*13%*
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
clickcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]10
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]01
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]
typecss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]2019
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]06
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-day > input[name="date-day"]30
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]
typecss=fieldset[name="clins-0-end_date"] > .date-picker-component > .usa-form-group.usa-form-group-year > input[name="date-year"]2020
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
clickcss=fieldset[name="clins-0-start_date"] > .date-picker-component > .usa-form-group.usa-form-group-month > input[name="date-month"]
waitForPageToLoad
waitForElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > div:nth-of-type(4) > .usa-form-group-date-ok > .icon.icon--ok.icon--green > svg.svg-inline--fa.fa-check-circle.fa-w-16
assertElementPresentcss=fieldset[name="clins-0-end_date"] > .date-picker-component > div:nth-of-type(4) > .usa-form-group-date-ok > .icon.icon--ok.icon--green > svg.svg-inline--fa.fa-check-circle.fa-w-16
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Task Order (step 4 of 5)*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(2)
assertTextcss=.totals-box > .h3:nth-of-type(2)*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(4)
assertTextcss=.totals-box > .h3:nth-of-type(4)*$800,000.00*
waitForPageToLoad
waitForElementPresentcss=.col.task-order__details > div:nth-of-type(2)
assertTextcss=.col.task-order__details > div:nth-of-type(2)*1234567890123*
waitForPageToLoad
waitForElementPresentcss=table.fixed-table-wrapper > thead > tr > th.task-order__amount:nth-of-type(6)
assertTextcss=table.fixed-table-wrapper > thead > tr > th.task-order__amount:nth-of-type(6)*Amount Obligated*
waitForPageToLoad
waitForElementPresentcss=a.usa-button.usa-button-primary
clickcss=a.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.h2
assertTextcss=.h2*Confirm Signature*
waitForPageToLoad
waitForElementPresentcss=fieldset.usa-input__choices > legend > label
clickcss=fieldset.usa-input__choices > legend > label
waitForPageToLoad
captureEntirePageScreenshot
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.usa-alert-text
assertTextcss=.usa-alert-text*Your task order form for Tatooine Energy Maintenance Systems has been submitted.*
waitForPageToLoad
waitForElementPresentcss=.icon.icon--chart-pie > svg
clickcss=.icon.icon--chart-pie > svg
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Reports*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(1) > .reporting-summary-item__value*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(2) > .reporting-summary-item__value*October 01, 2019 + - + June 30, 2020*
waitForPageToLoad
waitForElementPresentcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text
assertTextcss=.row > .col.col--grow.reporting-summary-item:nth-of-type(3) > h5.reporting-summary-item__header > .reporting-summary-item__header-text*Days Remaining*
waitForPageToLoad
waitForElementPresentcss=div.jedi-clin-funding__active-task-orders > a
clickcss=div.jedi-clin-funding__active-task-orders > a
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Task order details*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(2)
assertTextcss=.totals-box > .h3:nth-of-type(2)*$100,000.00*
waitForPageToLoad
waitForElementPresentcss=.totals-box > .h3:nth-of-type(4)
assertTextcss=.totals-box > .h3:nth-of-type(4)*$800,000.00*
waitForPageToLoad
waitForElementPresentcss=.col.task-order__details > div:nth-of-type(2)
assertTextcss=.col.task-order__details > div:nth-of-type(2)*1234567890123*
+ + \ No newline at end of file diff --git a/uitests/Resend_App_Member_Invite.html b/uitests/Resend_App_Member_Invite.html new file mode 100644 index 00000000..81dc7e36 --- /dev/null +++ b/uitests/Resend_App_Member_Invite.html @@ -0,0 +1,1322 @@ + + + + + + +Resend App Member Invite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Resend App Member Invite
waitForPageToLoad
open/login-dev?username=brandon
waitForPageToLoad
waitForElementPresentcss=a[href="/user"] > .topbar__link-label
assertTextcss=a[href="/user"] > .topbar__link-label*Brandon Buchannan*
waitForPageToLoad
waitForElementPresentcss=a[href="/logout"] > .topbar__link-label
clickcss=a[href="/logout"] > .topbar__link-label
waitForPageToLoad
waitForElementPresentcss=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading
assertTextcss=.col > .usa-alert.usa-alert-info:nth-of-type(2) > .usa-alert-body > h3.usa-alert-heading*Logged out*
waitForPageToLoad
open/login-dev
waitForPageToLoad
waitForElementPresentcss=.about-cloud > h1
assertTextcss=.about-cloud > h1About Cloud Services
waitForPageToLoad
waitForElementPresentcss=a[href="/portfolios/new"]
clickcss=a[href="/portfolios/new"]
waitForPageToLoad
waitForElementPresentcss=main.usa-section > h1
assertTextcss=main.usa-section > h1*New Portfolio Form*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameTatooine Energy Maintenance Systems
waitForPageToLoad
waitForElementPresentcss=#defense_component
typecss=#defense_componentDefense Logistics Agency
waitForPageToLoad
waitForElementPresentcss=#description
typecss=#descriptionMaintenance and monitoring of the energy grid
waitForPageToLoad
waitForElementPresentcss=#app_migration > li:nth-of-type(3) > label
clickcss=#app_migration > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#native_apps > li:nth-of-type(1) > label
clickcss=#native_apps > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(1) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(4) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(6) > fieldset.usa-input__choices > ul > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
clickcss=#portfolio-create > .usa-input:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(2) > label
waitForPageToLoad
waitForElementPresentcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
clickcss=#portfolio-create > .usa-input.usa-input--success:nth-of-type(7) > fieldset.usa-input__choices > ul > li:nth-of-type(3) > label
waitForPageToLoad
waitForElementPresentcss=#team_experience > li:nth-of-type(5) > label
clickcss=#team_experience > li:nth-of-type(5) > label
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
assertTextcss=input[type="submit"]Save
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.portfolio-header__name > h1
assertTextcss=.portfolio-header__name > h1*Tatooine Energy Maintenance Systems*
waitForPageToLoad
waitForElementPresentcss=.empty-state h3
assertTextcss=.empty-state h3*You don't have any Applications yet*
waitForPageToLoad
waitForElementPresentcss=a.usa-button.usa-button-primary
clickcss=a.usa-button.usa-button-primary
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Name and Describe New Application*
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-context
assertTextcss=.sticky-cta-context*Step 1 of 3*
waitForPageToLoad
waitForElementPresentcss=#name
typecss=#nameStarter Application
waitForPageToLoad
waitForElementPresentcss=#description
typecss=#descriptionAny basic application
waitForPageToLoad
waitForElementPresentcss=button[type="submit"]
clickcss=button[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Environments to Starter Application*
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-context
assertTextcss=.sticky-cta-context*Step 2 of 3*
waitForPageToLoad
waitForElementPresentcss=h3.usa-alert-heading
assertTextcss=h3.usa-alert-heading*Application Saved*
waitForPageToLoad
waitForElementPresentcss=button[type="submit"]
clickcss=button[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-text > h3
assertTextcss=.sticky-cta-text > h3*Add Members to Starter Application*
waitForPageToLoad
waitForElementPresentcss=.sticky-cta-context
assertTextcss=.sticky-cta-context*Step 3 of 3*
waitForPageToLoad
waitForElementPresentcss=h3.usa-alert-heading
assertTextcss=h3.usa-alert-heading*Application environments updated*
waitForPageToLoad
waitForElementPresentcss=.empty-state__message
assertTextcss=.empty-state__message*This Application has no members*
waitForPageToLoad
waitForElementPresentcss=a.usa-button.usa-button-secondary.add-new-button
clickcss=a.usa-button.usa-button-secondary.add-new-button
waitForPageToLoad
waitForElementPresentcss=#add-app-mem > div > div:nth-of-type(1) > h1
assertElementPresentcss=#add-app-mem > div > div:nth-of-type(1) > h1
waitForPageToLoad
waitForElementPresentcss=#user_data-first_name
typecss=#user_data-first_nameBrandon
waitForPageToLoad
waitForElementPresentcss=#user_data-last_name
typecss=#user_data-last_nameBuchannan
waitForPageToLoad
waitForElementPresentcss=#user_data-email
typecss=#user_data-emailbrandon@example.com
waitForPageToLoad
waitForElementPresentcss=#user_data-phone_number
typecss=#user_data-phone_number(206) 555-2342
waitForPageToLoad
waitForElementPresentcss=#user_data-dod_id
typecss=#user_data-dod_id3456789012
waitForPageToLoad
waitForElementPresentcss=#add-app-mem > div > div:nth-of-type(1) > .action-group > input[type="button"].action-group__action.usa-button
clickcss=#add-app-mem > div > div:nth-of-type(1) > .action-group > input[type="button"].action-group__action.usa-button
waitForPageToLoad
waitForElementPresentcss=#add-app-mem > div > div:nth-of-type(2) > h2
assertElementPresentcss=#add-app-mem > div > div:nth-of-type(2) > h2
waitForPageToLoad
waitForElementPresentcss=.application-perms > div:nth-of-type(1) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label
clickcss=.application-perms > div:nth-of-type(1) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label
waitForPageToLoad
waitForElementPresentcss=.application-perms > div:nth-of-type(2) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label
clickcss=.application-perms > div:nth-of-type(2) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label
waitForPageToLoad
waitForElementPresentcss=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label
clickcss=.application-perms > div:nth-of-type(3) > .usa-input.input__inline-fields > fieldset.usa-input__choices > legend > label
waitForPageToLoad
waitForElementPresentcss=[name=environment_roles-0-role]
typecss=[name=environment_roles-0-role]Basic Access
waitForPageToLoad
waitForElementPresentcss=[name=environment_roles-1-role]
typecss=[name=environment_roles-1-role]Network Admin
waitForPageToLoad
waitForElementPresentcss=input[type="submit"]
clickcss=input[type="submit"]
waitForPageToLoad
waitForElementPresentcss=table.atat-table > tbody > tr > td:nth-of-type(1) > strong
assertTextcss=table.atat-table > tbody > tr > td:nth-of-type(1) > strong*Brandon Buchannan*
waitForPageToLoad
waitForElementPresentcss=.label
assertTextcss=.label*INVITE PENDING*
waitForPageToLoad
waitForElementPresentcss=section.member-list.application-list
assertElementPresentcss=section.member-list.application-list
waitForPageToLoad
waitForElementPresentcss=h3.usa-alert-heading
assertTextcss=h3.usa-alert-heading*Brandon's invitation has been sent*
waitForPageToLoad
waitForElementPresentcss=span.action-group-footer > a.usa-button
clickcss=span.action-group-footer > a.usa-button
waitForPageToLoad
waitForElementPresentcss=.usa-input.usa-input--validation--anything > input[id="name"][type="text"]
assertTextcss=.usa-input.usa-input--validation--anything > input[id="name"][type="text"]*Starter Application*
waitForPageToLoad
waitForElementPresentcss=#description
assertTextcss=#description*Any basic application*
waitForPageToLoad
waitForElementPresentcss=.label
assertTextcss=.label*INVITE PENDING*
waitForPageToLoad
waitForElementPresentcss=.accordion-table__items > .accordion-table__item:nth-of-type(1) > .accordion-table__item-content > .environment-list__item > .label.label--below
assertElementPresentcss=.accordion-table__items > .accordion-table__item:nth-of-type(1) > .accordion-table__item-content > .environment-list__item > .label.label--below
waitForPageToLoad
waitForElementPresentcss=#delete-application
assertTextcss=#delete-application*Delete application*
waitForPageToLoad
waitForElementPresentcss=svg.svg-inline--fa.fa-ellipsis-h
clickcss=svg.svg-inline--fa.fa-ellipsis-h
waitForPageToLoad
waitForElementPresentcss=.accordion-table__item-toggle-content > a:nth-of-type(2)
assertTextcss=.accordion-table__item-toggle-content > a:nth-of-type(2)*Resend Invite*
waitForPageToLoad
waitForElementPresentcss=.accordion-table__item-toggle-content > a:nth-of-type(2)
clickcss=.accordion-table__item-toggle-content > a:nth-of-type(2)
waitForPageToLoad
waitForElementPresentcss=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > .modal__form--header > h1
assertTextcss=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > .modal__form--header > h1*Verify Member Information*
waitForPageToLoad
waitForElementPresentcss=.application-member__user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]
assertTextcss=.application-member__user-info > .usa-input.usa-input--validation--requiredField:nth-of-type(1) > input[id="first_name"][type="text"]*Brandon*
waitForPageToLoad
waitForElementPresentcss=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]
assertTextcss=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]*Resend Invite*
waitForPageToLoad
waitForElementPresentcss=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]
clickcss=.panel > div:nth-of-type(2) > .modal.form-content--app-mem > .modal__container > .modal__dialog > .modal__body > form[action] > .action-group > button[type="submit"]
waitForPageToLoad
waitForElementPresentcss=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading
assertTextcss=.usa-alert.usa-alert-success > .usa-alert-body > h3.usa-alert-heading*Application invitation resent*
waitForPageToLoad
waitForElementPresentcss=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text
assertTextcss=.usa-alert.usa-alert-success > .usa-alert-body > .usa-alert-text*You have successfully resent the invite for Brandon Buchannan*
+ + \ No newline at end of file From c5014317193d5448512f4137efade2fe2a07f25a Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Wed, 27 Nov 2019 15:07:42 -0500 Subject: [PATCH 043/111] Check to see if the environment has been provisioned before disabling the env role in the csp --- atst/domain/environment_roles.py | 5 +++-- tests/domain/test_environment_roles.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index add92091..9c722828 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -105,8 +105,9 @@ class EnvironmentRoles(object): def disable(cls, environment_role_id): environment_role = EnvironmentRoles.get_by_id(environment_role_id) - credentials = environment_role.environment.csp_credentials - app.csp.cloud.disable_user(credentials, environment_role.csp_user_id) + if not environment_role.environment.is_pending: + credentials = environment_role.environment.csp_credentials + app.csp.cloud.disable_user(credentials, environment_role.csp_user_id) environment_role.status = EnvironmentRole.Status.DISABLED db.session.add(environment_role) diff --git a/tests/domain/test_environment_roles.py b/tests/domain/test_environment_roles.py index 486da70a..7c7fda08 100644 --- a/tests/domain/test_environment_roles.py +++ b/tests/domain/test_environment_roles.py @@ -91,6 +91,25 @@ 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_get_for_update(application_role, environment): EnvironmentRoleFactory.create( application_role=application_role, environment=environment, deleted=True From 0f6581848cd8909e7f17d78cfaf7e94b08c67fd1 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Fri, 6 Dec 2019 09:54:56 -0500 Subject: [PATCH 044/111] Remove superfluous steps; adjust to allow modifying user access in other tests --- uitests/New_App_Step_3.html | 140 +++++++----------------------------- 1 file changed, 26 insertions(+), 114 deletions(-) diff --git a/uitests/New_App_Step_3.html b/uitests/New_App_Step_3.html index 0aab34c0..5ebf05ce 100644 --- a/uitests/New_App_Step_3.html +++ b/uitests/New_App_Step_3.html @@ -92,7 +92,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -105,6 +106,25 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -180,24 +200,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 @@ -450,13 +452,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 @@ -467,12 +469,12 @@ Imported from: AT-AT CI - New Portfolio--> 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 @@ -884,21 +886,6 @@ Imported from: AT-AT CI - New App Step 1--> waitForElementPresent -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -click -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -waitForPageToLoad - - - - -waitForElementPresent css=[name=environment_roles-1-role] @@ -914,81 +901,6 @@ Imported from: AT-AT CI - New App Step 1--> waitForElementPresent -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -click -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -waitForPageToLoad - - - - -waitForElementPresent -css=[name=environment_roles-2-role] - - - -type -css=[name=environment_roles-2-role] -Business Read-only - - -waitForPageToLoad - - - - -waitForElementPresent -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -click -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -waitForPageToLoad - - - - -waitForElementPresent -css=[name=environment_roles-3-role] - - - -type -css=[name=environment_roles-3-role] -Technical Read-only - - -waitForPageToLoad - - - - -waitForElementPresent -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -click -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -waitForPageToLoad - - - - -waitForElementPresent css=input[type="submit"] From 708d8110fef0e558edc6d5b7eea52723b9b3b1d0 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Fri, 6 Dec 2019 09:55:46 -0500 Subject: [PATCH 045/111] Test that user can add environment access --- uitests/Edit_App_Member.html | 186 +++++++++++------------------------ 1 file changed, 58 insertions(+), 128 deletions(-) diff --git a/uitests/Edit_App_Member.html b/uitests/Edit_App_Member.html index 7e5070a1..276dcf34 100644 --- a/uitests/Edit_App_Member.html +++ b/uitests/Edit_App_Member.html @@ -102,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 @@ -117,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 @@ -200,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 @@ -500,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 @@ -519,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 @@ -990,23 +992,6 @@ Imported from: AT-AT CI - New App Step 3--> Imported from: AT-AT CI - New App Step 3--> waitForElementPresent -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -click -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=[name=environment_roles-1-role] @@ -1024,91 +1009,6 @@ Imported from: AT-AT CI - New App Step 3--> Imported from: AT-AT CI - New App Step 3--> waitForElementPresent -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -click -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-2-role] - - - -type -css=[name=environment_roles-2-role] -Business Read-only - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -click -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-3-role] - - - -type -css=[name=environment_roles-3-role] -Technical Read-only - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -click -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=input[type="submit"] @@ -1393,6 +1293,21 @@ Imported from: AT-AT CI - New App Step 3--> waitForElementPresent +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"] + + + +type +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"] +Business Read-only + + +waitForPageToLoad + + + + +waitForElementPresent css=input[type="submit"] @@ -1461,6 +1376,21 @@ Imported from: AT-AT CI - New App Step 3--> css=table.atat-table > tbody > tr:nth-of-type(1) > td:nth-of-type(2) > div:nth-of-type(2) *View Environments* + +waitForPageToLoad + + + + +waitForElementPresent +css=table.atat-table > tbody > tr:nth-of-type(1) > td.env_role--td > .row:nth-of-type(3) > .env-role__role + + + +assertText +css=table.atat-table > tbody > tr:nth-of-type(1) > td.env_role--td > .row:nth-of-type(3) > .env-role__role +*Business Read-only* + From 7638afaa9caf0d6f0b63b3805bf5d8ad69abe163 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Fri, 6 Dec 2019 09:56:55 -0500 Subject: [PATCH 046/111] Add Ghost Inspector-recommended flow control to wait for file upload to complete; accommodate empty-state changes --- uitests/TO_Step_1.html | 63 +++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/uitests/TO_Step_1.html b/uitests/TO_Step_1.html index 7a3a29d9..f1c42b68 100644 --- a/uitests/TO_Step_1.html +++ b/uitests/TO_Step_1.html @@ -31,7 +31,8 @@ - + open /login-dev @@ -42,6 +43,23 @@ + + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + waitForElementPresent @@ -109,22 +127,6 @@ waitForElementPresent -css=#defense_component > option:nth-of-type(14) - - - -click -css=#defense_component > option:nth-of-type(14) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=#description @@ -349,13 +351,13 @@ 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 @@ -379,13 +381,13 @@ waitForElementPresent -css=.empty-state__message +css=.empty-state h3 assertText -css=.empty-state__message -*This portfolio doesn’t have any active or pending task orders.* +css=.empty-state h3 +*Add approved task orders* waitForPageToLoad @@ -394,12 +396,12 @@ waitForElementPresent -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary click -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary @@ -452,6 +454,17 @@ + + +assertElementNotPresent +css=input[type="submit"][disabled="disabled"] + + + +waitForPageToLoad + + + waitForElementPresent css=a[href="#"].uploaded-file__remove From 4e14cd6ffb5846de7200c85f705924982f3bd18f Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Fri, 6 Dec 2019 09:58:50 -0500 Subject: [PATCH 047/111] Accommodate empty-state changes --- uitests/New_App_Step_1.html | 46 ++++++++++--------- uitests/New_Portfolio.html | 38 +++++++-------- .../New_Portfolio_-_No_optional_fields.html | 6 +-- 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/uitests/New_App_Step_1.html b/uitests/New_App_Step_1.html index d2d15a5d..0a36b66c 100644 --- a/uitests/New_App_Step_1.html +++ b/uitests/New_App_Step_1.html @@ -31,7 +31,8 @@ - + open /login-dev @@ -42,6 +43,23 @@ + + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + waitForElementPresent @@ -109,22 +127,6 @@ waitForElementPresent -css=#defense_component > option:nth-of-type(14) - - - -click -css=#defense_component > option:nth-of-type(14) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=#description @@ -349,13 +351,13 @@ 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 @@ -364,12 +366,12 @@ 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 diff --git a/uitests/New_Portfolio.html b/uitests/New_Portfolio.html index b385698a..356cb2b3 100644 --- a/uitests/New_Portfolio.html +++ b/uitests/New_Portfolio.html @@ -31,6 +31,7 @@ + open /login-dev @@ -41,6 +42,22 @@ + + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + waitForElementPresent css=a[href="/portfolios/new"] @@ -103,21 +120,6 @@ waitForElementPresent -css=#defense_component > option:nth-of-type(14) - - - -click -css=#defense_component > option:nth-of-type(14) - - - -waitForPageToLoad - - - - -waitForElementPresent css=#description @@ -328,13 +330,13 @@ 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* diff --git a/uitests/New_Portfolio_-_No_optional_fields.html b/uitests/New_Portfolio_-_No_optional_fields.html index eae79a14..269f96df 100644 --- a/uitests/New_Portfolio_-_No_optional_fields.html +++ b/uitests/New_Portfolio_-_No_optional_fields.html @@ -165,13 +165,13 @@ 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* From fb2beb5bc5f73860337c35dbebabae69a6ce2b16 Mon Sep 17 00:00:00 2001 From: "Jay R. Newlin (PromptWorks)" Date: Fri, 6 Dec 2019 09:59:16 -0500 Subject: [PATCH 048/111] Minor adjustments --- uitests/Application_Settings.html | 148 +++++------------------- uitests/Edit_Portfolio_Member.html | 46 ++++---- uitests/New_App_Step_2.html | 48 ++++---- uitests/New_App_Step_2_-_Add_Env.html | 50 +++++---- uitests/New_Portfolio_Member.html | 44 ++++---- uitests/Portfolio_Settings.html | 42 +++---- uitests/Remove_Portfolio_Member.html | 46 ++++---- uitests/Revoke_App_Member_Invite.html | 156 +++++--------------------- uitests/TO_Step_2.html | 66 ++++++----- uitests/TO_Step_3.html | 84 +++++++------- uitests/TO_Step_3_-_Add_CLIN.html | 88 +++++++-------- uitests/TO_Step_4.html | 88 +++++++-------- uitests/TO_Step_5.html | 92 +++++++-------- 13 files changed, 415 insertions(+), 583 deletions(-) diff --git a/uitests/Application_Settings.html b/uitests/Application_Settings.html index 08ab153d..76557d2d 100644 --- a/uitests/Application_Settings.html +++ b/uitests/Application_Settings.html @@ -97,7 +97,8 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -111,6 +112,26 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -190,25 +211,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 @@ -475,13 +477,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 @@ -493,12 +495,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 @@ -937,22 +939,6 @@ Imported from: AT-AT CI - New App Step 2--> waitForElementPresent -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -click -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=[name=environment_roles-1-role] @@ -969,86 +955,6 @@ Imported from: AT-AT CI - New App Step 2--> waitForElementPresent -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -click -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-2-role] - - - -type -css=[name=environment_roles-2-role] -Business Read-only - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -click -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-3-role] - - - -type -css=[name=environment_roles-3-role] -Technical Read-only - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -click -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=input[type="submit"] diff --git a/uitests/Edit_Portfolio_Member.html b/uitests/Edit_Portfolio_Member.html index b2f179e4..e5003462 100644 --- a/uitests/Edit_Portfolio_Member.html +++ b/uitests/Edit_Portfolio_Member.html @@ -96,7 +96,8 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -109,6 +110,25 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -184,24 +204,6 @@ Imported from: AT-AT CI - Portfolio Settings 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 @@ -454,13 +456,13 @@ Imported from: AT-AT CI - Portfolio Settings 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 diff --git a/uitests/New_App_Step_2.html b/uitests/New_App_Step_2.html index 0e751ace..16c2877a 100644 --- a/uitests/New_App_Step_2.html +++ b/uitests/New_App_Step_2.html @@ -32,7 +32,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -44,6 +45,24 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -115,23 +134,6 @@ Imported from: AT-AT CI - New Portfolio--> 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 @@ -370,13 +372,13 @@ Imported from: AT-AT CI - New Portfolio--> 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 @@ -386,12 +388,12 @@ Imported from: AT-AT CI - New Portfolio--> 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 diff --git a/uitests/New_App_Step_2_-_Add_Env.html b/uitests/New_App_Step_2_-_Add_Env.html index 4034713f..5a89c577 100644 --- a/uitests/New_App_Step_2_-_Add_Env.html +++ b/uitests/New_App_Step_2_-_Add_Env.html @@ -33,7 +33,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -46,6 +47,25 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -121,24 +141,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 @@ -391,13 +393,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 @@ -408,12 +410,12 @@ Imported from: AT-AT CI - New Portfolio--> 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 diff --git a/uitests/New_Portfolio_Member.html b/uitests/New_Portfolio_Member.html index 090ed9cc..13a426c2 100644 --- a/uitests/New_Portfolio_Member.html +++ b/uitests/New_Portfolio_Member.html @@ -91,7 +91,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -103,6 +104,24 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -174,23 +193,6 @@ Imported from: AT-AT CI - New Portfolio--> 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 @@ -429,13 +431,13 @@ Imported from: AT-AT CI - New Portfolio--> 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 diff --git a/uitests/Portfolio_Settings.html b/uitests/Portfolio_Settings.html index 89f85a79..d12bdffd 100644 --- a/uitests/Portfolio_Settings.html +++ b/uitests/Portfolio_Settings.html @@ -31,7 +31,8 @@ - + open /login-dev @@ -42,6 +43,23 @@ + + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + waitForElementPresent @@ -109,22 +127,6 @@ waitForElementPresent -css=#defense_component > option:nth-of-type(14) - - - -click -css=#defense_component > option:nth-of-type(14) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=#description @@ -349,13 +351,13 @@ 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 diff --git a/uitests/Remove_Portfolio_Member.html b/uitests/Remove_Portfolio_Member.html index 47d5b49d..a6e3f87a 100644 --- a/uitests/Remove_Portfolio_Member.html +++ b/uitests/Remove_Portfolio_Member.html @@ -96,7 +96,8 @@ Imported from: AT-AT CI - Login Brandon--> +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -109,6 +110,25 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -184,24 +204,6 @@ Imported from: AT-AT CI - Portfolio Settings 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 @@ -454,13 +456,13 @@ Imported from: AT-AT CI - Portfolio Settings 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 diff --git a/uitests/Revoke_App_Member_Invite.html b/uitests/Revoke_App_Member_Invite.html index 1fd8dec6..df43cb2d 100644 --- a/uitests/Revoke_App_Member_Invite.html +++ b/uitests/Revoke_App_Member_Invite.html @@ -102,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 @@ -117,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 @@ -200,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 @@ -500,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 @@ -519,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 @@ -990,23 +992,6 @@ Imported from: AT-AT CI - New App Step 3--> Imported from: AT-AT CI - New App Step 3--> waitForElementPresent -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -click -css=[name=environment_roles-0-role] > option:nth-of-type(1) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=[name=environment_roles-1-role] @@ -1024,91 +1009,6 @@ Imported from: AT-AT CI - New App Step 3--> Imported from: AT-AT CI - New App Step 3--> waitForElementPresent -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -click -css=[name=environment_roles-1-role] > option:nth-of-type(2) - - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-2-role] - - - -type -css=[name=environment_roles-2-role] -Business Read-only - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -click -css=[name=environment_roles-2-role] > option:nth-of-type(3) - - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-3-role] - - - -type -css=[name=environment_roles-3-role] -Technical Read-only - - -waitForPageToLoad - - - - - -waitForElementPresent -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -click -css=[name=environment_roles-3-role] > option:nth-of-type(4) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=input[type="submit"] diff --git a/uitests/TO_Step_2.html b/uitests/TO_Step_2.html index 9a91e6fb..30cd6c58 100644 --- a/uitests/TO_Step_2.html +++ b/uitests/TO_Step_2.html @@ -32,7 +32,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -44,6 +45,24 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -115,23 +134,6 @@ Imported from: AT-AT CI - New Portfolio--> 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 @@ -370,13 +372,13 @@ Imported from: AT-AT CI - New Portfolio--> 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 @@ -402,13 +404,13 @@ 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 active or pending task orders.* +css=.empty-state h3 +*Add approved task orders* waitForPageToLoad @@ -418,12 +420,12 @@ Imported from: AT-AT CI - New Portfolio--> waitForElementPresent -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary click -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary @@ -479,6 +481,18 @@ Imported from: AT-AT CI - New Portfolio--> + + +assertElementNotPresent +css=input[type="submit"][disabled="disabled"] + + + +waitForPageToLoad + + + waitForElementPresent diff --git a/uitests/TO_Step_3.html b/uitests/TO_Step_3.html index 501a7ca1..346d68b3 100644 --- a/uitests/TO_Step_3.html +++ b/uitests/TO_Step_3.html @@ -33,7 +33,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -46,6 +47,25 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -121,24 +141,6 @@ Imported from: AT-AT CI - TO 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 @@ -391,13 +393,13 @@ Imported from: AT-AT CI - TO 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 @@ -425,13 +427,13 @@ Imported from: AT-AT CI - TO Step 1--> Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.empty-state__message +css=.empty-state h3 assertText -css=.empty-state__message -*This portfolio doesn’t have any active or pending task orders.* +css=.empty-state h3 +*Add approved task orders* waitForPageToLoad @@ -442,12 +444,12 @@ Imported from: AT-AT CI - TO Step 1--> Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary click -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary @@ -507,6 +509,19 @@ Imported from: AT-AT CI - TO Step 1--> + +assertElementNotPresent +css=input[type="submit"][disabled="disabled"] + + + +waitForPageToLoad + + + + waitForElementPresent @@ -664,21 +679,6 @@ Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -click -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -waitForPageToLoad - - - - -waitForElementPresent css=#percent-obligated diff --git a/uitests/TO_Step_3_-_Add_CLIN.html b/uitests/TO_Step_3_-_Add_CLIN.html index 4b1bcbde..d7ec07cf 100644 --- a/uitests/TO_Step_3_-_Add_CLIN.html +++ b/uitests/TO_Step_3_-_Add_CLIN.html @@ -34,7 +34,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -48,6 +49,26 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -127,25 +148,6 @@ Imported from: AT-AT CI - TO 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 @@ -412,13 +414,13 @@ Imported from: AT-AT CI - TO 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 @@ -448,13 +450,13 @@ Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.empty-state__message +css=.empty-state h3 assertText -css=.empty-state__message -*This portfolio doesn’t have any active or pending task orders.* +css=.empty-state h3 +*Add approved task orders* waitForPageToLoad @@ -466,12 +468,12 @@ Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary click -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary @@ -535,6 +537,20 @@ Imported from: AT-AT CI - TO Step 1--> + +assertElementNotPresent +css=input[type="submit"][disabled="disabled"] + + + +waitForPageToLoad + + + + waitForElementPresent @@ -702,22 +718,6 @@ Imported from: AT-AT CI - TO Step 2--> waitForElementPresent -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -click -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=#percent-obligated diff --git a/uitests/TO_Step_4.html b/uitests/TO_Step_4.html index 5ac0bec3..1ea441eb 100644 --- a/uitests/TO_Step_4.html +++ b/uitests/TO_Step_4.html @@ -34,7 +34,8 @@ +Imported from: AT-AT CI - New Portfolio +Imported from: AT-AT CI - login--> open /login-dev @@ -48,6 +49,26 @@ Imported from: AT-AT CI - New Portfolio--> + +waitForElementPresent +css=.about-cloud > h1 + + + +assertText +css=.about-cloud > h1 +About Cloud Services + + +waitForPageToLoad + + + + waitForElementPresent @@ -127,25 +148,6 @@ Imported from: AT-AT CI - TO 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 @@ -412,13 +414,13 @@ Imported from: AT-AT CI - TO 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 @@ -448,13 +450,13 @@ Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.empty-state__message +css=.empty-state h3 assertText -css=.empty-state__message -*This portfolio doesn’t have any active or pending task orders.* +css=.empty-state h3 +*Add approved task orders* waitForPageToLoad @@ -466,12 +468,12 @@ Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary click -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary @@ -535,6 +537,20 @@ Imported from: AT-AT CI - TO Step 1--> + +assertElementNotPresent +css=input[type="submit"][disabled="disabled"] + + + +waitForPageToLoad + + + + waitForElementPresent @@ -702,22 +718,6 @@ Imported from: AT-AT CI - TO Step 2--> waitForElementPresent -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -click -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=#percent-obligated diff --git a/uitests/TO_Step_5.html b/uitests/TO_Step_5.html index 56713fcd..c52df676 100644 --- a/uitests/TO_Step_5.html +++ b/uitests/TO_Step_5.html @@ -35,7 +35,8 @@ Imported from: AT-AT CI - TO Step 3 Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO 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 @@ -50,6 +51,27 @@ Imported from: AT-AT CI - New Portfolio--> Imported from: AT-AT CI - TO Step 3 Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO 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 @@ -133,26 +155,6 @@ Imported from: AT-AT CI - TO 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 @@ -433,13 +435,13 @@ Imported from: AT-AT CI - TO 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 @@ -471,13 +473,13 @@ Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.empty-state__message +css=.empty-state h3 assertText -css=.empty-state__message -*This portfolio doesn’t have any active or pending task orders.* +css=.empty-state h3 +*Add approved task orders* waitForPageToLoad @@ -490,12 +492,12 @@ Imported from: AT-AT CI - TO Step 2 Imported from: AT-AT CI - TO Step 1--> waitForElementPresent -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary click -css=.usa-button.usa-button-big +css=.usa-button.usa-button-primary @@ -563,6 +565,21 @@ Imported from: AT-AT CI - TO Step 1--> + +assertElementNotPresent +css=input[type="submit"][disabled="disabled"] + + + +waitForPageToLoad + + + + waitForElementPresent @@ -740,23 +757,6 @@ Imported from: AT-AT CI - TO Step 3--> Imported from: AT-AT CI - TO Step 3--> waitForElementPresent -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -click -css=#clins-0-jedi_clin_type > option:nth-of-type(2) - - - -waitForPageToLoad - - - - - -waitForElementPresent css=#percent-obligated From 8330b4de24b62a96915c57a60cd88f43153c9c93 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 3 Dec 2019 16:19:33 -0500 Subject: [PATCH 049/111] Check to see if the env_role has been provisioned before disabling env_role in the csp --- atst/domain/environment_roles.py | 2 +- tests/domain/test_environment_roles.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index 9c722828..ec34b123 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -105,7 +105,7 @@ class EnvironmentRoles(object): def disable(cls, environment_role_id): environment_role = EnvironmentRoles.get_by_id(environment_role_id) - if not environment_role.environment.is_pending: + if environment_role.csp_user_id and not environment_role.environment.is_pending: credentials = environment_role.environment.csp_credentials app.csp.cloud.disable_user(credentials, environment_role.csp_user_id) diff --git a/tests/domain/test_environment_roles.py b/tests/domain/test_environment_roles.py index 7c7fda08..b11d14f9 100644 --- a/tests/domain/test_environment_roles.py +++ b/tests/domain/test_environment_roles.py @@ -110,6 +110,23 @@ def test_disable_checks_env_provisioning_status(session): 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 From 2d714cae39560a176819884e118f7dd5dd02bd5a Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 5 Dec 2019 13:08:18 -0500 Subject: [PATCH 050/111] Add some missing configuration settings. These settings are used in the deployed instance and mentioned elsewhere in the README, but were missing from the base INI file and the Configuration Guide section of the README. --- .secrets.baseline | 4 ++-- README.md | 17 ++++++++++------- config/base.ini | 8 ++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 49a70104..385c0b04 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "lines": null }, - "generated_at": "2019-12-03T19:44:47Z", + "generated_at": "2019-12-05T17:54:05Z", "plugins_used": [ { "base64_limit": 4.5, @@ -98,7 +98,7 @@ "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_secret": false, "is_verified": false, - "line_number": 21, + "line_number": 29, "type": "Secret Keyword" } ], diff --git a/README.md b/README.md index 44749862..9c31fc19 100644 --- a/README.md +++ b/README.md @@ -168,12 +168,7 @@ Testing file uploads and downloads locally requires a few configuration options. In the flask config (`config/base.ini`, perhaps): ``` -CSP= - -AWS_REGION_NAME="" -AWS_ACCESS_KEY="" -AWS_SECRET_KEY="" -AWS_BUCKET_NAME="" +CSP=< azure | mock> AZURE_STORAGE_KEY="" AZURE_ACCOUNT_NAME="" @@ -183,7 +178,7 @@ AZURE_TO_BUCKET_NAME="" There are also some build-time configuration that are used by parcel. Add these to `.env.local`, and run `rm -r .cache/` before running `yarn build`: ``` -CLOUD_PROVIDER= +CLOUD_PROVIDER= AZURE_ACCOUNT_NAME="" AZURE_CONTAINER_NAME="" ``` @@ -223,6 +218,9 @@ To generate coverage reports for the Javascript tests: ## Configuration - `ASSETS_URL`: URL to host which serves static assets (such as a CDN). +- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account +- `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account +- `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads - `BLOB_STORAGE_URL`: URL to Azure blob storage container. - `CAC_URL`: URL for the CAC authentication route. - `CA_CHAIN`: Path to the CA chain file. @@ -238,6 +236,11 @@ To generate coverage reports for the Javascript tests: - `ENVIRONMENT`: String specifying the current environment. Acceptable values: "dev", "prod". - `LIMIT_CONCURRENT_SESSIONS`: Boolean specifying if users should be allowed only one active session at a time. - `LOG_JSON`: Boolean specifying whether app should log in a json format. +- `MAIL_PASSWORD`: String. Password for the SMTP server. +- `MAIL_PORT`: Integer. Port to use on the SMTP server. +- `MAIL_SENDER`: String. Email address to send outgoing mail from. +- `MAIL_SERVER`: The SMTP host +- `MAIL_TLS`: Boolean. Use TLS to connect to the SMTP server. - `PERMANENT_SESSION_LIFETIME`: Integer specifying how many seconds a user's session can stay valid for. https://flask.palletsprojects.com/en/1.1.x/config/#PERMANENT_SESSION_LIFETIME - `PGDATABASE`: String specifying the name of the postgres database. - `PGHOST`: String specifying the hostname of the postgres database. diff --git a/config/base.ini b/config/base.ini index ade3abe1..2cc8fd93 100644 --- a/config/base.ini +++ b/config/base.ini @@ -1,5 +1,8 @@ [default] ASSETS_URL +AZURE_ACCOUNT_NAME +AZURE_STORAGE_KEY +AZURE_TO_BUCKET_NAME BLOB_STORAGE_URL=http://localhost:8000/ CAC_URL = http://localhost:8000/login-redirect CA_CHAIN = ssl/server-certs/ca-chain.pem @@ -15,6 +18,11 @@ DISABLE_CRL_CHECK = false ENVIRONMENT = dev LIMIT_CONCURRENT_SESSIONS = false LOG_JSON = false +MAIL_PASSWORD +MAIL_PORT +MAIL_SENDER +MAIL_SERVER +MAIL_TLS PERMANENT_SESSION_LIFETIME = 1800 PGDATABASE = atat PGHOST = localhost From 972cf14a66792e219ba97a36714bc5cc3935356b Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 5 Dec 2019 13:18:28 -0500 Subject: [PATCH 051/111] K8s configuration for mounting application config. This adds an additional volume mount for Flask application secrets. These will be mounted into the ATST container so that their values can be read in as config. --- deploy/azure/azure.yml | 38 +++++++++++++++++++++ deploy/azure/crls-sync.yaml | 17 ++++++++++ deploy/overlays/staging/flex_vol.yml | 49 ++++++++++++++++++++++++++++ deploy/shared/migration.yaml | 27 +++++++++------ 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index 02952029..cf91cf58 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -50,6 +50,8 @@ spec: - name: uwsgi-config mountPath: "/opt/atat/atst/uwsgi.ini" subPath: uwsgi.ini + - name: flask-secret + mountPath: "/config" - name: nginx image: nginx:alpine ports: @@ -141,6 +143,16 @@ spec: keyvaultobjectaliases: "dhparam.pem;atat.key;atat.crt" keyvaultobjecttypes: "secret;secret;secret" tenantid: $TENANT_ID + - name: flask-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY" + keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY" + keyvaultobjecttypes: "secret;secret;secret;secret;key" + tenantid: $TENANT_ID --- apiVersion: extensions/v1beta1 kind: Deployment @@ -161,6 +173,7 @@ spec: labels: app: atst role: worker + aadpodidbinding: atat-kv-id-binding spec: securityContext: fsGroup: 101 @@ -188,6 +201,8 @@ spec: - name: pgsslrootcert mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt" subPath: pgsslrootcert.crt + - name: flask-secret + mountPath: "/config" volumes: - name: atst-config secret: @@ -203,6 +218,16 @@ spec: - key: cert path: pgsslrootcert.crt mode: 0666 + - name: flask-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY" + keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY" + keyvaultobjecttypes: "secret;secret;secret;secret;key" + tenantid: $TENANT_ID --- apiVersion: extensions/v1beta1 kind: Deployment @@ -223,6 +248,7 @@ spec: labels: app: atst role: beat + aadpodidbinding: atat-kv-id-binding spec: securityContext: fsGroup: 101 @@ -250,6 +276,8 @@ spec: - name: pgsslrootcert mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt" subPath: pgsslrootcert.crt + - name: flask-secret + mountPath: "/config" volumes: - name: atst-config secret: @@ -265,6 +293,16 @@ spec: - key: cert path: pgsslrootcert.crt mode: 0666 + - name: flask-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY" + keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY" + keyvaultobjecttypes: "secret;secret;secret;secret;key" + tenantid: $TENANT_ID --- apiVersion: v1 kind: Service diff --git a/deploy/azure/crls-sync.yaml b/deploy/azure/crls-sync.yaml index 5e95e331..c2a07327 100644 --- a/deploy/azure/crls-sync.yaml +++ b/deploy/azure/crls-sync.yaml @@ -10,6 +10,11 @@ spec: jobTemplate: spec: template: + metadata: + labels: + app: atst + role: crl-sync + aadpodidbinding: atat-kv-id-binding spec: restartPolicy: OnFailure containers: @@ -32,6 +37,8 @@ spec: subPath: atst-overrides.ini - name: crls-vol mountPath: "/opt/atat/atst/crls" + - name: flask-secret + mountPath: "/config" volumes: - name: atst-config secret: @@ -43,3 +50,13 @@ spec: - name: crls-vol persistentVolumeClaim: claimName: crls-vol-claim + - name: flask-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY" + keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY" + keyvaultobjecttypes: "secret;secret;secret;secret;key" + tenantid: $TENANT_ID diff --git a/deploy/overlays/staging/flex_vol.yml b/deploy/overlays/staging/flex_vol.yml index 0ebeea84..0efa4044 100644 --- a/deploy/overlays/staging/flex_vol.yml +++ b/deploy/overlays/staging/flex_vol.yml @@ -11,3 +11,52 @@ spec: options: keyvaultname: "atat-vault-test" keyvaultobjectnames: "dhparam4096;staging-cert;staging-cert" + - name: flask-secret + flexVolume: + options: + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY" +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: atst-worker +spec: + template: + spec: + volumes: + - name: flask-secret + flexVolume: + options: + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY" +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: atst-beat +spec: + template: + spec: + volumes: + - name: flask-secret + flexVolume: + options: + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY" +--- +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + name: crls +spec: + jobTemplate: + spec: + template: + spec: + volumes: + - name: flask-secret + flexVolume: + options: + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY" diff --git a/deploy/shared/migration.yaml b/deploy/shared/migration.yaml index d571c84d..b5161114 100644 --- a/deploy/shared/migration.yaml +++ b/deploy/shared/migration.yaml @@ -7,6 +7,11 @@ spec: ttlSecondsAfterFinished: 100 backoffLimit: 2 template: + metadata: + labels: + app: atst + role: migration + aadpodidbinding: atat-kv-id-binding spec: containers: - name: migration @@ -28,20 +33,12 @@ spec: - configMapRef: name: atst-worker-envvars volumeMounts: - - name: atst-config - mountPath: "/opt/atat/atst/atst-overrides.ini" - subPath: atst-overrides.ini - name: pgsslrootcert mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt" subPath: pgsslrootcert.crt + - name: flask-secret + mountPath: "/config" volumes: - - name: atst-config - secret: - secretName: atst-config-ini - items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert @@ -49,4 +46,14 @@ spec: - key: cert path: pgsslrootcert.crt mode: 0666 + - name: flask-secret + flexVolume: + driver: "azure/kv" + options: + usepodidentity: "true" + keyvaultname: "atat-vault-test" + keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY" + keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY" + keyvaultobjecttypes: "secret;secret;secret;secret;key" + tenantid: $TENANT_ID restartPolicy: Never From f8c31e4dcfa2385ce858eeadf4d33694629824d8 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 5 Dec 2019 14:11:35 -0500 Subject: [PATCH 052/111] Add function for Flask app to read config from a directory. The application now checks for an environment variable, OVERRIDE_CONFIG_DIRECTORY. If that value is set, it loops all the files in the specified directory and checks if their names correspond to known configuration settings. The contents of any matching files are read and set as the new configuration value for that setting. This will allow us to read mounted Azure Key Vault items as config values. This also moves the functionality for applying environment variables to the config into an analogous function. --- atst/app.py | 42 +++++++++++++++++++++++++++++++++++------- tests/test_app.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/atst/app.py b/atst/app.py index 1b60f64c..e9daabd6 100644 --- a/atst/app.py +++ b/atst/app.py @@ -200,23 +200,21 @@ def make_config(direct_config=None): ENV_CONFIG_FILENAME = os.path.join( os.path.dirname(__file__), "../config/", "{}.ini".format(ENV.lower()) ) - OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH") + OVERRIDE_CONFIG_DIRECTORY = os.getenv("OVERRIDE_CONFIG_DIRECTORY") config = ConfigParser(allow_no_value=True) config.optionxform = str config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME] - if OVERRIDE_CONFIG_FILENAME: - config_files.append(OVERRIDE_CONFIG_FILENAME) # ENV_CONFIG will override values in BASE_CONFIG. config.read(config_files) + if OVERRIDE_CONFIG_DIRECTORY: + apply_config_from_directory(OVERRIDE_CONFIG_DIRECTORY, config) + # Check for ENV variables as a final source of overrides - for confsetting in config.options("default"): - env_override = os.getenv(confsetting.upper()) - if env_override: - config.set("default", confsetting, env_override) + apply_config_from_environment(config) # override if a dictionary of options has been given if direct_config: @@ -244,6 +242,36 @@ def make_config(direct_config=None): return map_config(config) +def apply_config_from_directory(config_dir, config, section="default"): + """ + Loop files in a directory, check if the names correspond to + known config values, and apply the file contents as the value + for that setting if they do. + """ + for confsetting in os.listdir(config_dir): + if confsetting in config.options(section): + full_path = os.path.join(config_dir, confsetting) + with open(full_path, "r") as conf_file: + config.set(section, confsetting, conf_file.read().strip()) + + return config + + +def apply_config_from_environment(config, section="default"): + """ + Loops all the configuration settins in a given section of a + config object and checks whether those settings also exist as + environment variables. If so, it applies the environment + variables value as the new configuration setting value. + """ + for confsetting in config.options(section): + env_override = os.getenv(confsetting.upper()) + if env_override: + config.set(section, confsetting, env_override) + + return config + + def make_redis(app, config): r = redis.Redis.from_url(config["REDIS_URI"]) app.redis = r 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") From ec638d6b019c6d4b8f5c1b4450dda554d52b9056 Mon Sep 17 00:00:00 2001 From: dandds Date: Thu, 5 Dec 2019 14:56:07 -0500 Subject: [PATCH 053/111] Transition to using secrets in Key Vault. This does the following: - Removes references to the atst-override.ini file, now deprecated. - Adds all non-secret data that was managed in the override file to the relevant K8s ConfigMaps. - Adds additional documentation explaining out use of Key Vault for secrets management. --- deploy/README.md | 52 ++++++++----------- deploy/azure/atst-envvars-configmap.yml | 15 +++++- .../azure/atst-worker-envvars-configmap.yml | 18 ++++++- deploy/azure/azure.yml | 30 ----------- deploy/azure/crls-sync.yaml | 10 ---- 5 files changed, 53 insertions(+), 72 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 25380293..c0683ae0 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -37,35 +37,6 @@ If you are satisfied with the output from the diff, you can apply the new config ## Secrets and Configuration -### atst-overrides.ini - -Production configuration values are provided to the ATAT Flask app by writing an `atst-overrides.ini` file to the running Docker container. This file is stored as a Kubernetes secret. It contains configuration information for the database connection, mailer, etc. - -To update the configuration, you can do the following: - -``` -kubectl -n atat get secret atst-config-ini -o=jsonpath='{.data.override\.ini}' | base64 --decode > override.ini -``` - -This base64 decodes the secret and writes it to a local file called `override.ini`. Make any necessary config changes to that file. - -To apply the new config, first delete the existing copy of the secret: - -``` -kubectl -n atat delete secret atst-config-ini -``` - -Then create a new copy of the secret from your updated copy: - -``` -kubectl -n atat create secret generic atst-config-ini --from-file=./override.ini -``` - -Notes: - -- Be careful not to check the override.ini file into source control. -- Be careful not to overwrite one CSP cluster's config with the other's. This will break everything. - ### nginx-htpasswd If the site is running in dev mode, the `/login-dev` endpoint is available. This endpoint is protected by basic HTTP auth. To create a new password file, run: @@ -178,11 +149,32 @@ az keyvault secret set --vault-name --name --value ``` --- +# Secrets Management + +Secrets, keys, and certificates are managed from Azure Key Vault. These items are mounted into the containers at runtime using the FlexVol implementation described below. + +The following are mounted into the NGINX container in the atst pod: + +- The TLS certs for the site +- The DH parameter for TLS connections + +These are mounted into every instance of the Flask application container (the atst container, the celery worker, etc.): + +- The Azure storage key used to access blob storage (AZURE_STORAGE_KEY) +- The password for the SMTP server used to send mail (MAIL_PASSWORD) +- The Postgres database user password (PGPASSWORD) +- The Redis user password (REDIS_PASSWORD) +- The Flask secret key used for session signing and generating CSRF tokens (SECRET_KEY) + +Secrets should be added to Key Vault with the following naming pattern: [branch/environment]-[all-caps config setting name]. Note that Key Vault does not support underscores. Substitute hyphens. For example, the config setting for the SMTP server password is MAIL_SERVER. The corresponding secret name in Key Vault is "master-MAIL-SERVER" for the credential used in the primary environment.These secrets are mounted into the containers via FlexVol. + +To add or manage secrets, keys, and certificates in Key Vault, see the [documentation](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-cli). + # Setting Up FlexVol for Secrets ## Preparing Azure Environment -A Key Vault will need to be created. Save it's full id (the full path) for use later. +A Key Vault will need to be created. Save its full id (the full path) for use later. ## Preparing Cluster diff --git a/deploy/azure/atst-envvars-configmap.yml b/deploy/azure/atst-envvars-configmap.yml index d6bd60ef..8907493d 100644 --- a/deploy/azure/atst-envvars-configmap.yml +++ b/deploy/azure/atst-envvars-configmap.yml @@ -6,15 +6,28 @@ metadata: namespace: atat data: ASSETS_URL: https://atat-cdn.azureedge.net/ + AZURE_ACCOUNT_NAME: atat + AZURE_TO_BUCKET_NAME: task-order-pdfs BLOB_STORAGE_URL: https://atat.blob.core.windows.net/ + CAC_URL: https://auth-staging.atat.code.mil/login-redirect CDN_ORIGIN: https://azure.atat.code.mil CELERY_DEFAULT_QUEUE: celery-master CSP: azure + DEBUG: 0 FLASK_ENV: master LOG_JSON: "true" - OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini + MAIL_PORT: 587 + MAIL_SENDER: postmaster@atat.code.mil + MAIL_SERVER: smtp.mailgun.org + MAIL_TLS: "true" + OVERRIDE_CONFIG_DIRECTORY: /config + PGAPPNAME: atst + PGDATABASE: staging + PGHOST: atat-db.postgres.database.azure.com + PGPORT: 5432 PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt + PGUSER: atat_master@atat-db REDIS_HOST: atat.redis.cache.windows.net:6380 REDIS_TLS: "true" STATIC_URL: https://atat-cdn.azureedge.net/static/ diff --git a/deploy/azure/atst-worker-envvars-configmap.yml b/deploy/azure/atst-worker-envvars-configmap.yml index c3522f70..ab10c118 100644 --- a/deploy/azure/atst-worker-envvars-configmap.yml +++ b/deploy/azure/atst-worker-envvars-configmap.yml @@ -5,9 +5,25 @@ metadata: name: atst-worker-envvars namespace: atat data: + AZURE_ACCOUNT_NAME: atat + AZURE_TO_BUCKET_NAME: task-order-pdfs + CAC_URL: https://auth-staging.atat.code.mil/login-redirect CELERY_DEFAULT_QUEUE: celery-master - DISABLE_CRL_CHECK: "True" + DEBUG: 0 + DISABLE_CRL_CHECK: "true" + MAIL_PORT: 587 + MAIL_SENDER: postmaster@atat.code.mil + MAIL_SERVER: smtp.mailgun.org + MAIL_TLS: "true" + OVERRIDE_CONFIG_DIRECTORY: /config + PGAPPNAME: atst + PGDATABASE: staging + PGHOST: atat-db.postgres.database.azure.com + PGPORT: 5432 PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt + PGUSER: atat_master@atat-db + REDIS_HOST: atat.redis.cache.windows.net:6380 + REDIS_TLS: "true" SERVER_NAME: azure.atat.code.mil TZ: UTC diff --git a/deploy/azure/azure.yml b/deploy/azure/azure.yml index cf91cf58..8fe7fd87 100644 --- a/deploy/azure/azure.yml +++ b/deploy/azure/azure.yml @@ -34,9 +34,6 @@ spec: - configMapRef: name: atst-envvars volumeMounts: - - name: atst-config - mountPath: "/opt/atat/atst/atst-overrides.ini" - subPath: atst-overrides.ini - name: nginx-client-ca-bundle mountPath: "/opt/atat/atst/ssl/server-certs/ca-chain.pem" subPath: client-ca-bundle.pem @@ -81,13 +78,6 @@ spec: - name: nginx-secret mountPath: "/etc/ssl/" volumes: - - name: atst-config - secret: - secretName: atst-config-ini - items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 - name: nginx-client-ca-bundle configMap: name: nginx-client-ca-bundle @@ -195,22 +185,12 @@ spec: - configMapRef: name: atst-worker-envvars volumeMounts: - - name: atst-config - mountPath: "/opt/atat/atst/atst-overrides.ini" - subPath: atst-overrides.ini - name: pgsslrootcert mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt" subPath: pgsslrootcert.crt - name: flask-secret mountPath: "/config" volumes: - - name: atst-config - secret: - secretName: atst-config-ini - items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert @@ -270,22 +250,12 @@ spec: - configMapRef: name: atst-worker-envvars volumeMounts: - - name: atst-config - mountPath: "/opt/atat/atst/atst-overrides.ini" - subPath: atst-overrides.ini - name: pgsslrootcert mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt" subPath: pgsslrootcert.crt - name: flask-secret mountPath: "/config" volumes: - - name: atst-config - secret: - secretName: atst-config-ini - items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 - name: pgsslrootcert configMap: name: pgsslrootcert diff --git a/deploy/azure/crls-sync.yaml b/deploy/azure/crls-sync.yaml index c2a07327..5fdcd7b8 100644 --- a/deploy/azure/crls-sync.yaml +++ b/deploy/azure/crls-sync.yaml @@ -32,21 +32,11 @@ spec: - configMapRef: name: atst-worker-envvars volumeMounts: - - name: atst-config - mountPath: "/opt/atat/atst/atst-overrides.ini" - subPath: atst-overrides.ini - name: crls-vol mountPath: "/opt/atat/atst/crls" - name: flask-secret mountPath: "/config" volumes: - - name: atst-config - secret: - secretName: atst-config-ini - items: - - key: override.ini - path: atst-overrides.ini - mode: 0644 - name: crls-vol persistentVolumeClaim: claimName: crls-vol-claim From dc9a21a5013501abb7bc13c93aba0de7de54049c Mon Sep 17 00:00:00 2001 From: graham-dds Date: Mon, 9 Dec 2019 09:20:31 -0500 Subject: [PATCH 054/111] Refactor mock reporting data and class methods All mock reporting data was moved to a JSON file. The concept of what JEDI CLIN a particular environment drew money from was added to the data. This change had a cascade effect to the reporting class methods, templates, and Vue components that ingested that reporting data. Many of these files were modified to adapt to these changes. This also included modifying the obligated funding bar graphs to reflect new design changes. --- atst/domain/csp/fixture_spend_data.json | 390 ++++++++++++++++ atst/domain/csp/reports.py | 436 +++++------------- atst/domain/reports.py | 29 +- atst/filters.py | 10 + atst/routes/portfolios/index.py | 9 +- js/components/tables/spend_table.js | 22 +- styles/core/_util.scss | 4 + styles/sections/_reports.scss | 73 ++- .../reports/application_and_env_spending.html | 31 +- .../reports/expired_task_orders.html | 10 +- .../portfolios/reports/obligated_funds.html | 62 ++- 11 files changed, 676 insertions(+), 400 deletions(-) create mode 100644 atst/domain/csp/fixture_spend_data.json diff --git a/atst/domain/csp/fixture_spend_data.json b/atst/domain/csp/fixture_spend_data.json new file mode 100644 index 00000000..2a59eadf --- /dev/null +++ b/atst/domain/csp/fixture_spend_data.json @@ -0,0 +1,390 @@ +{ + "A-Wing": { + "applications": [ + { + "name": "LC04", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 663, + "JEDI_CLIN_2": 397 + }, + "last_month": { + "JEDI_CLIN_1": 590, + "JEDI_CLIN_2": 829 + }, + "total": { + "JEDI_CLIN_1": 42467, + "JEDI_CLIN_2": 33873 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 1000, + "JEDI_CLIN_2": 626 + }, + "last_month": { + "JEDI_CLIN_1": 685, + "JEDI_CLIN_2": 331 + }, + "total": { + "JEDI_CLIN_1": 21874, + "JEDI_CLIN_2": 25506 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 856, + "JEDI_CLIN_2": 627 + }, + "last_month": { + "JEDI_CLIN_1": 921, + "JEDI_CLIN_2": 473 + }, + "total": { + "JEDI_CLIN_1": 35566, + "JEDI_CLIN_2": 42514 + } + } + } + ] + }, + { + "name": "SF18", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 777, + "JEDI_CLIN_2": 850 + }, + "last_month": { + "JEDI_CLIN_1": 584, + "JEDI_CLIN_2": 362 + }, + "total": { + "JEDI_CLIN_1": 44505, + "JEDI_CLIN_2": 21378 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 487, + "JEDI_CLIN_2": 733 + }, + "last_month": { + "JEDI_CLIN_1": 542, + "JEDI_CLIN_2": 999 + }, + "total": { + "JEDI_CLIN_1": 8713, + "JEDI_CLIN_2": 10586 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 420, + "JEDI_CLIN_2": 503 + }, + "last_month": { + "JEDI_CLIN_1": 756, + "JEDI_CLIN_2": 941 + }, + "total": { + "JEDI_CLIN_1": 43003, + "JEDI_CLIN_2": 20601 + } + } + } + ] + }, + { + "name": "Canton", + "environments": [ + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 661, + "JEDI_CLIN_2": 599 + }, + "last_month": { + "JEDI_CLIN_1": 962, + "JEDI_CLIN_2": 383 + }, + "total": { + "JEDI_CLIN_1": 24501, + "JEDI_CLIN_2": 7551 + } + } + } + ] + }, + { + "name": "BD04", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 790, + "JEDI_CLIN_2": 513 + }, + "last_month": { + "JEDI_CLIN_1": 886, + "JEDI_CLIN_2": 991 + }, + "total": { + "JEDI_CLIN_1": 43684, + "JEDI_CLIN_2": 40196 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 513, + "JEDI_CLIN_2": 706 + }, + "last_month": { + "JEDI_CLIN_1": 945, + "JEDI_CLIN_2": 380 + }, + "total": { + "JEDI_CLIN_1": 28189, + "JEDI_CLIN_2": 9759 + } + } + } + ] + }, + { + "name": "SCV18", + "environments": [ + { + "name": "Dev", + "spending": { + "this_month": { + "JEDI_CLIN_1": 933, + "JEDI_CLIN_2": 993 + }, + "last_month": { + "JEDI_CLIN_1": 319, + "JEDI_CLIN_2": 619 + }, + "total": { + "JEDI_CLIN_1": 40585, + "JEDI_CLIN_2": 28872 + } + } + } + ] + }, + { + "name": "Crown", + "environments": [ + { + "name": "CR Portal Dev", + "spending": { + "this_month": { + "JEDI_CLIN_1": 711, + "JEDI_CLIN_2": 413 + }, + "last_month": { + "JEDI_CLIN_1": 908, + "JEDI_CLIN_2": 632 + }, + "total": { + "JEDI_CLIN_1": 18753, + "JEDI_CLIN_2": 4004 + } + } + }, + { + "name": "CR Staging", + "spending": { + "this_month": { + "JEDI_CLIN_1": 440, + "JEDI_CLIN_2": 918 + }, + "last_month": { + "JEDI_CLIN_1": 370, + "JEDI_CLIN_2": 472 + }, + "total": { + "JEDI_CLIN_1": 40602, + "JEDI_CLIN_2": 6834 + } + } + }, + { + "name": "CR Portal Test 1", + "spending": { + "this_month": { + "JEDI_CLIN_1": 928, + "JEDI_CLIN_2": 796 + }, + "last_month": { + "JEDI_CLIN_1": 680, + "JEDI_CLIN_2": 312 + }, + "total": { + "JEDI_CLIN_1": 36058, + "JEDI_CLIN_2": 42375 + } + } + }, + { + "name": "Jewels Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 304, + "JEDI_CLIN_2": 428 + }, + "last_month": { + "JEDI_CLIN_1": 898, + "JEDI_CLIN_2": 729 + }, + "total": { + "JEDI_CLIN_1": 3162, + "JEDI_CLIN_2": 49836 + } + } + }, + { + "name": "Jewels Dev", + "spending": { + "this_month": { + "JEDI_CLIN_1": 498, + "JEDI_CLIN_2": 890 + }, + "last_month": { + "JEDI_CLIN_1": 506, + "JEDI_CLIN_2": 659 + }, + "total": { + "JEDI_CLIN_1": 6248, + "JEDI_CLIN_2": 3866 + } + } + } + ] + } + ] + }, + "B-Wing": { + "applications": [ + { + "name": "NP02", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 455, + "JEDI_CLIN_2": 746 + }, + "last_month": { + "JEDI_CLIN_1": 973, + "JEDI_CLIN_2": 504 + }, + "total": { + "JEDI_CLIN_1": 11493, + "JEDI_CLIN_2": 17751 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 582, + "JEDI_CLIN_2": 339 + }, + "last_month": { + "JEDI_CLIN_1": 392, + "JEDI_CLIN_2": 885 + }, + "total": { + "JEDI_CLIN_1": 41856, + "JEDI_CLIN_2": 46399 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 446, + "JEDI_CLIN_2": 670 + }, + "last_month": { + "JEDI_CLIN_1": 368, + "JEDI_CLIN_2": 963 + }, + "total": { + "JEDI_CLIN_1": 10030, + "JEDI_CLIN_2": 29253 + } + } + } + ] + }, + { + "name": "FM", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 994, + "JEDI_CLIN_2": 573 + }, + "last_month": { + "JEDI_CLIN_1": 699, + "JEDI_CLIN_2": 418 + }, + "total": { + "JEDI_CLIN_1": 27881, + "JEDI_CLIN_2": 37092 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 838, + "JEDI_CLIN_2": 839 + }, + "last_month": { + "JEDI_CLIN_1": 775, + "JEDI_CLIN_2": 946 + }, + "total": { + "JEDI_CLIN_1": 45007, + "JEDI_CLIN_2": 16197 + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index 4b69e904..9949050e 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -1,344 +1,124 @@ -from itertools import groupby -import pendulum +from collections import defaultdict +import json +import os from decimal import Decimal -from collections import OrderedDict -class ReportingInterface: - def monthly_totals_for_environment(environment): - """Return the monthly totals for the specified environment. +def load_fixture_data(): + with open( + os.path.join(os.path.dirname(__file__), "fixture_spend_data.json"), "r" + ) as json_file: + return json.load(json_file) - Data should be in the format of a dictionary with the month as the key - and the spend in that month as the value. For example: - { "01/2018": 79.85, "02/2018": 86.54 } +class MockReportingProvider: + FIXTURE_SPEND_DATA = load_fixture_data() + @classmethod + def get_portfolio_monthly_spending(cls, portfolio): """ - raise NotImplementedError() - - -class MockEnvironment: - def __init__(self, id_, env_name): - self.id = id_ - self.name = env_name - - -class MockApplication: - def __init__(self, application_name, envs): - def make_env(name): - return MockEnvironment("{}_{}".format(application_name, name), name) - - self.name = application_name - self.environments = [make_env(env_name) for env_name in envs] - - -def generate_sample_dates(_max=8): - current = pendulum.now() - sample_dates = [] - for _i in range(_max): - sample_dates.append(current.strftime("%m/%Y")) - current = current.subtract(months=1) - - reversed(sample_dates) - return sample_dates - - -class MockReportingProvider(ReportingInterface): - MOCK_PERCENT_EXPENDED_FUNDS = 0.75 - - FIXTURE_MONTHS = generate_sample_dates() - - MONTHLY_SPEND_BY_ENVIRONMENT = { - "LC04_Integ": { - FIXTURE_MONTHS[7]: 284, - FIXTURE_MONTHS[6]: 1210, - FIXTURE_MONTHS[5]: 1430, - FIXTURE_MONTHS[4]: 1366, - FIXTURE_MONTHS[3]: 1169, - FIXTURE_MONTHS[2]: 991, - FIXTURE_MONTHS[1]: 978, - FIXTURE_MONTHS[0]: 737, - }, - "LC04_PreProd": { - FIXTURE_MONTHS[7]: 812, - FIXTURE_MONTHS[6]: 1389, - FIXTURE_MONTHS[5]: 1425, - FIXTURE_MONTHS[4]: 1306, - FIXTURE_MONTHS[3]: 1112, - FIXTURE_MONTHS[2]: 936, - FIXTURE_MONTHS[1]: 921, - FIXTURE_MONTHS[0]: 694, - }, - "LC04_Prod": { - FIXTURE_MONTHS[7]: 1742, - FIXTURE_MONTHS[6]: 1716, - FIXTURE_MONTHS[5]: 1866, - FIXTURE_MONTHS[4]: 1809, - FIXTURE_MONTHS[3]: 1839, - FIXTURE_MONTHS[2]: 1633, - FIXTURE_MONTHS[1]: 1654, - FIXTURE_MONTHS[0]: 1103, - }, - "SF18_Integ": { - FIXTURE_MONTHS[5]: 1498, - FIXTURE_MONTHS[4]: 1400, - FIXTURE_MONTHS[3]: 1394, - FIXTURE_MONTHS[2]: 1171, - FIXTURE_MONTHS[1]: 1200, - FIXTURE_MONTHS[0]: 963, - }, - "SF18_PreProd": { - FIXTURE_MONTHS[5]: 1780, - FIXTURE_MONTHS[4]: 1667, - FIXTURE_MONTHS[3]: 1703, - FIXTURE_MONTHS[2]: 1474, - FIXTURE_MONTHS[1]: 1441, - FIXTURE_MONTHS[0]: 933, - }, - "SF18_Prod": { - FIXTURE_MONTHS[5]: 1686, - FIXTURE_MONTHS[4]: 1779, - FIXTURE_MONTHS[3]: 1792, - FIXTURE_MONTHS[2]: 1570, - FIXTURE_MONTHS[1]: 1539, - FIXTURE_MONTHS[0]: 986, - }, - "Canton_Prod": { - FIXTURE_MONTHS[4]: 28699, - FIXTURE_MONTHS[3]: 26766, - FIXTURE_MONTHS[2]: 22619, - FIXTURE_MONTHS[1]: 24090, - FIXTURE_MONTHS[0]: 16719, - }, - "BD04_Integ": {}, - "BD04_PreProd": { - FIXTURE_MONTHS[7]: 7019, - FIXTURE_MONTHS[6]: 3004, - FIXTURE_MONTHS[5]: 2691, - FIXTURE_MONTHS[4]: 2901, - FIXTURE_MONTHS[3]: 3463, - FIXTURE_MONTHS[2]: 3314, - FIXTURE_MONTHS[1]: 3432, - FIXTURE_MONTHS[0]: 723, - }, - "SCV18_Dev": {FIXTURE_MONTHS[1]: 9797}, - "Crown_CR Portal Dev": { - FIXTURE_MONTHS[6]: 208, - FIXTURE_MONTHS[5]: 457, - FIXTURE_MONTHS[4]: 671, - FIXTURE_MONTHS[3]: 136, - FIXTURE_MONTHS[2]: 1524, - FIXTURE_MONTHS[1]: 2077, - FIXTURE_MONTHS[0]: 1858, - }, - "Crown_CR Staging": { - FIXTURE_MONTHS[6]: 208, - FIXTURE_MONTHS[5]: 457, - FIXTURE_MONTHS[4]: 671, - FIXTURE_MONTHS[3]: 136, - FIXTURE_MONTHS[2]: 1524, - FIXTURE_MONTHS[1]: 2077, - FIXTURE_MONTHS[0]: 1858, - }, - "Crown_CR Portal Test 1": { - FIXTURE_MONTHS[2]: 806, - FIXTURE_MONTHS[1]: 1966, - FIXTURE_MONTHS[0]: 2597, - }, - "Crown_Jewels Prod": { - FIXTURE_MONTHS[2]: 806, - FIXTURE_MONTHS[1]: 1966, - FIXTURE_MONTHS[0]: 2597, - }, - "Crown_Jewels Dev": { - FIXTURE_MONTHS[6]: 145, - FIXTURE_MONTHS[5]: 719, - FIXTURE_MONTHS[4]: 1243, - FIXTURE_MONTHS[3]: 2214, - FIXTURE_MONTHS[2]: 2959, - FIXTURE_MONTHS[1]: 4151, - FIXTURE_MONTHS[0]: 4260, - }, - "NP02_Integ": {FIXTURE_MONTHS[1]: 284, FIXTURE_MONTHS[0]: 1210}, - "NP02_PreProd": {FIXTURE_MONTHS[1]: 812, FIXTURE_MONTHS[0]: 1389}, - "NP02_Prod": {FIXTURE_MONTHS[1]: 3742, FIXTURE_MONTHS[0]: 4716}, - "FM_Integ": {FIXTURE_MONTHS[1]: 1498}, - "FM_Prod": {FIXTURE_MONTHS[0]: 5686}, - } - - REPORT_FIXTURE_MAP = { - "A-Wing": { - "applications": [ - MockApplication("LC04", ["Integ", "PreProd", "Prod"]), - MockApplication("SF18", ["Integ", "PreProd", "Prod"]), - MockApplication("Canton", ["Prod"]), - MockApplication("BD04", ["Integ", "PreProd"]), - MockApplication("SCV18", ["Dev"]), - MockApplication( - "Crown", - [ - "CR Portal Dev", - "CR Staging", - "CR Portal Test 1", - "Jewels Prod", - "Jewels Dev", - ], - ), - ], - "budget": 500_000, - }, - "B-Wing": { - "applications": [ - MockApplication("NP02", ["Integ", "PreProd", "Prod"]), - MockApplication("FM", ["Integ", "Prod"]), - ], - "budget": 70000, - }, - } - - def _rollup_application_totals(self, data): - application_totals = {} - for application, environments in data.items(): - application_spend = [ - (month, spend) - for env in environments.values() - if env - for month, spend in env.items() - ] - application_totals[application] = { - month: sum([spend[1] for spend in spends]) - for month, spends in groupby(sorted(application_spend), lambda x: x[0]) - } - - return application_totals - - def _rollup_portfolio_totals(self, application_totals): - monthly_spend = [ - (month, spend) - for application in application_totals.values() - for month, spend in application.items() - ] - portfolio_totals = {} - for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]): - portfolio_totals[month] = sum([spend[1] for spend in spends]) - - return portfolio_totals - - def monthly_totals_for_environment(self, environment_id): - """Return the monthly totals for the specified environment. - - Data should be in the format of a dictionary with the month as the key - and the spend in that month as the value. For example: - - { "01/2018": 79.85, "02/2018": 86.54 } - - """ - environment_monthly_totals = self.MONTHLY_SPEND_BY_ENVIRONMENT.get( - environment_id, {} - ).copy() - - environment_monthly_totals["total_spend_to_date"] = sum( - monthly_total for monthly_total in environment_monthly_totals.values() - ) - return environment_monthly_totals - - def monthly_totals(self, portfolio): - """Return month totals rolled up by environment, application, and portfolio. - - Data should returned with three top level keys, "portfolio", "applications", - and "environments". - The "applications" key will have budget data per month for each application, - The "environments" key will have budget data for each environment. - The "portfolio" key will be total monthly spending for the portfolio. - For example: - + returns an array of application and environment spending for the + portfolio. Applications and their nested environments are sorted in + alphabetical order by name. + [ { - "environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } }, - "applications": { "X-Wing": { "01/2018": 75.42 } }, - "portfolio": { "01/2018": 75.42 }, + name + this_month + last_month + total + environments [ + { + name + this_month + last_month + total + } + ] } - + ] """ - applications = portfolio.applications - if portfolio.name in self.REPORT_FIXTURE_MAP: - applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"] - environments = { - application.name: { - env.name: self.monthly_totals_for_environment(env.id) - for env in application.environments - } - for application in applications - } - - application_totals = self._rollup_application_totals(environments) - portfolio_totals = self._rollup_portfolio_totals(application_totals) - - return { - "environments": environments, - "applications": application_totals, - "portfolio": portfolio_totals, - } - - def get_obligated_funds_by_JEDI_clin(self, portfolio): - """ - Returns a dictionary of obligated funds and spending per JEDI CLIN - { - JEDI_CLIN: { - obligated_funds, - expended_funds - } - } - """ - if portfolio.name in self.REPORT_FIXTURE_MAP: - return_dict = {} - for jedi_clin, clins in groupby( - portfolio.active_clins, lambda clin: clin.jedi_clin_type - ): - obligated_funds = sum(clin.obligated_amount for clin in clins) - return_dict[jedi_clin.value] = { - "obligated_funds": obligated_funds, - "expended_funds": ( - obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) - ), - } - return OrderedDict( - # 0 index for dict item, -1 for last digit of 4 digit CLIN, e.g. 0001 - sorted(return_dict.items(), key=lambda clin: clin[0][-1]) + if portfolio.name in cls.FIXTURE_SPEND_DATA: + applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"] + return sorted( + [ + cls._get_application_monthly_totals(application) + for application in applications + ], + key=lambda app: app["name"], ) - return {} + return [] - def get_expired_task_orders(self, portfolio): - def sorted_task_orders(to_list): - return sorted(to_list, key=lambda to: to["number"]) + @classmethod + def _get_environment_monthly_totals(cls, environment): + """ + returns a dictionary that represents spending totals for an environment e.g. + { + name + this_month + last_month + total + } + """ + return { + "name": environment["name"], + "this_month": sum(environment["spending"]["this_month"].values()), + "last_month": sum(environment["spending"]["last_month"].values()), + "total": sum(environment["spending"]["total"].values()), + } - def sorted_clins(clin_list): - return sorted(clin_list, key=lambda clin: clin["number"]) - - def serialize_clin(clin): - return { - "number": clin.number, - "jedi_clin_type": clin.jedi_clin_type, - "period_of_performance": { - "start_date": clin.start_date, - "end_date": clin.end_date, - }, - "total_value": clin.total_amount, - "total_obligated_funds": clin.obligated_amount, - "expended_funds": ( - clin.obligated_amount * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) - ), + @classmethod + def _get_application_monthly_totals(cls, application): + """ + returns a dictionary that represents spending totals for an application + and its environments e.g. + { + name + this_month + last_month + total + environments: [ + { + name + this_month + last_month + total + } + ] } - - return sorted_task_orders( + """ + environments = sorted( [ - { - "id": task_order.id, - "number": task_order.number, - "clins": sorted_clins( - [serialize_clin(clin) for clin in task_order.clins] - ), - } - for task_order in portfolio.task_orders - if task_order.is_expired - ] + cls._get_environment_monthly_totals(env) + for env in application["environments"] + ], + key=lambda env: env["name"], ) + return { + "name": application["name"], + "this_month": sum(env["this_month"] for env in environments), + "last_month": sum(env["last_month"] for env in environments), + "total": sum(env["total"] for env in environments), + "environments": environments, + } + + @classmethod + def get_spending_by_JEDI_clin(cls, portfolio): + """ + returns an dictionary of spending per JEDI CLIN for a portfolio + { + jedi_clin: { + invoiced + estimated + }, + } + """ + if portfolio.name in cls.FIXTURE_SPEND_DATA: + CLIN_spend_dict = defaultdict(lambda: defaultdict(Decimal)) + for application in cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]: + for environment in application["environments"]: + for clin, spend in environment["spending"]["this_month"].items(): + CLIN_spend_dict[clin]["estimated"] += Decimal(spend) + for clin, spend in environment["spending"]["total"].items(): + CLIN_spend_dict[clin]["invoiced"] += Decimal(spend) + return CLIN_spend_dict + return {} diff --git a/atst/domain/reports.py b/atst/domain/reports.py index 94f6c54e..22760ec9 100644 --- a/atst/domain/reports.py +++ b/atst/domain/reports.py @@ -1,15 +1,36 @@ from flask import current_app +from itertools import groupby class Reports: @classmethod - def monthly_totals(cls, portfolio): - return current_app.csp.reports.monthly_totals(portfolio) + def monthly_spending(cls, portfolio): + return current_app.csp.reports.get_portfolio_monthly_spending(portfolio) @classmethod def expired_task_orders(cls, portfolio): - return current_app.csp.reports.get_expired_task_orders(portfolio) + return [ + task_order for task_order in portfolio.task_orders if task_order.is_expired + ] @classmethod def obligated_funds_by_JEDI_clin(cls, portfolio): - return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio) + clin_spending = current_app.csp.reports.get_spending_by_JEDI_clin(portfolio) + active_clins = portfolio.active_clins + for jedi_clin, clins in groupby( + active_clins, key=lambda clin: clin.jedi_clin_type + ): + if not clin_spending.get(jedi_clin.name): + clin_spending[jedi_clin.name] = {} + clin_spending[jedi_clin.name]["obligated"] = sum( + clin.obligated_amount for clin in clins + ) + return [ + { + "name": clin, + "invoiced": clin_spending[clin].get("invoiced", 0), + "estimated": clin_spending[clin].get("estimated", 0), + "obligated": clin_spending[clin].get("obligated", 0), + } + for clin in sorted(clin_spending.keys()) + ] diff --git a/atst/filters.py b/atst/filters.py index 48b8166c..3508f1e9 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -5,6 +5,7 @@ from flask import render_template from jinja2 import contextfilter from jinja2.exceptions import TemplateNotFound from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from decimal import DivisionByZero as DivisionByZeroException def iconSvg(name): @@ -38,6 +39,14 @@ def usPhone(number): return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:]) +def obligatedFundingGraphWidth(values): + numerator, denominator = values + try: + return (numerator / denominator) * 100 + except DivisionByZeroException: + return 0 + + def formattedDate(value, formatter="%m/%d/%Y"): if value: return value.strftime(formatter) @@ -76,6 +85,7 @@ def register_filters(app): app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["withExtraParams"] = with_extra_params + app.jinja_env.filters["obligatedFundingGraphWidth"] = obligatedFundingGraphWidth @contextfilter def translateWithoutCache(context, *kwargs): diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index a34d9057..8787c4f6 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timedelta +from datetime import datetime from flask import redirect, render_template, url_for, request as http_request, g @@ -35,9 +35,6 @@ def create_portfolio(): @user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports") def reports(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) - today = date.today() - current_month = date(int(today.year), int(today.month), 15) - prev_month = current_month - timedelta(days=28) # wrapped in str() because the sum of obligated funds returns a Decimal object total_portfolio_value = str( sum( @@ -51,10 +48,10 @@ def reports(portfolio_id): total_portfolio_value=total_portfolio_value, current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio), expired_task_orders=Reports.expired_task_orders(portfolio), - monthly_totals=Reports.monthly_totals(portfolio), + monthly_spending=Reports.monthly_spending(portfolio), current_month=current_month, prev_month=prev_month, - now=datetime.now(), # mocked datetime of reporting data retrival + retrieved=datetime.now(), # mocked datetime of reporting data retrival ) diff --git a/js/components/tables/spend_table.js b/js/components/tables/spend_table.js index c3a1c90f..bd5f5a01 100644 --- a/js/components/tables/spend_table.js +++ b/js/components/tables/spend_table.js @@ -1,14 +1,12 @@ import { set } from 'vue/dist/vue' import { formatDollars } from '../../lib/dollars' +import { set as _set } from 'lodash' export default { name: 'spend-table', props: { - applications: Object, - environments: Object, - currentMonthIndex: String, - prevMonthIndex: String, + applications: Array, }, data: function() { @@ -18,20 +16,16 @@ export default { }, created: function() { - Object.keys(this.applications).forEach(application => { - set(this.applicationsState[application], 'isVisible', false) + this.applicationsState.forEach(application => { + application.isVisible = false }) }, methods: { - toggle: function(e, applicationName) { - this.applicationsState = Object.assign(this.applicationsState, { - [applicationName]: Object.assign( - this.applicationsState[applicationName], - { - isVisible: !this.applicationsState[applicationName].isVisible, - } - ), + toggle: function(e, applicationIndex) { + set(this.applicationsState, applicationIndex, { + ...this.applicationsState[applicationIndex], + isVisible: !this.applicationsState[applicationIndex].isVisible, }) }, diff --git a/styles/core/_util.scss b/styles/core/_util.scss index b719d855..ff6e8e3f 100644 --- a/styles/core/_util.scss +++ b/styles/core/_util.scss @@ -73,6 +73,10 @@ color: $color-green; } +.text-danger { + color: $color-secondary; +} + .user-permission { font-weight: $font-normal; } diff --git a/styles/sections/_reports.scss b/styles/sections/_reports.scss index 0cd737c8..ee43ffa2 100644 --- a/styles/sections/_reports.scss +++ b/styles/sections/_reports.scss @@ -16,6 +16,16 @@ } .jedi-clin-funding { + $insufficient-gradient: repeating-linear-gradient( + 45deg, + $color-secondary-dark, + $color-secondary-dark 10px, + $color-secondary-darkest 11px, + $color-secondary-darkest 14px + ); + + $graph-bar-height: 2rem; + padding-top: $gap * 3; padding-bottom: $gap * 3; @@ -37,14 +47,36 @@ margin: 0; } - &__meter { - margin: 10px 0; - -moz-transform: scale(-1, 1); - -webkit-transform: scale(-1, 1); - -o-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); + &__graph { width: 100%; + height: $graph-bar-height; + margin-top: $gap * 2; + margin-bottom: $gap * 2; + display: flex; + + &-bar { + height: 100%; + display: block; + float: left; + margin-right: $gap / 2; + + &:last-child { + margin-right: 0; + } + + &--invoiced { + background: $color-green; + } + &--estimated { + background: $color-green-lighter; + } + &--remaining { + background: $color-primary-darkest; + } + &--insufficient { + background: $insufficient-gradient; + } + } &-values { display: flex; @@ -52,13 +84,32 @@ } &__meta { - &--remaining { - margin-left: auto; - text-align: right; - } + margin-right: $gap * 5; + &-header { @include small-copy; margin-bottom: 0; + display: flex; + align-items: center; + } + + &-key { + height: $graph-bar-height; + width: $graph-bar-height; + margin-right: $gap / 2; + + &--invoiced { + background: $color-green; + } + &--estimated { + background: $color-green-lighter; + } + &--remaining { + background: $color-primary-darkest; + } + &--insufficient { + background: $insufficient-gradient; + } } &-value { margin-bottom: 0; diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index 0af72112..c7d94f55 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -24,12 +24,7 @@ ) }} {% else %} - +
@@ -41,41 +36,41 @@ -