diff --git a/.circleci/config.yml b/.circleci/config.yml index 2851bce1..1b472716 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -135,6 +135,67 @@ jobs: - run: "docker tag ${AZURE_SERVER_NAME}/atat:atat-${CIRCLE_SHA1} ${AZURE_SERVER_NAME}/atat:latest" - run: "docker push ${AZURE_SERVER_NAME}/atat:latest" + integration-tests: + docker: + - image: docker:17.05.0-ce-git + steps: + - setup_remote_docker: + version: 18.06.0-ce + - checkout + - run: + name: Set up temporary docker network + command: docker network create atat + - run: + name: Build image + command: docker build . -t atat:latest + - run: + name: Get storage containers + command: docker pull postgres:latest && docker pull redis:latest + - run: + name: Start redis + command: docker run -d --network atat --link redis:redis -p 6379:6379 --name redis redis:latest + - run: + name: Start postgres + command: docker run -d --network atat --link postgres:postgres -p 5432:5432 --name postgres postgres:latest + - run: + name: Start application container + command: | + docker run -d \ + -e DISABLE_CRL_CHECK=true \ + -e PGHOST=postgres \ + -e REDIS_URI=redis://redis:6379 \ + -p 8000:8000 \ + --network atat \ + --name test-atat \ + atat:latest \ + uwsgi \ + --callable app \ + --module app \ + --plugin python3 \ + --virtualenv /opt/atat/atst/.venv \ + --http-socket :8000 + - run: + name: Wait for containers + command: sleep 3 + - run: + name: Create database + command: docker exec postgres createdb -U postgres atat + - run: + name: Apply migrations + command: docker exec test-atat .venv/bin/python .venv/bin/alembic upgrade head + - run: + name: Execute Ghost Inspector test suite + command: | + 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 + workflows: version: 2 run-tests: @@ -143,9 +204,12 @@ workflows: - test: requires: - app_setup - - azure-build-and-push-image: + - integration-tests: requires: - test + - azure-build-and-push-image: + requires: + - integration-tests filters: branches: only: @@ -204,7 +268,7 @@ workflows: repo: atat tag: "atat-${CIRCLE_SHA1},latest" requires: - - test + - integration-tests filters: branches: only: diff --git a/.gitignore b/.gitignore index 36a8261e..b50dc9ba 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,12 @@ ssl/client-certs/*.srl # je coverage output coverage - -# selenium testing +# BrowserStack browserstacklocal +# decompiled CircleCI yaml +local-ci.yml + # python config .python-version diff --git a/.secrets.baseline b/.secrets.baseline index 87e6414b..a71319f1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2019-10-03T14:34:50Z", + "generated_at": "2019-10-14T19:14:26Z", "plugins_used": [ { "base64_limit": 4.5, @@ -40,6 +40,13 @@ "is_verified": false, "line_number": 156, "type": "Secret Keyword" + }, + { + "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", + "is_secret": false, + "is_verified": false, + "line_number": 244, + "type": "Secret Keyword" } ], "alembic.ini": [ @@ -153,24 +160,6 @@ "type": "Private Key" } ], - "tests/acceptance/conftest.py": [ - { - "hashed_secret": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Basic Auth Credentials" - } - ], - "tests/fixtures/chain/make-chain.sh": [ - { - "hashed_secret": "bad2e396920ce37fe53fc291f90b130d915375fb", - "is_secret": false, - "is_verified": false, - "line_number": 35, - "type": "Secret Keyword" - } - ], "tests/forms/test_validators.py": [ { "hashed_secret": "260408f687da9094705a841acda9b029563053ee", @@ -194,7 +183,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 651, + "line_number": 656, "type": "Hex High Entropy String" } ] diff --git a/README.md b/README.md index bfb1a016..15a4f102 100644 --- a/README.md +++ b/README.md @@ -220,30 +220,37 @@ To generate coverage reports for the Javascript tests: yarn test:coverage -### Selenium Tests +### Ghost Inspector Tests -Selenium tests rely on BrowserStack. In order to run the Selenium tests -locally, you need BrowserStack credentials. The user email and key can -be found on the account settings page. To run the selenium tests: +AT-AT uses [Ghost Inpsector](https://app.ghostinspector.com/) for +integration testing. These tests do not run locally as part of the +regular test suite but do run in CI. To run them locally, 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: ``` -BROWSERSTACK_TOKEN= BROWSERSTACK_EMAIL= ./script/selenium_test +circleci config process .circleci/config.yml > local-ci.yml ``` -The selenium tests are in `tests/acceptance`. This directory is ignored by -pytest for normal test runs. - -The `selenium_test` script manages the setup of a separate database and -launching the BrowserStackLocal client. If you already have the client running -locally, you can run the selenium tests with: +Then run the job: ``` -BROWSERSTACK_TOKEN= BROWSERSTACK_EMAIL= pipenv run pytest tests/acceptance +circleci local execute -e GI_SUITE= -e GI_API_KEY= -e NGROK_TOKEN= --job integration-tests -c local-ci.yml ``` -The BrowserStack email is the one associated with the account. The token is -available in the BrowserStack profile information page. Go to the dashboard, -then "Account" > "Settings", then the token is under "Local Testing". +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 +``` ## Notes diff --git a/atst/domain/invitations.py b/atst/domain/invitations.py index fb889aec..663d2201 100644 --- a/atst/domain/invitations.py +++ b/atst/domain/invitations.py @@ -142,3 +142,9 @@ class PortfolioInvitations(BaseInvitations): class ApplicationInvitations(BaseInvitations): model = ApplicationInvitation role_domain_class = ApplicationRoles + + @classmethod + def revoke(cls, token): + invite = super().revoke(token) + ApplicationRoles.disable(invite.role) + return invite diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index 18b92603..8b92ca0f 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -183,11 +183,7 @@ def handle_create_member(application_id, form_data): token=invite.token, ) - flash( - "new_application_member", - user_name=invite.user_name, - application_name=application.name, - ) + flash("new_application_member", user_name=invite.first_name) except AlreadyExistsError: return render_template( diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 623306d5..5fbad688 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -8,9 +8,9 @@ MESSAGES = { "category": "success", }, "application_created": { - "title_template": translate("flash.success"), + "title_template": translate("flash.application.created.title"), "message_template": """ - {{ "flash.application.created" | translate({"application_name": application_name}) }} + {{ "flash.application.created.message" | translate({"application_name": application_name}) }} """, "category": "success", }, @@ -40,7 +40,7 @@ MESSAGES = { "category": "error", }, "application_invite_resent": { - "title_template": "Application invitation revoked", + "title_template": "Application invitation resent", "message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}", "category": "success", }, @@ -104,9 +104,9 @@ MESSAGES = { "category": "warning", }, "new_application_member": { - "title_template": translate("flash.success"), + "title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""", "message_template": """ -

