mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-04 08:52:16 +02:00
merged and resolved conflict
This commit is contained in:
commit
829b2dd1cf
37 changed files with 1069 additions and 464 deletions
|
@ -83,6 +83,6 @@ export GPG_TTY
|
|||
|
||||
## Setting up developer sandbox
|
||||
|
||||
We have two types of environments: stable, and sandbox. Stable gets deployed via tagged release every sprint, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
|
||||
We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
|
||||
|
||||
All automation for setting up a developer sandbox is documented in the scripts for [creating a developer sandbox](../../ops/scripts/create_dev_sandbox.sh) and [removing a developer sandbox](../../ops/scripts/destroy_dev_sandbox.sh). A Cloud.gov organization administrator will have to perform the script in order to create the sandbox.
|
||||
|
|
9
.github/workflows/deploy-sandbox.yaml
vendored
9
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -9,15 +9,12 @@ on:
|
|||
jobs:
|
||||
variables:
|
||||
if: |
|
||||
startsWith(github.head_ref, 'ik/')
|
||||
|| startsWith(github.head_ref, 'jon')
|
||||
|| startsWith(github.head_ref, 'sspj/')
|
||||
|| startsWith(github.head_ref, 'mr/')
|
||||
|| startsWith(github.head_ref, 'nmb/')
|
||||
|| startsWith(github.head_ref, 'ab/')
|
||||
startsWith(github.head_ref, 'ab/')
|
||||
|| startsWith(github.head_ref, 'bl/')
|
||||
|| startsWith(github.head_ref, 'rjm/')
|
||||
|| startsWith(github.head_ref, 'rb/')
|
||||
|| startsWith(github.head_ref, 'ko/')
|
||||
|| startsWith(github.head_ref, 'gd/')
|
||||
outputs:
|
||||
environment: ${{ steps.var.outputs.environment}}
|
||||
runs-on: "ubuntu-latest"
|
||||
|
|
41
.github/workflows/deploy-staging.yaml
vendored
Normal file
41
.github/workflows/deploy-staging.yaml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
# This workflow runs on pushes of tagged commits.
|
||||
# "Releases" of tagged commits will deploy selected branch to staging.
|
||||
|
||||
name: Build and deploy staging for tagged release
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
- '.gitignore'
|
||||
|
||||
tags:
|
||||
- staging-*
|
||||
|
||||
jobs:
|
||||
deploy-staging:
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Compile USWDS assets
|
||||
working-directory: ./src
|
||||
run: |
|
||||
docker compose run node npm install &&
|
||||
docker compose run node npx gulp copyAssets &&
|
||||
docker compose run node npx gulp compile
|
||||
- name: Collect static assets
|
||||
working-directory: ./src
|
||||
run: docker compose run app python manage.py collectstatic --no-input
|
||||
- name: Deploy to cloud.gov sandbox
|
||||
uses: 18f/cg-deploy-action@main
|
||||
env:
|
||||
DEPLOY_NOW: thanks
|
||||
with:
|
||||
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
|
||||
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
|
||||
cf_org: cisa-getgov-prototyping
|
||||
cf_space: staging
|
||||
push_arguments: "-f ops/manifests/manifest-staging.yaml"
|
8
.github/workflows/migrate.yaml
vendored
8
.github/workflows/migrate.yaml
vendored
|
@ -14,15 +14,13 @@ on:
|
|||
description: Which environment should we run migrations for?
|
||||
options:
|
||||
- stable
|
||||
- staging
|
||||
- gd
|
||||
- rb
|
||||
- ko
|
||||
- ab
|
||||
- bl
|
||||
- rjm
|
||||
- jon
|
||||
- ik
|
||||
- sspj
|
||||
- nmb
|
||||
- mr
|
||||
|
||||
jobs:
|
||||
migrate:
|
||||
|
|
8
.github/workflows/reset-db.yaml
vendored
8
.github/workflows/reset-db.yaml
vendored
|
@ -15,15 +15,13 @@ on:
|
|||
description: Which environment should we flush and re-load data for?
|
||||
options:
|
||||
- stable
|
||||
- staging
|
||||
- gd
|
||||
- rb
|
||||
- ko
|
||||
- ab
|
||||
- bl
|
||||
- rjm
|
||||
- jon
|
||||
- ik
|
||||
- sspj
|
||||
- nmb
|
||||
- mr
|
||||
|
||||
jobs:
|
||||
reset-db:
|
||||
|
|
|
@ -42,10 +42,9 @@ Optionally, load data from fixtures as well
|
|||
cf run-task getgov-ENVIRONMENT --wait --command 'python manage.py load' --name loaddata
|
||||
```
|
||||
|
||||
For the `stable` environment, developers don't have credentials so we need to
|
||||
run that command using Github Actions. Go to
|
||||
For the `stable` or `staging` environments, developers don't have credentials so we need to run that command using Github Actions. Go to
|
||||
<https://github.com/cisagov/getgov/actions/workflows/migrate.yaml> and select
|
||||
the "Run workflow" button, making sure that `stable` is selected.
|
||||
the "Run workflow" button, making sure that `stable` or `staging` depending on which envirornment you desire to update.
|
||||
|
||||
## Getting data for fixtures
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# Operations
|
||||
========================
|
||||
|
||||
Some basic information and setup steps are included in this README.
|
||||
|
||||
|
@ -36,7 +35,9 @@ Binding the database in `manifest-<ENVIRONMENT>.json` automatically inserts the
|
|||
|
||||
# Deploy
|
||||
|
||||
We have two types of environments: developer "sandboxes" and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `stable`, and it does so when we make tagged releases of `main`. This is to ensure that we have a "golden" environment to point to, and can still test things out in a sandbox space. You should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`:
|
||||
We have three types of environments: developer "sandboxes", `staging` and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `staging` and `stable`, and it does so when we make tagged releases of `main`. For `staging`, this is done to ensure there is a non-production level test envirornment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`.
|
||||
|
||||
You should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`:
|
||||
|
||||
For ease of use, you can run the `deploy.sh <sandbox name>` script in the `/src` directory to build the assets and deploy to your sandbox. Similarly, you could run `build.sh <sandbox name>` script to just compile and collect the assets without deploying.
|
||||
|
||||
|
@ -46,3 +47,114 @@ Your sandbox space should've been setup as part of the onboarding process. If th
|
|||
We are using [WhiteNoise](http://whitenoise.evans.io/en/stable/index.html) plugin to serve our static assets on cloud.gov. This plugin is added to the `MIDDLEWARE` list in our apps `settings.py`.
|
||||
|
||||
Note that it’s a good idea to run `collectstatic` locally or in the docker container before pushing files up to your sandbox. This is because `collectstatic` relies on timestamps when deciding to whether to overwrite the existing assets in `/public`. Due the way files are uploaded, the compiled css in the `/assets/css` folder on your sandbox will have a slightly earlier timestamp than the files in `/public/css`, and consequently running `collectstatic` on your sandbox will not update `public/css` as you may expect. For convenience, both the `deploy.sh` and `build.sh` scripts will take care of that.
|
||||
|
||||
# Debugging
|
||||
|
||||
Debugging errors observed in applications running on Cloud.gov requires being
|
||||
able to see the log information from the environment that the application is
|
||||
running in. There are (at least) three different ways to see that information:
|
||||
Cloud.gov dashboard, CloudFoundry CLI application, and Cloud.gov Kibana logging
|
||||
queries. There is also SSH access into Cloud.gov containers and Github Actions
|
||||
that can be used for specific tasks.
|
||||
|
||||
## Cloud.gov dashboard
|
||||
|
||||
At <https://dashboard.fr.cloud.gov/applications> there is a list for all of the
|
||||
applications that a Cloud.gov user has access to. Clicking on an application
|
||||
goes to a screen for that individual application, e.g.
|
||||
<https://dashboard.fr.cloud.gov/applications/2oBn9LBurIXUNpfmtZCQTCHnxUM/53b88024-1492-46aa-8fb6-1429bdb35f95/summary>.
|
||||
On that page is a left-hand link for "Log Stream" e.g.
|
||||
<https://dashboard.fr.cloud.gov/applications/2oBn9LBurIXUNpfmtZCQTCHnxUM/53b88024-1492-46aa-8fb6-1429bdb35f95/log-stream>.
|
||||
That log stream shows a stream of Cloud.gov log messages. Cloud.gov has
|
||||
different layers that log requests. One is `RTR` which is the router within
|
||||
Cloud.gov. Messages from our Django app are prefixed with `APP/PROC/WEB`. While
|
||||
it is possible to search inside the browser for particular log messages, this
|
||||
is not a sophisticated interface for querying logs.
|
||||
|
||||
## CloudFoundry CLI
|
||||
|
||||
When logged in with the CloudFoundry CLI (see
|
||||
[above](#authenticating-to-cloudgov-via-the-command-line)) Cloudfoundry
|
||||
application logs can be viewed with the `cf logs <application>` where
|
||||
`<application>` is the name of the application in the currently targeted space.
|
||||
By default `cf logs` starts a streaming view of log messages from the
|
||||
application. It appears to show the same information as the dashboard web
|
||||
application, but in the terminal. There is a `--recent` option that will dump
|
||||
things that happened prior to the current time rather than starting a stream of
|
||||
the present log messages, but that is also not a full log archive and search
|
||||
system.
|
||||
|
||||
CloudFoundry also offers a `run-task` command that can be used to run a single
|
||||
command in the running Cloud.gov container. For example, to run our Django
|
||||
admin command that loads test fixture data:
|
||||
|
||||
```
|
||||
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures
|
||||
```
|
||||
|
||||
However, this task runs asynchronously in the background without any command
|
||||
output, so it can sometimes be hard to know if the command has completed and if
|
||||
so, if it was successful.
|
||||
|
||||
## Cloud.gov Kibana
|
||||
|
||||
Cloud.gov provides an instance of the log query program Kibana at
|
||||
<https://logs.fr.cloud.gov>. Kibana is powerful, but also complicated software
|
||||
that can take time to learn how to use most effectively. A few hints:
|
||||
|
||||
- Set the timeframe of the display appropriately, the default is the last
|
||||
15 minutes which may not show any results in some environments.
|
||||
|
||||
- Kibana queries and filters can be used to narrow in on particular
|
||||
environments. Try the query `@source.type:APP` to focus on messages from the
|
||||
Django application or `@cf.app:"getgov-{environment}"` to see results from a single
|
||||
environment.
|
||||
|
||||
Currently, our application emits Python's default log format which is textual
|
||||
and not record-based. In particular, tracebacks are on multiple lines and show
|
||||
up in Kibana as multiple records that are not necessarily connected. As the
|
||||
application gets closer to production, we may want to switch to a JSON log format
|
||||
where errors will be captured by Kibana as a single message, however with a
|
||||
slightly more difficult developer experience when reading logs by eyeball.
|
||||
|
||||
|
||||
## SSH access
|
||||
|
||||
The CloudFoundry CLI provides SSH access to the running container of an
|
||||
application. Use `cf ssh <application>` to SSH into the container. To make sure
|
||||
that your shell is seeing the same configuration as the running application, be
|
||||
sure to run `/tmp/lifecycle/shell` very first.
|
||||
|
||||
Inside the container, the python code should be in `/app` and you can check
|
||||
there to see if the expected version of code is deployed in a particular file.
|
||||
There is no hot-reloading inside the container, so it isn't possible to make
|
||||
code changes there and see the results reflected in the running application.
|
||||
(Templates may be read directly from disk every page load so it is possible
|
||||
that you could change a page template and see the result in the application.)
|
||||
|
||||
Inside the container, it can be useful to run various Django admin commands
|
||||
using `./manage.py`. For example, `./manage.py shell` can be used to give a
|
||||
python interpreter where code can be run to modify objects in the database, say
|
||||
to make a user an administrator.
|
||||
|
||||
## Github Actions
|
||||
|
||||
In order to allow some ops activities by people without CloudFoundry on a
|
||||
laptop, we have some ops-related actions under
|
||||
<https://github.com/cisagov/getgov/actions>.
|
||||
|
||||
### Migrate data
|
||||
|
||||
This Github action runs Django's `manage.py migrate` command on the specified
|
||||
environment. **This is the first thing to try when fixing 500 errors from an
|
||||
application environment**. The migrations should be idempotent, so running the
|
||||
same migrations more than once should never cause an additional problem.
|
||||
|
||||
### Reset database
|
||||
|
||||
Very occasionally, there are migrations that don't succeed when run against a
|
||||
database with data already in it. This action drops the database and re-creates
|
||||
it with the latest model schema. Once launched, this should never be used on
|
||||
the `stable` environment, but during development, it may be useful on the
|
||||
various sandbox environments. After launch, some schema changes may take the
|
||||
involvement of a skilled DBA to fix problems like this.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-ik
|
||||
- name: getgov-gd
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
|
@ -17,13 +17,13 @@ applications:
|
|||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-ik.app.cloud.gov
|
||||
DJANGO_BASE_URL: https://getgov-gd.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# Public site base URL
|
||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
routes:
|
||||
- route: getgov-ik.app.cloud.gov
|
||||
- route: getgov-gd.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-ik-database
|
||||
- getgov-gd-database
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-nmb
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-nmb.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# Public site base URL
|
||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
||||
routes:
|
||||
- route: getgov-nmb.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-nmb-database
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-mr
|
||||
- name: getgov-rb
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
|
@ -17,13 +17,13 @@ applications:
|
|||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-mr.app.cloud.gov
|
||||
DJANGO_BASE_URL: https://getgov-rb.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# Public site base URL
|
||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
routes:
|
||||
- route: getgov-mr.app.cloud.gov
|
||||
- route: getgov-rb.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-mr-database
|
||||
- getgov-rb-database
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-sspj
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-sspj.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# Public site base URL
|
||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
||||
routes:
|
||||
- route: getgov-sspj.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-sspj-database
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-jon
|
||||
- name: getgov-staging
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
|
@ -17,13 +17,13 @@ applications:
|
|||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-jon.app.cloud.gov
|
||||
DJANGO_BASE_URL: https://getgov-staging.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# Public site base URL
|
||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
routes:
|
||||
- route: getgov-jon.app.cloud.gov
|
||||
- route: getgov-staging.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-jon-database
|
||||
- getgov-staging-database
|
|
@ -43,7 +43,7 @@ cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml
|
|||
sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml"
|
||||
|
||||
echo "Adding new environment to settings.py..."
|
||||
sed -i '' '/getgov-stable.app.cloud.gov/ {a\
|
||||
sed -i '' '/getgov-staging.app.cloud.gov/ {a\
|
||||
'\"getgov-$1.app.cloud.gov\"',
|
||||
}' src/registrar/config/settings.py
|
||||
|
||||
|
@ -65,7 +65,7 @@ done
|
|||
echo "Creating new cloud.gov credentials for $1..."
|
||||
django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')
|
||||
openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt
|
||||
login_key=$(base64 private-$1.pem)
|
||||
login_key=$(base64 -i private-$1.pem)
|
||||
jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json
|
||||
cf cups getgov-credentials -p credentials-$1.json
|
||||
|
||||
|
@ -105,11 +105,11 @@ echo
|
|||
echo "Moving on to setup Github automation..."
|
||||
|
||||
echo "Adding new environment to Github Actions..."
|
||||
sed -i '' '/ - stable/ {a\
|
||||
sed -i '' '/ - staging/ {a\
|
||||
- '"$1"'
|
||||
}' .github/workflows/reset-db.yaml
|
||||
|
||||
sed -i '' '/ - stable/ {a\
|
||||
sed -i '' '/ - staging/ {a\
|
||||
- '"$1"'
|
||||
}' .github/workflows/migrate.yaml
|
||||
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
{
|
||||
"defaults": {
|
||||
"concurrency": 1,
|
||||
"timeout": 30000,
|
||||
"hideElements": "a[href='/whoami/']"
|
||||
|
||||
"timeout": 30000
|
||||
},
|
||||
"urls": [
|
||||
"http://localhost:8080/",
|
||||
"http://localhost:8080/health/",
|
||||
"http://localhost:8080/whoami/",
|
||||
"http://localhost:8080/register/",
|
||||
"http://localhost:8080/register/organization/",
|
||||
"http://localhost:8080/register/org_federal/",
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
from django.contrib import admin
|
||||
import logging
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
|
||||
from . import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditedAdmin(admin.ModelAdmin):
|
||||
|
||||
|
@ -50,13 +52,65 @@ class MyHostAdmin(AuditedAdmin):
|
|||
inlines = [HostIPInline]
|
||||
|
||||
|
||||
class DomainAdmin(AuditedAdmin):
|
||||
|
||||
"""Custom domain admin class to add extra buttons."""
|
||||
|
||||
change_form_template = "django/admin/domain_change_form.html"
|
||||
readonly_fields = ["state"]
|
||||
|
||||
def response_change(self, request, obj):
|
||||
ACTION_BUTTON = "_place_client_hold"
|
||||
if ACTION_BUTTON in request.POST:
|
||||
try:
|
||||
obj.place_client_hold()
|
||||
except Exception as err:
|
||||
self.message_user(request, err, messages.ERROR)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
(
|
||||
"%s is in client hold. This domain is no longer accessible on"
|
||||
" the public internet."
|
||||
)
|
||||
% obj.name,
|
||||
)
|
||||
return HttpResponseRedirect(".")
|
||||
|
||||
return super().response_change(request, obj)
|
||||
|
||||
|
||||
class DomainApplicationAdmin(AuditedAdmin):
|
||||
|
||||
"""Customize the applications listing view."""
|
||||
|
||||
# Trigger action when a fieldset is changed
|
||||
def save_model(self, request, obj, form, change):
|
||||
if change: # Check if the application is being edited
|
||||
# Get the original application from the database
|
||||
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
||||
|
||||
if (
|
||||
obj.status != original_obj.status
|
||||
and obj.status == models.DomainApplication.INVESTIGATING
|
||||
):
|
||||
# This is a transition annotated method in model which will throw an
|
||||
# error if the condition is violated. To make this work, we need to
|
||||
# call it on the original object which has the right status value,
|
||||
# but pass the current object which contains the up-to-date data
|
||||
# for the email.
|
||||
original_obj.in_review(obj)
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
admin.site.register(models.User, MyUserAdmin)
|
||||
admin.site.register(models.UserDomainRole, AuditedAdmin)
|
||||
admin.site.register(models.Contact, AuditedAdmin)
|
||||
admin.site.register(models.DomainInvitation, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, AuditedAdmin)
|
||||
admin.site.register(models.DomainInformation, AuditedAdmin)
|
||||
admin.site.register(models.Domain, AuditedAdmin)
|
||||
admin.site.register(models.Domain, DomainAdmin)
|
||||
admin.site.register(models.Host, MyHostAdmin)
|
||||
admin.site.register(models.Nameserver, MyHostAdmin)
|
||||
admin.site.register(models.Website, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||
|
|
|
@ -564,15 +564,13 @@ SECURE_SSL_REDIRECT = True
|
|||
# web server configurations.
|
||||
ALLOWED_HOSTS = [
|
||||
"getgov-stable.app.cloud.gov",
|
||||
"getgov-staging.app.cloud.gov",
|
||||
"getgov-gd.app.cloud.gov",
|
||||
"getgov-rb.app.cloud.gov",
|
||||
"getgov-ko.app.cloud.gov",
|
||||
"getgov-ab.app.cloud.gov",
|
||||
"getgov-bl.app.cloud.gov",
|
||||
"getgov-rjm.app.cloud.gov",
|
||||
"getgov-jon.app.cloud.gov",
|
||||
"getgov-mr.app.cloud.gov",
|
||||
"getgov-sspj.app.cloud.gov",
|
||||
"getgov-nmb.app.cloud.gov",
|
||||
"getgov-ik.app.cloud.gov",
|
||||
"get.gov",
|
||||
]
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ for step, view in [
|
|||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="home"),
|
||||
path("whoami/", views.whoami, name="whoami"),
|
||||
path("admin/", admin.site.urls),
|
||||
path(
|
||||
"application/<id>/edit/",
|
||||
|
|
|
@ -24,31 +24,6 @@ class UserFixture:
|
|||
"""
|
||||
|
||||
ADMINS = [
|
||||
{
|
||||
"username": "c4a0e101-73b4-4d7d-9e5e-7f19a726a0fa",
|
||||
"first_name": "Seamus",
|
||||
"last_name": "Johnston",
|
||||
},
|
||||
{
|
||||
"username": "d4c3bd84-dc3a-48bc-a3c3-f53111df2ec6",
|
||||
"first_name": "Igor",
|
||||
"last_name": "",
|
||||
},
|
||||
{
|
||||
"username": "ee80bfe0-49ad-456d-8d82-e2b608a66517",
|
||||
"first_name": "Logan",
|
||||
"last_name": "",
|
||||
},
|
||||
{
|
||||
"username": "2ffe71b0-cea4-4097-8fb6-7a35b901dd70",
|
||||
"first_name": "Neil",
|
||||
"last_name": "Martinsen-Burrell",
|
||||
},
|
||||
{
|
||||
"username": "7185e6cd-d3c8-4adc-90a3-ceddba71d24f",
|
||||
"first_name": "Jon",
|
||||
"last_name": "Roberts",
|
||||
},
|
||||
{
|
||||
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
|
||||
"first_name": "Rachid",
|
||||
|
@ -59,11 +34,6 @@ class UserFixture:
|
|||
"first_name": "Alysia",
|
||||
"last_name": "Broddrick",
|
||||
},
|
||||
{
|
||||
"username": "55a3bc26-cd1d-4a5c-a8c0-7e1f561ef7f4",
|
||||
"first_name": "Michelle",
|
||||
"last_name": "Rago",
|
||||
},
|
||||
{
|
||||
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
|
||||
"first_name": "Katherine",
|
||||
|
|
|
@ -498,7 +498,7 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
|
|||
|
||||
@classmethod
|
||||
def on_fetch(cls, query):
|
||||
return [{"alternative_domain": Domain.sld(domain.name)} for domain in query]
|
||||
return [{"alternative_domain": Domain.sld(domain.website)} for domain in query]
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
|
|
|
@ -313,7 +313,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
|
||||
def place_client_hold(self):
|
||||
"""This domain should not be active."""
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError("This is not implemented yet.")
|
||||
|
||||
def remove_client_hold(self):
|
||||
"""This domain is okay to be active."""
|
||||
|
|
|
@ -490,9 +490,38 @@ class DomainApplication(TimeStampedModel):
|
|||
self.submitter.email,
|
||||
context={"application": self},
|
||||
)
|
||||
logger.info(
|
||||
f"Submission confirmation email sent to: {self.submitter.email}"
|
||||
)
|
||||
except EmailSendingError:
|
||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||
|
||||
def _send_in_review_email(self):
|
||||
"""Send an email that this application is now in review.
|
||||
|
||||
The email goes to the email address that the submitter gave as their
|
||||
contact information. If there is not submitter information, then do
|
||||
nothing.
|
||||
"""
|
||||
if self.submitter is None or self.submitter.email is None:
|
||||
logger.warning(
|
||||
"Cannot send status change (in review) email,"
|
||||
"no submitter email address."
|
||||
)
|
||||
return
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/status_change_in_review.txt",
|
||||
"emails/status_change_in_review_subject.txt",
|
||||
self.submitter.email,
|
||||
context={"application": self},
|
||||
)
|
||||
logging.info(f"In review email sent to: {self.submitter.email}")
|
||||
except EmailSendingError:
|
||||
logger.warning(
|
||||
"Failed to send status change (in review) email", exc_info=True
|
||||
)
|
||||
|
||||
@transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED)
|
||||
def submit(self):
|
||||
"""Submit an application that is started."""
|
||||
|
@ -541,6 +570,19 @@ class DomainApplication(TimeStampedModel):
|
|||
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
|
||||
)
|
||||
|
||||
@transition(field="status", source=SUBMITTED, target=INVESTIGATING)
|
||||
def in_review(self, updated_domain_application):
|
||||
"""Investigate an application that has been submitted.
|
||||
|
||||
This method is called in admin.py on the original application
|
||||
which has the correct status value, but is passed the changed
|
||||
application which has the up-to-date data that we'll use
|
||||
in the email."""
|
||||
|
||||
# When an application is moved to in review, we need to send a
|
||||
# confirmation email. This is a side-effect of the state transition
|
||||
updated_domain_application._send_in_review_email()
|
||||
|
||||
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=WITHDRAWN)
|
||||
def withdraw(self):
|
||||
"""Withdraw an application that has been submitted."""
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
<ul class="usa-nav__primary usa-accordion">
|
||||
<li class="usa-nav__primary-item">
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'whoami' %}"><span>{{ user.email }}</span></a>
|
||||
<span>{{ user.email }}</span>
|
||||
</li>
|
||||
<li class="usa-nav__primary-item display-flex flex-align-center">
|
||||
<span class="text-base"> | </span>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{% extends 'admin/change_form.html' %}
|
||||
|
||||
{% block field_sets %}
|
||||
<div class="submit-row">
|
||||
<input type="submit" value="Place hold" name="_place_client_hold">
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,39 @@
|
|||
SUMMARY OF YOUR DOMAIN REQUEST
|
||||
|
||||
Type of organization:
|
||||
{{ application.get_organization_type_display }}
|
||||
|
||||
Organization name and mailing address:
|
||||
{% spaceless %}{{ application.organization_name }}
|
||||
{{ application.address_line1 }}{% if application.address_line2 %}
|
||||
{{ application.address_line2 }}{% endif %}
|
||||
{{ application.city }}, {{ application.state_territory }}
|
||||
{{ application.zipcode }}{% if application.urbanization %}
|
||||
{{ application.urbanization }}{% endif %}{% endspaceless %}
|
||||
{% if application.type_of_work %}{# if block makes one newline if it's false #}
|
||||
Type of work:
|
||||
{% spaceless %}{{ application.type_of_work }}{% endspaceless %}
|
||||
{% endif %}
|
||||
Authorizing official:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
|
||||
{% if application.current_websites.exists %}{# if block makes a newline #}
|
||||
Current website for your organization: {% for site in application.current_websites.all %}
|
||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ application.requested_domain.name }}
|
||||
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}
|
||||
Purpose of your domain:
|
||||
{{ application.purpose }}
|
||||
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
|
||||
{% if application.other_contacts.all %}
|
||||
Other employees from your organization:
|
||||
{% for other in application.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% endfor %}{% endif %}{% if application.anything_else %}
|
||||
Anything else we should know?
|
||||
{{ application.anything_else }}
|
||||
{% endif %}
|
43
src/registrar/templates/emails/status_change_in_review.txt
Normal file
43
src/registrar/templates/emails/status_change_in_review.txt
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi {{ application.submitter.first_name }}.
|
||||
|
||||
Your .gov domain request is being reviewed.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.updated_at|date }}
|
||||
REQUEST #: {{ application.id }}
|
||||
STATUS: In review
|
||||
|
||||
|
||||
NEED TO MAKE CHANGES?
|
||||
|
||||
If you need to change your request you have to first withdraw it. Once you
|
||||
withdraw the request you can edit it and submit it again. Changing your request
|
||||
might add to the wait time. Learn more about withdrawing your request.
|
||||
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
|
||||
|
||||
|
||||
NEXT STEPS
|
||||
|
||||
- We’re reviewing your request. This usually takes 20 business days.
|
||||
|
||||
- You can check the status of your request at any time.
|
||||
<https://registrar.get.gov/application/{{ application.id }}>
|
||||
|
||||
- We’ll email you with questions or when we complete our review.
|
||||
|
||||
|
||||
THANK YOU
|
||||
|
||||
.Gov helps the public identify official, trusted information. Thank you for
|
||||
requesting a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
{% include 'emails/includes/application_summary.txt' %}
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Visit <https://get.gov>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
Your .gov domain request is being reviewed
|
|
@ -33,45 +33,7 @@ requesting a .gov domain.
|
|||
|
||||
----------------------------------------------------------------
|
||||
|
||||
SUMMARY OF YOUR DOMAIN REQUEST
|
||||
|
||||
Type of organization:
|
||||
{{ application.get_organization_type_display }}
|
||||
|
||||
Organization name and mailing address:
|
||||
{% spaceless %}{{ application.organization_name }}
|
||||
{{ application.address_line1 }}{% if application.address_line2 %}
|
||||
{{ application.address_line2 }}{% endif %}
|
||||
{{ application.city }}, {{ application.state_territory }}
|
||||
{{ application.zipcode }}{% if application.urbanization %}
|
||||
{{ application.urbanization }}{% endif %}{% endspaceless %}
|
||||
{% if application.type_of_work %}{# if block makes one newline if it's false #}
|
||||
Type of work:
|
||||
{% spaceless %}{{ application.type_of_work }}{% endspaceless %}
|
||||
{% endif %}
|
||||
Authorizing official:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
|
||||
{% if application.current_websites.exists %}{# if block makes a newline #}
|
||||
Current website for your organization: {% for site in application.current_websites.all %}
|
||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ application.requested_domain.name }}
|
||||
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}
|
||||
Purpose of your domain:
|
||||
{{ application.purpose }}
|
||||
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
|
||||
{% if application.other_contacts.all %}
|
||||
Other employees from your organization:
|
||||
{% for other in application.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% endfor %}{% endif %}{% if application.anything_else %}
|
||||
Anything else we should know?
|
||||
{{ application.anything_else }}
|
||||
{% endif %}
|
||||
{% include 'emails/includes/application_summary.txt' %}
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<p>Domain requests from state legislatures and courts must be authorized by an agency’s <strong>Chief Information Officer</strong> or <strong>highest-ranking executive</strong>.</p>
|
||||
|
||||
{% elif organization_type == 'tribal' %}
|
||||
<p>Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe, as recognized by the <a href="https://www.bia.gov/service/tribal-leaders-directory" class="usa-link">Bureau of Indian Affairs.</a></p>
|
||||
<p>Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe, as determined by the state’s tribal recognition initiative.</p>
|
||||
<p><strong>Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe</strong>, as recognized by the <a href="https://www.bia.gov/service/tribal-leaders-directory" class="usa-link">Bureau of Indian Affairs.</a></p>
|
||||
<p><strong>Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe</strong>, as determined by the state’s tribal recognition initiative.</p>
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} Hello {% endblock %}
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<p> Hello {{ user.last_name|default:"No last name given" }}, {{ user.first_name|default:"No first name given" }} <{{ user.email }}>! </p>
|
||||
|
||||
<p><a href="{% url 'logout' %}">Click here to log out</a></p>
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -8,6 +8,8 @@ from typing import List, Dict
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model, login
|
||||
|
||||
from registrar.models import Contact, DraftDomain, Website, DomainApplication
|
||||
|
||||
|
||||
def get_handlers():
|
||||
"""Obtain pointers to all StreamHandlers."""
|
||||
|
@ -84,3 +86,74 @@ class MockSESClient(Mock):
|
|||
|
||||
def send_email(self, *args, **kwargs):
|
||||
self.EMAILS_SENT.append({"args": args, "kwargs": kwargs})
|
||||
|
||||
|
||||
def completed_application(
|
||||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
has_type_of_work=True,
|
||||
has_anything_else=True,
|
||||
status=DomainApplication.STARTED,
|
||||
user=False,
|
||||
):
|
||||
"""A completed domain application."""
|
||||
if not user:
|
||||
user = get_user_model().objects.create(username="username")
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(555) 555 5555",
|
||||
)
|
||||
domain, _ = DraftDomain.objects.get_or_create(name="city.gov")
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
)
|
||||
domain_application_kwargs = dict(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
address_line2="address 2",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
requested_domain=domain,
|
||||
submitter=you,
|
||||
creator=user,
|
||||
status=status,
|
||||
)
|
||||
if has_type_of_work:
|
||||
domain_application_kwargs["type_of_work"] = "e-Government"
|
||||
if has_anything_else:
|
||||
domain_application_kwargs["anything_else"] = "There is more"
|
||||
|
||||
application, _ = DomainApplication.objects.get_or_create(
|
||||
**domain_application_kwargs
|
||||
)
|
||||
|
||||
if has_other_contacts:
|
||||
application.other_contacts.add(other)
|
||||
if has_current_website:
|
||||
application.current_websites.add(current)
|
||||
if has_alternative_gov_domain:
|
||||
application.alternative_domains.add(alt)
|
||||
|
||||
return application
|
||||
|
|
64
src/registrar/tests/test_admin.py
Normal file
64
src/registrar/tests/test_admin.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
from django.test import TestCase, RequestFactory
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from registrar.admin import DomainApplicationAdmin
|
||||
from registrar.models import DomainApplication, User
|
||||
from .common import completed_application
|
||||
|
||||
from django.conf import settings
|
||||
from unittest.mock import MagicMock
|
||||
import boto3_mocking # type: ignore
|
||||
|
||||
|
||||
class TestDomainApplicationAdmin(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_email_on_property_change(self):
|
||||
# make sure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||
)
|
||||
|
||||
# Create an instance of the model admin
|
||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.INVESTIGATING
|
||||
|
||||
# Use the model admin's save_model method
|
||||
model_admin.save_model(request, application, form=None, change=True)
|
||||
|
||||
# Access the arguments passed to send_email
|
||||
call_args = mock_client_instance.send_email.call_args
|
||||
args, kwargs = call_args
|
||||
|
||||
# Retrieve the email details from the arguments
|
||||
from_email = kwargs.get("FromEmailAddress")
|
||||
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||
email_content = kwargs["Content"]
|
||||
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||
|
||||
# Assert or perform other checks on the email details
|
||||
expected_string = "Your .gov domain request is being reviewed"
|
||||
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||
self.assertEqual(to_email, EMAIL)
|
||||
self.assertIn(expected_string, email_body)
|
||||
|
||||
# Perform assertions on the mock call itself
|
||||
mock_client_instance.send_email.assert_called_once()
|
||||
|
||||
# Cleanup
|
||||
application.delete()
|
|
@ -2,82 +2,14 @@
|
|||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from .common import completed_application
|
||||
|
||||
from registrar.models import Contact, DraftDomain, Website, DomainApplication
|
||||
|
||||
import boto3_mocking # type: ignore
|
||||
|
||||
|
||||
class TestEmails(TestCase):
|
||||
def _completed_application(
|
||||
self,
|
||||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
has_type_of_work=True,
|
||||
has_anything_else=True,
|
||||
):
|
||||
"""A completed domain application."""
|
||||
user = get_user_model().objects.create(username="username")
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(555) 555 5555",
|
||||
)
|
||||
domain, _ = DraftDomain.objects.get_or_create(name="city.gov")
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="testy-admin@town.com",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
)
|
||||
domain_application_kwargs = dict(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
address_line2="address 2",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
requested_domain=domain,
|
||||
submitter=you,
|
||||
creator=user,
|
||||
)
|
||||
if has_type_of_work:
|
||||
domain_application_kwargs["type_of_work"] = "e-Government"
|
||||
if has_anything_else:
|
||||
domain_application_kwargs["anything_else"] = "There is more"
|
||||
|
||||
application, _ = DomainApplication.objects.get_or_create(
|
||||
**domain_application_kwargs
|
||||
)
|
||||
|
||||
if has_other_contacts:
|
||||
application.other_contacts.add(other)
|
||||
if has_current_website:
|
||||
application.current_websites.add(current)
|
||||
if has_alternative_gov_domain:
|
||||
application.alternative_domains.add(alt)
|
||||
|
||||
return application
|
||||
|
||||
def setUp(self):
|
||||
self.mock_client_class = MagicMock()
|
||||
self.mock_client = self.mock_client_class.return_value
|
||||
|
@ -85,7 +17,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation(self):
|
||||
"""Submission confirmation email works."""
|
||||
application = self._completed_application()
|
||||
application = completed_application()
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
|
@ -122,7 +54,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_current_website_spacing(self):
|
||||
"""Test line spacing without current_website."""
|
||||
application = self._completed_application(has_current_website=False)
|
||||
application = completed_application(has_current_website=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -134,7 +66,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_current_website_spacing(self):
|
||||
"""Test line spacing with current_website."""
|
||||
application = self._completed_application(has_current_website=True)
|
||||
application = completed_application(has_current_website=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -147,7 +79,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_other_contacts_spacing(self):
|
||||
"""Test line spacing with other contacts."""
|
||||
application = self._completed_application(has_other_contacts=True)
|
||||
application = completed_application(has_other_contacts=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -160,7 +92,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_other_contacts_spacing(self):
|
||||
"""Test line spacing without other contacts."""
|
||||
application = self._completed_application(has_other_contacts=False)
|
||||
application = completed_application(has_other_contacts=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -172,7 +104,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_alternative_govdomain_spacing(self):
|
||||
"""Test line spacing with alternative .gov domain."""
|
||||
application = self._completed_application(has_alternative_gov_domain=True)
|
||||
application = completed_application(has_alternative_gov_domain=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -184,7 +116,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_alternative_govdomain_spacing(self):
|
||||
"""Test line spacing without alternative .gov domain."""
|
||||
application = self._completed_application(has_alternative_gov_domain=False)
|
||||
application = completed_application(has_alternative_gov_domain=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -196,7 +128,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_type_of_work_spacing(self):
|
||||
"""Test line spacing with type of work."""
|
||||
application = self._completed_application(has_type_of_work=True)
|
||||
application = completed_application(has_type_of_work=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -208,7 +140,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_type_of_work_spacing(self):
|
||||
"""Test line spacing without type of work."""
|
||||
application = self._completed_application(has_type_of_work=False)
|
||||
application = completed_application(has_type_of_work=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -220,7 +152,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_anything_else_spacing(self):
|
||||
"""Test line spacing with anything else."""
|
||||
application = self._completed_application(has_anything_else=True)
|
||||
application = completed_application(has_anything_else=True)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
@ -231,7 +163,7 @@ class TestEmails(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_anything_else_spacing(self):
|
||||
"""Test line spacing without anything else."""
|
||||
application = self._completed_application(has_anything_else=False)
|
||||
application = completed_application(has_anything_else=False)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
|
|
|
@ -12,7 +12,6 @@ from registrar.models import (
|
|||
DomainInvitation,
|
||||
UserDomainRole,
|
||||
)
|
||||
from unittest import skip
|
||||
|
||||
import boto3_mocking # type: ignore
|
||||
from .common import MockSESClient, less_console_noise
|
||||
|
@ -213,55 +212,3 @@ class TestInvitations(TestCase):
|
|||
"""A new user's first_login callback retrieves their invitations."""
|
||||
self.user.first_login()
|
||||
self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain))
|
||||
|
||||
|
||||
@skip("Not implemented yet.")
|
||||
class TestDomainApplicationLifeCycle(TestCase):
|
||||
def test_application_approval(self):
|
||||
# DomainApplication is created
|
||||
# test: Domain is created and is inactive
|
||||
# analyst approves DomainApplication
|
||||
# test: Domain is activated
|
||||
pass
|
||||
|
||||
def test_application_rejection(self):
|
||||
# DomainApplication is created
|
||||
# test: Domain is created and is inactive
|
||||
# analyst rejects DomainApplication
|
||||
# test: Domain remains inactive
|
||||
pass
|
||||
|
||||
def test_application_deleted_before_approval(self):
|
||||
# DomainApplication is created
|
||||
# test: Domain is created and is inactive
|
||||
# admin deletes DomainApplication
|
||||
# test: Domain is deleted; Hosts, HostIps and Nameservers are deleted
|
||||
pass
|
||||
|
||||
def test_application_deleted_following_approval(self):
|
||||
# DomainApplication is created
|
||||
# test: Domain is created and is inactive
|
||||
# analyst approves DomainApplication
|
||||
# admin deletes DomainApplication
|
||||
# test: DomainApplication foreign key field on Domain is set to null
|
||||
pass
|
||||
|
||||
def test_application_approval_with_conflicting_name(self):
|
||||
# DomainApplication #1 is created
|
||||
# test: Domain #1 is created and is inactive
|
||||
# analyst approves DomainApplication #1
|
||||
# test: Domain #1 is activated
|
||||
# DomainApplication #2 is created, with the same domain name string
|
||||
# test: Domain #2 is created and is inactive
|
||||
# analyst approves DomainApplication #2
|
||||
# test: error is raised
|
||||
# test: DomainApplication #1 remains approved
|
||||
# test: Domain #1 remains active
|
||||
# test: DomainApplication #2 remains in investigating
|
||||
# test: Domain #2 remains inactive
|
||||
pass
|
||||
|
||||
def test_application_approval_with_network_errors(self):
|
||||
# TODO: scenario wherein application is approved,
|
||||
# but attempts to contact the registry to activate the domain fail
|
||||
pass
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
"""
|
||||
Feature being tested: Registry Integration
|
||||
|
||||
This file tests the various ways in which the registrar interacts with the registry.
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.db.utils import IntegrityError
|
||||
from unittest.mock import patch, MagicMock
|
||||
import datetime
|
||||
from registrar.models import DomainApplication, User, Domain
|
||||
from registrar.models import Domain # add in DomainApplication, User,
|
||||
|
||||
from unittest import skip
|
||||
from epplibwrapper import commands
|
||||
|
||||
|
||||
class TestDomain(TestCase):
|
||||
class TestDomainCache(TestCase):
|
||||
class fakedEppObject(object):
|
||||
""""""
|
||||
|
||||
|
@ -47,18 +53,6 @@ class TestDomain(TestCase):
|
|||
def tearDown(self):
|
||||
self.patcher.stop()
|
||||
|
||||
def test_empty_create_fails(self):
|
||||
"""Can't create a completely empty domain."""
|
||||
with self.assertRaisesRegex(IntegrityError, "name"):
|
||||
Domain.objects.create()
|
||||
|
||||
def test_minimal_create(self):
|
||||
"""Can create with just a name."""
|
||||
Domain.objects.create(name="igorville.gov")
|
||||
# this assertion will not work -- for now, the fact that the
|
||||
# above command didn't error out is proof enough
|
||||
# self.assertEquals(domain.state, Domain.State.DRAFTED)
|
||||
|
||||
def test_cache_sets_resets(self):
|
||||
"""Cache should be set on getter and reset on setter calls"""
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
|
@ -123,6 +117,53 @@ class TestDomain(TestCase):
|
|||
domain._get_property("hosts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
|
||||
|
||||
class TestDomainCreation(TestCase):
|
||||
"""Rule: An approved domain application must result in a domain"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given that a valid domain application exists
|
||||
"""
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_approved_application_creates_domain_locally(self):
|
||||
"""
|
||||
Scenario: Analyst approves a domain application
|
||||
When the DomainApplication transitions to approved
|
||||
Then a Domain exists in the database with the same `name`
|
||||
But a domain object does not exist in the registry
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_accessing_domain_properties_creates_domain_in_registry(self):
|
||||
"""
|
||||
Scenario: A registrant checks the status of a newly approved domain
|
||||
Given that no domain object exists in the registry
|
||||
When a property is accessed
|
||||
Then Domain sends `commands.CreateDomain` to the registry
|
||||
And `domain.state` is set to `CREATED`
|
||||
And `domain.is_active()` returns False
|
||||
"""
|
||||
raise
|
||||
|
||||
def test_empty_domain_creation(self):
|
||||
"""Can't create a completely empty domain."""
|
||||
with self.assertRaisesRegex(IntegrityError, "name"):
|
||||
Domain.objects.create()
|
||||
|
||||
def test_minimal_creation(self):
|
||||
"""Can create with just a name."""
|
||||
Domain.objects.create(name="igorville.gov")
|
||||
|
||||
def test_duplicate_creation(self):
|
||||
"""Can't create domain if name is not unique."""
|
||||
Domain.objects.create(name="igorville.gov")
|
||||
with self.assertRaisesRegex(IntegrityError, "name"):
|
||||
Domain.objects.create(name="igorville.gov")
|
||||
|
||||
@skip("cannot activate a domain without mock registry")
|
||||
def test_get_status(self):
|
||||
"""Returns proper status based on `state`."""
|
||||
|
@ -133,23 +174,430 @@ class TestDomain(TestCase):
|
|||
domain.save()
|
||||
self.assertIn("ok", domain.status)
|
||||
|
||||
@skip("cannot activate a domain without mock registry")
|
||||
def test_fsm_activate_fail_unique(self):
|
||||
"""Can't activate domain if name is not unique."""
|
||||
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
d2, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
d1.activate()
|
||||
d1.save()
|
||||
with self.assertRaises(ValueError):
|
||||
d2.activate()
|
||||
|
||||
@skip("cannot activate a domain without mock registry")
|
||||
def test_fsm_activate_fail_unapproved(self):
|
||||
"""Can't activate domain if application isn't approved."""
|
||||
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user)
|
||||
d1.domain_application = application
|
||||
d1.save()
|
||||
with self.assertRaises(ValueError):
|
||||
d1.activate()
|
||||
class TestRegistrantContacts(TestCase):
|
||||
"""Rule: Registrants may modify their WHOIS data"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given the registrant is logged in
|
||||
And the registrant is the admin on a domain
|
||||
"""
|
||||
pass
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_no_security_email(self):
|
||||
"""
|
||||
Scenario: Registrant has not added a security contact email
|
||||
Given `domain.security_contact` has not been set to anything
|
||||
When the domain is created in the registry
|
||||
Then the domain has a valid security contact with CISA defaults
|
||||
And disclose flags are set to keep the email address hidden
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_adds_security_email(self):
|
||||
"""
|
||||
Scenario: Registrant adds a security contact email
|
||||
When `domain.security_contact` is set equal to a PublicContact with the
|
||||
chosen security contact email
|
||||
Then Domain sends `commands.CreateContact` to the registry
|
||||
And Domain sends `commands.UpdateDomain` to the registry with the newly
|
||||
created contact of type 'security'
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_security_email_is_idempotent(self):
|
||||
"""
|
||||
Scenario: Registrant adds a security contact email twice, due to a UI glitch
|
||||
When `commands.CreateContact` and `commands.UpdateDomain` are sent
|
||||
to the registry twice with identical data
|
||||
Then no errors are raised in Domain
|
||||
"""
|
||||
# implementation note: this requires seeing what happens when these are actually
|
||||
# sent like this, and then implementing appropriate mocks for any errors the
|
||||
# registry normally sends in this case
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_deletes_security_email(self):
|
||||
"""
|
||||
Scenario: Registrant clears out an existing security contact email
|
||||
Given a domain exists in the registry with a user-added security email
|
||||
When `domain.security_contact` is set equal to a PublicContact with an empty
|
||||
security contact email
|
||||
Then Domain sends `commands.UpdateDomain` and `commands.DeleteContact`
|
||||
to the registry
|
||||
And the domain has a valid security contact with CISA defaults
|
||||
And disclose flags are set to keep the email address hidden
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_updates_security_email(self):
|
||||
"""
|
||||
Scenario: Registrant replaces one valid security contact email with another
|
||||
Given a domain exists in the registry with a user-added security email
|
||||
When `domain.security_contact` is set equal to a PublicContact with a new
|
||||
security contact email
|
||||
Then Domain sends `commands.UpdateContact` to the registry
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to the security contact is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
raise
|
||||
|
||||
|
||||
class TestRegistrantNameservers(TestCase):
|
||||
"""Rule: Registrants may modify their nameservers"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given the registrant is logged in
|
||||
And the registrant is the admin on a domain
|
||||
"""
|
||||
pass
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_adds_one_nameserver(self):
|
||||
"""
|
||||
Scenario: Registrant adds a single nameserver
|
||||
Given the domain has zero nameservers
|
||||
When `domain.nameservers` is set to an array of length 1
|
||||
Then `commands.CreateHost` and `commands.UpdateDomain` is sent
|
||||
to the registry
|
||||
And `domain.is_active` returns False
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_adds_two_nameservers(self):
|
||||
"""
|
||||
Scenario: Registrant adds 2 or more nameservers, thereby activating the domain
|
||||
Given the domain has zero nameservers
|
||||
When `domain.nameservers` is set to an array of length 2
|
||||
Then `commands.CreateHost` and `commands.UpdateDomain` is sent
|
||||
to the registry
|
||||
And `domain.is_active` returns True
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_adds_too_many_nameservers(self):
|
||||
"""
|
||||
Scenario: Registrant adds 14 or more nameservers
|
||||
Given the domain has zero nameservers
|
||||
When `domain.nameservers` is set to an array of length 14
|
||||
Then Domain raises a user-friendly error
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_removes_some_nameservers(self):
|
||||
"""
|
||||
Scenario: Registrant removes some nameservers, while keeping at least 2
|
||||
Given the domain has 3 nameservers
|
||||
When `domain.nameservers` is set to an array containing nameserver #1 and #2
|
||||
Then `commands.UpdateDomain` and `commands.DeleteHost` is sent
|
||||
to the registry
|
||||
And `domain.is_active` returns True
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_removes_too_many_nameservers(self):
|
||||
"""
|
||||
Scenario: Registrant removes some nameservers, bringing the total to less than 2
|
||||
Given the domain has 3 nameservers
|
||||
When `domain.nameservers` is set to an array containing nameserver #1
|
||||
Then `commands.UpdateDomain` and `commands.DeleteHost` is sent
|
||||
to the registry
|
||||
And `domain.is_active` returns False
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_replaces_nameservers(self):
|
||||
"""
|
||||
Scenario: Registrant simultaneously adds and removes some nameservers
|
||||
Given the domain has 3 nameservers
|
||||
When `domain.nameservers` is set to an array containing nameserver #1 plus
|
||||
two new nameservers
|
||||
Then `commands.CreateHost` is sent to create #4 and #5
|
||||
And `commands.UpdateDomain` is sent to add #4 and #5 plus remove #2 and #3
|
||||
And `commands.DeleteHost` is sent to delete #2 and #3
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_cannot_add_subordinate_without_ip(self):
|
||||
"""
|
||||
Scenario: Registrant adds a nameserver which is a subdomain of their .gov
|
||||
Given the domain exists in the registry
|
||||
When `domain.nameservers` is set to an array containing an entry
|
||||
with a subdomain of the domain and no IP addresses
|
||||
Then Domain raises a user-friendly error
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_updates_ips(self):
|
||||
"""
|
||||
Scenario: Registrant changes IP addresses for a nameserver
|
||||
Given the domain exists in the registry
|
||||
And has a subordinate nameserver
|
||||
When `domain.nameservers` is set to an array containing that nameserver
|
||||
with a different IP address(es)
|
||||
Then `commands.UpdateHost` is sent to the registry
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_cannot_add_non_subordinate_with_ip(self):
|
||||
"""
|
||||
Scenario: Registrant adds a nameserver which is NOT a subdomain of their .gov
|
||||
Given the domain exists in the registry
|
||||
When `domain.nameservers` is set to an array containing an entry
|
||||
which is not a subdomain of the domain and has IP addresses
|
||||
Then Domain raises a user-friendly error
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_nameservers_are_idempotent(self):
|
||||
"""
|
||||
Scenario: Registrant adds a set of nameservers twice, due to a UI glitch
|
||||
When `commands.CreateHost` and `commands.UpdateDomain` are sent
|
||||
to the registry twice with identical data
|
||||
Then no errors are raised in Domain
|
||||
"""
|
||||
# implementation note: this requires seeing what happens when these are actually
|
||||
# sent like this, and then implementing appropriate mocks for any errors the
|
||||
# registry normally sends in this case
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to the nameservers is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
raise
|
||||
|
||||
|
||||
class TestRegistrantDNSSEC(TestCase):
|
||||
"""Rule: Registrants may modify their secure DNS data"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given the registrant is logged in
|
||||
And the registrant is the admin on a domain
|
||||
"""
|
||||
pass
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_user_adds_dns_data(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNS data
|
||||
...
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_dnssec_is_idempotent(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNS data twice, due to a UI glitch
|
||||
...
|
||||
"""
|
||||
# implementation note: this requires seeing what happens when these are actually
|
||||
# sent like this, and then implementing appropriate mocks for any errors the
|
||||
# registry normally sends in this case
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to the dns data is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
raise
|
||||
|
||||
|
||||
class TestAnalystClientHold(TestCase):
|
||||
"""Rule: Analysts may suspend or restore a domain by using client hold"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given the analyst is logged in
|
||||
And a domain exists in the registry
|
||||
"""
|
||||
pass
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_places_client_hold(self):
|
||||
"""
|
||||
Scenario: Analyst takes a domain off the internet
|
||||
When `domain.place_client_hold()` is called
|
||||
Then `CLIENT_HOLD` is added to the domain's statuses
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_places_client_hold_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to place client hold twice
|
||||
Given `CLIENT_HOLD` is already in the domain's statuses
|
||||
When `domain.place_client_hold()` is called
|
||||
Then Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_removes_client_hold(self):
|
||||
"""
|
||||
Scenario: Analyst restores a suspended domain
|
||||
Given `CLIENT_HOLD` is in the domain's statuses
|
||||
When `domain.remove_client_hold()` is called
|
||||
Then `CLIENT_HOLD` is no longer in the domain's statuses
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_removes_client_hold_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to remove client hold twice
|
||||
Given `CLIENT_HOLD` is not in the domain's statuses
|
||||
When `domain.remove_client_hold()` is called
|
||||
Then Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to place or remove client hold is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
raise
|
||||
|
||||
|
||||
class TestAnalystLock(TestCase):
|
||||
"""Rule: Analysts may lock or unlock a domain to prevent or allow updates"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given the analyst is logged in
|
||||
And a domain exists in the registry
|
||||
"""
|
||||
pass
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_locks_domain(self):
|
||||
"""
|
||||
Scenario: Analyst locks a domain to prevent edits or deletion
|
||||
When `domain.lock()` is called
|
||||
Then `CLIENT_DELETE_PROHIBITED` is added to the domain's statuses
|
||||
And `CLIENT_TRANSFER_PROHIBITED` is added to the domain's statuses
|
||||
And `CLIENT_UPDATE_PROHIBITED` is added to the domain's statuses
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_locks_domain_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to lock a domain twice
|
||||
Given `CLIENT_*_PROHIBITED` is already in the domain's statuses
|
||||
When `domain.lock()` is called
|
||||
Then Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_removes_lock(self):
|
||||
"""
|
||||
Scenario: Analyst unlocks a domain to allow deletion or edits
|
||||
Given `CLIENT_*_PROHIBITED` is in the domain's statuses
|
||||
When `domain.unlock()` is called
|
||||
Then `CLIENT_DELETE_PROHIBITED` is no longer in the domain's statuses
|
||||
And `CLIENT_TRANSFER_PROHIBITED` is no longer in the domain's statuses
|
||||
And `CLIENT_UPDATE_PROHIBITED` is no longer in the domain's statuses
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_removes_lock_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to unlock a domain twice
|
||||
Given `CLIENT_*_PROHIBITED` is not in the domain's statuses
|
||||
When `domain.unlock()` is called
|
||||
Then Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to lock or unlock a domain is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
"""
|
||||
raise
|
||||
|
||||
|
||||
class TestAnalystDelete(TestCase):
|
||||
"""Rule: Analysts may delete a domain"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Background:
|
||||
Given the analyst is logged in
|
||||
And a domain exists in the registry
|
||||
"""
|
||||
pass
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_deletes_domain(self):
|
||||
"""
|
||||
Scenario: Analyst permanently deletes a domain
|
||||
When `domain.delete()` is called
|
||||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And `state` is set to `DELETED`
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_analyst_deletes_domain_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to delete an already deleted domain
|
||||
Given `state` is already `DELETED`
|
||||
When `domain.delete()` is called
|
||||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And Domain returns normally (without error)
|
||||
"""
|
||||
raise
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_deletion_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
And `state` is not set to `DELETED`
|
||||
"""
|
||||
raise
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.conf import settings
|
|||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from .common import completed_application
|
||||
|
||||
from django_webtest import WebTest # type: ignore
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -39,12 +40,6 @@ class TestViews(TestCase):
|
|||
response = self.client.get("/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_whoami_page_no_user(self):
|
||||
"""Whoami page not accessible without a logged-in user."""
|
||||
response = self.client.get("/whoami/")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn("?next=/whoami/", response.headers["Location"])
|
||||
|
||||
def test_application_form_not_logged_in(self):
|
||||
"""Application form not accessible without a logged-in user."""
|
||||
response = self.client.get("/register/")
|
||||
|
@ -99,13 +94,6 @@ class LoggedInTests(TestWithUser):
|
|||
# clean up
|
||||
role.delete()
|
||||
|
||||
def test_whoami_page(self):
|
||||
"""User information appears on the whoami page."""
|
||||
response = self.client.get("/whoami/")
|
||||
self.assertContains(response, self.user.first_name)
|
||||
self.assertContains(response, self.user.last_name)
|
||||
self.assertContains(response, self.user.email)
|
||||
|
||||
def test_application_form_view(self):
|
||||
response = self.client.get("/register/", follow=True)
|
||||
self.assertContains(
|
||||
|
@ -473,6 +461,41 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# check that any new pages are added to this test
|
||||
self.assertEqual(num_pages, num_pages_tested)
|
||||
|
||||
# This is the start of a test to check an existing application, it currently
|
||||
# does not work and results in errors as noted in:
|
||||
# https://github.com/cisagov/getgov/pull/728
|
||||
@skip("WIP")
|
||||
def test_application_form_started_allsteps(self):
|
||||
num_pages_tested = 0
|
||||
# elections, type_of_work, tribal_government, no_other_contacts
|
||||
SKIPPED_PAGES = 4
|
||||
DASHBOARD_PAGE = 1
|
||||
num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE
|
||||
|
||||
application = completed_application(user=self.user)
|
||||
application.save()
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
self.assertContains(home_page, "Started")
|
||||
num_pages_tested += 1
|
||||
|
||||
# TODO: For some reason this click results in a new application being generated
|
||||
# This appraoch is an alternatie to using get as is being done below
|
||||
#
|
||||
# type_page = home_page.click("Edit")
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
url = reverse("edit-application", kwargs={"id": application.pk})
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# TODO: The following line results in a django error on middleware
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertContains(response, "Type of organization")
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# TODO: Step through the remaining pages
|
||||
|
||||
self.assertEqual(num_pages, num_pages_tested)
|
||||
|
||||
def test_application_form_conditional_federal(self):
|
||||
"""Federal branch question is shown for federal organizations."""
|
||||
type_page = self.app.get(reverse("application:")).follow()
|
||||
|
@ -1407,85 +1430,18 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
self.app.set_user(self.user.username)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def _completed_application(
|
||||
self,
|
||||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
has_type_of_work=True,
|
||||
has_anything_else=True,
|
||||
):
|
||||
"""A completed domain application."""
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(555) 555 5555",
|
||||
)
|
||||
domain, _ = DraftDomain.objects.get_or_create(name="citystatus.gov")
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="testy-admin@town.com",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
)
|
||||
domain_application_kwargs = dict(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
address_line2="address 2",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
requested_domain=domain,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
)
|
||||
if has_type_of_work:
|
||||
domain_application_kwargs["type_of_work"] = "e-Government"
|
||||
if has_anything_else:
|
||||
domain_application_kwargs["anything_else"] = "There is more"
|
||||
|
||||
application, _ = DomainApplication.objects.get_or_create(
|
||||
**domain_application_kwargs
|
||||
)
|
||||
|
||||
application.status = DomainApplication.SUBMITTED
|
||||
application.save()
|
||||
|
||||
if has_other_contacts:
|
||||
application.other_contacts.add(other)
|
||||
if has_current_website:
|
||||
application.current_websites.add(current)
|
||||
if has_alternative_gov_domain:
|
||||
application.alternative_domains.add(alt)
|
||||
|
||||
return application
|
||||
|
||||
def test_application_status(self):
|
||||
"""Checking application status page"""
|
||||
application = self._completed_application()
|
||||
application = completed_application(
|
||||
status=DomainApplication.SUBMITTED, user=self.user
|
||||
)
|
||||
application.save()
|
||||
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "citystatus.gov")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Manage" link
|
||||
detail_page = home_page.click("Manage")
|
||||
self.assertContains(detail_page, "citystatus.gov")
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Admin Tester")
|
||||
|
@ -1493,14 +1449,16 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
|
||||
def test_application_withdraw(self):
|
||||
"""Checking application status page"""
|
||||
application = self._completed_application()
|
||||
application = completed_application(
|
||||
status=DomainApplication.SUBMITTED, user=self.user
|
||||
)
|
||||
application.save()
|
||||
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "citystatus.gov")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Manage" link
|
||||
detail_page = home_page.click("Manage")
|
||||
self.assertContains(detail_page, "citystatus.gov")
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Admin Tester")
|
||||
|
@ -1522,7 +1480,9 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
|||
|
||||
def test_application_status_no_permissions(self):
|
||||
"""Can't access applications without being the creator."""
|
||||
application = self._completed_application()
|
||||
application = completed_application(
|
||||
status=DomainApplication.SUBMITTED, user=self.user
|
||||
)
|
||||
other_user = User()
|
||||
other_user.save()
|
||||
application.creator = other_user
|
||||
|
|
|
@ -12,4 +12,3 @@ from .domain import (
|
|||
)
|
||||
from .health import *
|
||||
from .index import *
|
||||
from .whoami import *
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
@login_required
|
||||
def whoami(request):
|
||||
"""This is the first page someone goes to after logging in."""
|
||||
return render(request, "whoami.html")
|
Loading…
Add table
Add a link
Reference in a new issue