{{ "flash.new_application_member" | translate({ "user_name": user_name, "application_name": application_name }) }}

+

{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}

""", "category": "success", }, diff --git a/js/components/forms/new_application/name_and_description.js b/js/components/forms/new_application/name_and_description.js index f39234f9..cabcd92c 100644 --- a/js/components/forms/new_application/name_and_description.js +++ b/js/components/forms/new_application/name_and_description.js @@ -10,6 +10,10 @@ export default { components: { textinput, }, + created: function() { + this.$root.$on('field-change', this.handleFieldChange) + if (this.initialData) this.changed = true + }, data: function() { return { diff --git a/static/icons/applications.svg b/static/icons/applications.svg index d0768feb..b747f80b 100644 --- a/static/icons/applications.svg +++ b/static/icons/applications.svg @@ -1,13 +1 @@ - - - - Combined Shape - Created with Sketch. - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/static/icons/funding.svg b/static/icons/funding.svg index a765aeb7..ae7cf81e 100644 --- a/static/icons/funding.svg +++ b/static/icons/funding.svg @@ -1,13 +1 @@ - - - - Shape - Created with Sketch. - - - - - - - - \ No newline at end of file + diff --git a/styles/components/_footer.scss b/styles/components/_footer.scss index e5975115..2b7fc552 100644 --- a/styles/components/_footer.scss +++ b/styles/components/_footer.scss @@ -1,4 +1,5 @@ .app-footer { + z-index: 3; background-color: $color-white; border-top: 1px solid $color-gray-lightest; display: flex; diff --git a/styles/components/_global_navigation.scss b/styles/components/_global_navigation.scss index 75daa768..63bf0d41 100644 --- a/styles/components/_global_navigation.scss +++ b/styles/components/_global_navigation.scss @@ -1,6 +1,9 @@ .global-navigation { + z-index: 2; background-color: $color-white; height: auto; + box-shadow: $box-shadow; + margin-bottom: -$footer-height * 2.5; .sidenav__link { padding-right: $gap * 2; diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index c1af4db4..37658ac1 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -29,9 +29,15 @@ &__name { @include h1; h1 { - margin: ($gap * 2) $gap ($gap * 2) $gap; + margin: 0 $gap ($gap * 2) 0; font-size: 3.5rem; } + + p { + font-size: $small-font-size; + margin: 0 0 (-$gap * 0.5); + color: $color-gray-medium; + } } &__budget { @@ -69,17 +75,26 @@ font-size: $small-font-size; .icon-link { - padding: 0.8rem 1.6rem; + padding: $gap; + border-radius: 0; + color: $color-blue-darkest; + + &:hover { + background-color: $color-aqua-lightest; + } - color: $color-gray; - opacity: 0.3; .icon { - @include icon-color($color-gray); + @include icon-color($color-blue-darkest); } &.active { - opacity: 1; color: $color-blue; + background-color: $color-gray-lightest; + + &:hover { + background-color: $color-aqua-lightest; + } + .icon { @include icon-color($color-blue); } @@ -121,7 +136,7 @@ } .portfolio-content { - margin: 1 * $gap $gap 0 $gap; + margin: (4 * $gap) $gap 0 $gap; .panel { @include shadow-panel; @@ -133,11 +148,6 @@ padding-bottom: 0; } - .subheading { - font-size: 1.4rem; - color: $color-gray; - } - .responsive-table-wrapper { padding-bottom: $gap * 3; margin-bottom: 0; @@ -219,6 +229,8 @@ } .application-content { + margin-top: $gap * 4; + .subheading { @include subheading; position: relative; @@ -247,7 +259,6 @@ textarea { max-height: 9rem; - max-width: none; } .panel__footer { diff --git a/styles/core/_grid.scss b/styles/core/_grid.scss index f7037d42..ff07cca6 100644 --- a/styles/core/_grid.scss +++ b/styles/core/_grid.scss @@ -44,4 +44,9 @@ flex-grow: 1; padding-right: $spacing-small; } + + &.col--half { + width: 50%; + max-width: 30em; + } } diff --git a/styles/elements/_action_group.scss b/styles/elements/_action_group.scss index 379c2642..1f79f7d2 100644 --- a/styles/elements/_action_group.scss +++ b/styles/elements/_action_group.scss @@ -29,3 +29,24 @@ } } } + +.action-group-footer { + @extend .action-group; + + &:last-child { + margin-bottom: 0; + } + margin-top: 0; + margin-bottom: 0; + padding-top: $gap; + padding-bottom: $gap; + + position: fixed; + bottom: $footer-height; + background: white; + right: 0; + padding-right: $gap * 4; + border-top: 1px solid $color-gray-light; + width: 100%; + z-index: 1; +} diff --git a/styles/elements/_inputs.scss b/styles/elements/_inputs.scss index eeef4cc8..47cc4f3c 100644 --- a/styles/elements/_inputs.scss +++ b/styles/elements/_inputs.scss @@ -57,13 +57,9 @@ } .usa-input { - margin: ($gap * 4) ($gap * 2) ($gap * 4) 0; + margin: ($gap * 2) 0; max-width: 75rem; - @include media($medium-screen) { - margin: ($gap * 4) 0; - } - label { padding: 0 0 ($gap / 2) 0; margin: 0; @@ -363,6 +359,8 @@ select { margin-right: $gap * 4; .usa-input { + margin: 0; + input, label, .usa-input__message { @@ -379,6 +377,8 @@ select { margin-left: $gap * 4; .usa-input { + margin: 0; + input { max-width: 12rem; } diff --git a/styles/sections/_application_edit.scss b/styles/sections/_application_edit.scss index 1d6de7fd..9ee0e671 100644 --- a/styles/sections/_application_edit.scss +++ b/styles/sections/_application_edit.scss @@ -18,7 +18,6 @@ } .usa-input { - margin: 0 ($gap * 4) 0 0; flex-grow: 2; } } @@ -28,6 +27,10 @@ .form-content--app-mem { text-align: left; + .modal__body { + min-width: 75rem; + } + input[type="checkbox"] + label::before { margin-left: 0; } @@ -58,11 +61,8 @@ } } - .form-row { - margin-top: 0; - + .application-member__user-info { .usa-input { - margin: 0; width: 45rem; input, diff --git a/templates/applications/base.html b/templates/applications/base.html index a8ef0c7c..4f47e9ad 100644 --- a/templates/applications/base.html +++ b/templates/applications/base.html @@ -5,7 +5,7 @@ {% block portfolio_header %} {% include "portfolios/header.html" %} {% if application %} - {{ StickyCTA(text=application.name, return_link_url=url_for('applications.portfolio_applications', portfolio_id=application.portfolio_id), return_link_text="BACK TO APPLICATIONS") }} + {{ StickyCTA(text=application.name) }} {% endif %} {% endblock %} diff --git a/templates/applications/fragments/member_form_fields.html b/templates/applications/fragments/member_form_fields.html index 96c6966b..c4d7d8c8 100644 --- a/templates/applications/fragments/member_form_fields.html +++ b/templates/applications/fragments/member_form_fields.html @@ -52,20 +52,12 @@ {% endmacro %} {% macro InfoFields(member_form) %} -
+ -
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }} -
-
{{ TextInput(member_form.email, validation='email', optional=False) }} -
-
{{ PhoneInput(member_form.phone_number, member_form.phone_ext)}} -
-
{{ TextInput(member_form.dod_id, validation='dodId', optional=False) }} + How do I find the DoD ID?
- How do I find the DoD ID? {% endmacro %} diff --git a/templates/applications/new/step_1.html b/templates/applications/new/step_1.html index e6a2bd54..385d80de 100644 --- a/templates/applications/new/step_1.html +++ b/templates/applications/new/step_1.html @@ -23,29 +23,28 @@
-
- {{ form.csrf_token }} -
-
- {{ TextInput(form.name, optional=False) }} - {{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }} -
-
-
-
-
- {{ TextInput(form.description, paragraph=True, optional=True) }} - {{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }} -
-
+ {{ form.csrf_token }} +
+
+ {{ TextInput(form.name, optional=False) }} + {{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
- +
+
+
+
+ {{ TextInput(form.description, paragraph=True, optional=True) }} + {{ ('portfolios.applications.new.step_1_form_help_text.description' | translate | safe) }} +
+
- + + {% block next_button %} {{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }} {% endblock %} - + + Cancel diff --git a/templates/applications/new/step_2.html b/templates/applications/new/step_2.html index 3f876b46..bb622f8f 100644 --- a/templates/applications/new/step_2.html +++ b/templates/applications/new/step_2.html @@ -16,66 +16,63 @@ {% set modalName = "newApplicationConfirmation" %} {% include "fragments/flash.html" %} -
-

- {{ 'portfolios.applications.new.step_2_description' | translate }} -

-
- - -
{{ 'portfolios.applications.environments_heading' | translate }}
-
-
- {{ form.csrf_token }} -
{# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} -
- {{ Alert(message=None, level="error", vue_template=True) }} -
+

+ {{ 'portfolios.applications.new.step_2_description' | translate }} +

+
+ + +
{{ 'portfolios.applications.environments_heading' | translate }}
+
+
+ {{ form.csrf_token }} +
{# this extra div prevents this bug: https://www.pivotaltracker.com/story/show/160768940 #} +
+ {{ Alert(message=None, level="error", vue_template=True) }}
- -
-
    -
  • -
    - - -
    -
    - -
    -
  • -
- - -
-
-
- - - {% block next_button %} - {{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }} - {% endblock %} - - Previous - - - Cancel - - - - -
+
+ +
+
    +
  • +
    + + +
    +
    + +
    +
  • +
+ + +
+
+
+ + {% block next_button %} + {{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }} + {% endblock %} + + Previous + + + Cancel + + + +
{% endblock %} diff --git a/templates/applications/new/step_3.html b/templates/applications/new/step_3.html index 783af8d3..cdf458e4 100644 --- a/templates/applications/new/step_3.html +++ b/templates/applications/new/step_3.html @@ -11,31 +11,30 @@ {% block application_content %} {% include "fragments/flash.html" %} -
-

- {{ ('portfolios.applications.new.step_3_description' | translate) }} -

-
- - {{ MemberManagementTemplate( - application, - members, - new_member_form, - "applications.update_new_application_step_3", - user_can(permissions.CREATE_APPLICATION_MEMBER)) }} - +

+ {{ ('portfolios.applications.new.step_3_description' | translate) }} +

+
+ + {{ MemberManagementTemplate( + application, + members, + new_member_form, + "applications.update_new_application_step_3", + user_can(permissions.CREATE_APPLICATION_MEMBER)) }} + + + + + Return to Application Settings + + + Previous + + + Cancel + + - - - Return to Application Settings - - - Previous - - - Cancel - - -
{% endblock %} diff --git a/templates/applications/settings.html b/templates/applications/settings.html index 362d4e7b..58a5b17d 100644 --- a/templates/applications/settings.html +++ b/templates/applications/settings.html @@ -19,58 +19,40 @@ {% block application_content %} -
{{ 'portfolios.applications.settings.name_description' | translate }}
+

{{ 'portfolios.applications.settings.name_description' | translate }}

{% if user_can(permissions.EDIT_APPLICATION) %} -
-
-
- {{ application_form.csrf_token }} -
-
- {{ TextInput(application_form.name, optional=False) }} - {{ TextInput(application_form.description, paragraph=True, optional=True) }} -
-
-
- + + {{ application_form.csrf_token }} + {{ TextInput(application_form.name, optional=False) }} + {{ TextInput(application_form.description, paragraph=True, optional=True, showOptional=False) }} +
+ {{ SaveButton('common.save_changes'|translate) }}
{% else %} -
-
-

- {{ "fragments.edit_application_form.explain" | translate }} -

-
-
-
- {{ application_form.name.label() }} -
-

- {{ application_form.name.data }} -

-
-
-
-
-
- {{ application_form.description.label() }} -
-

- {{ application_form.description.data }} -

-
-
+ +
+

+ {{ "fragments.edit_application_form.explain" | translate }} +

+
+ {{ application_form.name.label() }}
+

+ {{ application_form.name.data }} +

+
+ {{ application_form.description.label() }} +
+

+ {{ application_form.description.data }} +

{% endif %} +
{{ MemberManagementTemplate( application, diff --git a/templates/components/alert.html b/templates/components/alert.html index 8406a3da..49f148b9 100644 --- a/templates/components/alert.html +++ b/templates/components/alert.html @@ -27,7 +27,7 @@ {% if vue_template %}

{% elif title %} -

{{title}}

+

{{ title | safe }}

{% endif %} {% if message %} diff --git a/templates/components/sticky_cta.html b/templates/components/sticky_cta.html index 3d66dd6a..3def43ee 100644 --- a/templates/components/sticky_cta.html +++ b/templates/components/sticky_cta.html @@ -1,14 +1,7 @@ {% from 'components/icon.html' import Icon %} -{% macro StickyCTA(text, context=None, return_link_url=None, return_link_text=None) -%} +{% macro StickyCTA(text, context=None) -%}
- {% if return_link_url and return_link_text %} - - {% endif %}

{{ text }}

diff --git a/templates/components/text_input.html b/templates/components/text_input.html index 9b9cb279..69ebd727 100644 --- a/templates/components/text_input.html +++ b/templates/components/text_input.html @@ -15,6 +15,7 @@ classes='', noMaxWidth=False, optional=True, + showOptional=True, showLabel=True, watch=False, show_validation=True) -%} @@ -40,7 +41,7 @@