Merge branch 'main' of https://github.com/cisagov/getgov into rh/687-formatting-nameservers

This commit is contained in:
Rebecca Hsieh 2023-10-30 09:35:29 -07:00
commit 5ae98c97d2
No known key found for this signature in database
GPG key ID: 644527A2F375A379
10 changed files with 105 additions and 54 deletions

View file

@ -46,7 +46,7 @@ This is a standard Django secret key. See Django documentation for tips on gener
This is the base64 encoded private key used in the OpenID Connect authentication flow with Login.gov. It is used to sign a token during user login; the signature is examined by Login.gov before their API grants access to user data. This is the base64 encoded private key used in the OpenID Connect authentication flow with Login.gov. It is used to sign a token during user login; the signature is examined by Login.gov before their API grants access to user data.
Generate a new key using this command (or whatever is most recently recommended by Login.gov): Generate a new key using this command (or whatever is most recently [recommended by Login.gov](https://developers.login.gov/testing/#creating-a-public-certificate)):
```bash ```bash
openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private.pem -out public.crt openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private.pem -out public.crt

View file

@ -5,14 +5,13 @@ import json
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import RequestFactory from django.test import RequestFactory
from ..views import available, in_domains from ..views import available, check_domain_available
from .common import less_console_noise from .common import less_console_noise
from registrar.tests.common import MockEppLib from registrar.tests.common import MockEppLib
from unittest.mock import call from unittest.mock import call
from epplibwrapper import ( from epplibwrapper import (
commands, commands,
RegistryError,
) )
API_BASE_PATH = "/api/v1/available/" API_BASE_PATH = "/api/v1/available/"
@ -37,10 +36,10 @@ class AvailableViewTest(MockEppLib):
response_object = json.loads(response.content) response_object = json.loads(response.content)
self.assertIn("available", response_object) self.assertIn("available", response_object)
def test_in_domains_makes_calls_(self): def test_domain_available_makes_calls_(self):
"""Domain searches successfully make correct mock EPP calls""" """Domain searches successfully make correct mock EPP calls"""
gsa_available = in_domains("gsa.gov") gsa_available = check_domain_available("gsa.gov")
igorville_available = in_domains("igorvilleremixed.gov") igorville_available = check_domain_available("igorville.gov")
"""Domain searches successfully make mock EPP calls""" """Domain searches successfully make mock EPP calls"""
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
@ -53,29 +52,32 @@ class AvailableViewTest(MockEppLib):
), ),
call( call(
commands.CheckDomain( commands.CheckDomain(
["igorvilleremixed.gov"], ["igorville.gov"],
), ),
cleaned=True, cleaned=True,
), ),
] ]
) )
"""Domain searches return correct availability results""" """Domain searches return correct availability results"""
self.assertTrue(gsa_available) self.assertFalse(gsa_available)
self.assertFalse(igorville_available) self.assertTrue(igorville_available)
def test_in_domains_capitalized(self): def test_domain_available_capitalized(self):
"""Domain searches work without case sensitivity""" """Domain searches work without case sensitivity"""
self.assertTrue(in_domains("gsa.gov")) self.assertFalse(check_domain_available("gsa.gov"))
# input is lowercased so GSA.GOV should be found self.assertTrue(check_domain_available("igorville.gov"))
self.assertTrue(in_domains("GSA.gov")) # input is lowercased so GSA.GOV should also not be available
self.assertFalse(check_domain_available("GSA.gov"))
# input is lowercased so IGORVILLE.GOV should also not be available
self.assertFalse(check_domain_available("IGORVILLE.gov"))
def test_in_domains_dotgov(self): def test_domain_available_dotgov(self):
"""Domain searches work without trailing .gov""" """Domain searches work without trailing .gov"""
self.assertTrue(in_domains("gsa")) self.assertFalse(check_domain_available("gsa"))
# input is lowercased so GSA.GOV should be found # input is lowercased so GSA.GOV should be found
self.assertTrue(in_domains("GSA")) self.assertFalse(check_domain_available("GSA"))
# This domain should not have been registered # This domain should be available to register
self.assertFalse(in_domains("igorvilleremixed")) self.assertTrue(check_domain_available("igorville"))
def test_not_available_domain(self): def test_not_available_domain(self):
"""gsa.gov is not available""" """gsa.gov is not available"""
@ -85,17 +87,17 @@ class AvailableViewTest(MockEppLib):
self.assertFalse(json.loads(response.content)["available"]) self.assertFalse(json.loads(response.content)["available"])
def test_available_domain(self): def test_available_domain(self):
"""igorvilleremixed.gov is still available""" """igorville.gov is still available"""
request = self.factory.get(API_BASE_PATH + "igorvilleremixed.gov") request = self.factory.get(API_BASE_PATH + "igorville.gov")
request.user = self.user request.user = self.user
response = available(request, domain="igorvilleremixed.gov") response = available(request, domain="igorville.gov")
self.assertTrue(json.loads(response.content)["available"]) self.assertTrue(json.loads(response.content)["available"])
def test_available_domain_dotgov(self): def test_available_domain_dotgov(self):
"""igorvilleremixed.gov is still available even without the .gov suffix""" """igorville.gov is still available even without the .gov suffix"""
request = self.factory.get(API_BASE_PATH + "igorvilleremixed") request = self.factory.get(API_BASE_PATH + "igorville")
request.user = self.user request.user = self.user
response = available(request, domain="igorvilleremixed") response = available(request, domain="igorville")
self.assertTrue(json.loads(response.content)["available"]) self.assertTrue(json.loads(response.content)["available"])
def test_error_handling(self): def test_error_handling(self):
@ -105,10 +107,9 @@ class AvailableViewTest(MockEppLib):
request.user = self.user request.user = self.user
response = available(request, domain=bad_string) response = available(request, domain=bad_string)
self.assertFalse(json.loads(response.content)["available"]) self.assertFalse(json.loads(response.content)["available"])
# domain set to raise error successfully raises error # domain set to raise error returns false for availability
with self.assertRaises(RegistryError): error_domain_available = available(request, "errordomain.gov")
error_domain_available = available(request, "errordomain.gov") self.assertFalse(json.loads(error_domain_available.content)["available"])
self.assertFalse(json.loads(error_domain_available.content)["available"])
class AvailableAPITest(MockEppLib): class AvailableAPITest(MockEppLib):

View file

@ -5,6 +5,8 @@ from django.http import JsonResponse
import requests import requests
from login_required import login_not_required
from cachetools.func import ttl_cache from cachetools.func import ttl_cache
@ -23,6 +25,7 @@ DOMAIN_API_MESSAGES = {
"invalid": "Enter a domain using only letters," "invalid": "Enter a domain using only letters,"
" numbers, or hyphens (though we don't recommend using hyphens).", " numbers, or hyphens (though we don't recommend using hyphens).",
"success": "That domain is available!", "success": "That domain is available!",
"error": "Error finding domain availability.",
} }
@ -50,22 +53,26 @@ def _domains():
return domains return domains
def in_domains(domain): def check_domain_available(domain):
"""Return true if the given domain is in the domains list. """Return true if the given domain is available.
The given domain is lowercased to match against the domains list. If the The given domain is lowercased to match against the domains list. If the
given domain doesn't end with .gov, ".gov" is added when looking for given domain doesn't end with .gov, ".gov" is added when looking for
a match. a match.
""" """
Domain = apps.get_model("registrar.Domain") Domain = apps.get_model("registrar.Domain")
if domain.endswith(".gov"): try:
return Domain.available(domain) if domain.endswith(".gov"):
else: return Domain.available(domain)
# domain search string doesn't end with .gov, add it on here else:
return Domain.available(domain + ".gov") # domain search string doesn't end with .gov, add it on here
return Domain.available(domain + ".gov")
except Exception:
return False
@require_http_methods(["GET"]) @require_http_methods(["GET"])
@login_not_required
def available(request, domain=""): def available(request, domain=""):
"""Is a given domain available or not. """Is a given domain available or not.
@ -83,11 +90,16 @@ def available(request, domain=""):
{"available": False, "message": DOMAIN_API_MESSAGES["invalid"]} {"available": False, "message": DOMAIN_API_MESSAGES["invalid"]}
) )
# a domain is available if it is NOT in the list of current domains # a domain is available if it is NOT in the list of current domains
if in_domains(domain): try:
if check_domain_available(domain):
return JsonResponse(
{"available": True, "message": DOMAIN_API_MESSAGES["success"]}
)
else:
return JsonResponse(
{"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]}
)
except Exception:
return JsonResponse( return JsonResponse(
{"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]} {"available": False, "message": DOMAIN_API_MESSAGES["error"]}
)
else:
return JsonResponse(
{"available": True, "message": DOMAIN_API_MESSAGES["success"]}
) )

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-30 15:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0042_create_groups_v03"),
]
operations = [
migrations.AddField(
model_name="domain",
name="expiration_date",
field=models.DateField(
help_text="Duplication of registry's expirationdate saved for ease of reporting",
null=True,
),
),
]

View file

@ -5,6 +5,7 @@ import re
from datetime import date from datetime import date
from string import digits from string import digits
from typing import Optional from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models from django.db import models
@ -29,6 +30,7 @@ from epplibwrapper import (
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
from django.db.models import DateField
from .utility.domain_field import DomainField from .utility.domain_field import DomainField
from .utility.domain_helper import DomainHelper from .utility.domain_helper import DomainHelper
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
@ -209,12 +211,12 @@ class Domain(TimeStampedModel, DomainHelper):
return self._get_property("up_date") return self._get_property("up_date")
@Cache @Cache
def expiration_date(self) -> date: def registry_expiration_date(self) -> date:
"""Get or set the `ex_date` element from the registry.""" """Get or set the `ex_date` element from the registry."""
return self._get_property("ex_date") return self._get_property("ex_date")
@expiration_date.setter # type: ignore @registry_expiration_date.setter # type: ignore
def expiration_date(self, ex_date: date): def registry_expiration_date(self, ex_date: date):
pass pass
@Cache @Cache
@ -943,6 +945,13 @@ class Domain(TimeStampedModel, DomainHelper):
help_text="Very basic info about the lifecycle of this domain object", help_text="Very basic info about the lifecycle of this domain object",
) )
expiration_date = DateField(
null=True,
help_text=(
"Duplication of registry's expiration" "date saved for ease of reporting"
),
)
def isActive(self): def isActive(self):
return self.state == Domain.State.CREATED return self.state == Domain.State.CREATED

View file

@ -1,6 +1,6 @@
import re import re
from api.views import in_domains from api.views import check_domain_available
from registrar.utility import errors from registrar.utility import errors
@ -44,7 +44,7 @@ class DomainHelper:
raise errors.ExtraDotsError() raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"): if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise ValueError() raise ValueError()
if in_domains(domain): if not check_domain_available(domain):
raise errors.DomainUnavailableError() raise errors.DomainUnavailableError()
return domain return domain

View file

@ -12,11 +12,13 @@
email, and DNS name servers. email, and DNS name servers.
</p> </p>
<ul> <ul class="usa-list">
<li>There is no limit to the number of domain managers you can add.</li> <li>There is no limit to the number of domain managers you can add.</li>
<li>After adding a domain manager, an email invitation will be sent to that user with <li>After adding a domain manager, an email invitation will be sent to that user with
instructions on how to set up an account.</li> instructions on how to set up an account.</li>
<li>To remove a domain manager, <a href="{% public_site_url 'contact/' %}" class="usa-link">contact us</a> for assistance. <li>To remove a domain manager, <a href="{% public_site_url 'contact/' %}"
target="_blank" rel="noopener noreferrer" class="usa-link">contact us</a> for
assistance.</li>
</ul> </ul>
{% if domain.permissions %} {% if domain.permissions %}

View file

@ -860,11 +860,17 @@ class MockEppLib(TestCase):
def mockCheckDomainCommand(self, _request, cleaned): def mockCheckDomainCommand(self, _request, cleaned):
if "gsa.gov" in getattr(_request, "names", None): if "gsa.gov" in getattr(_request, "names", None):
return self._mockDomainName("gsa.gov", True) return self._mockDomainName("gsa.gov", False)
elif "GSA.gov" in getattr(_request, "names", None): elif "GSA.gov" in getattr(_request, "names", None):
return self._mockDomainName("GSA.gov", True) return self._mockDomainName("GSA.gov", False)
elif "igorvilleremixed.gov" in getattr(_request, "names", None): elif "igorville.gov" in getattr(_request, "names", None):
return self._mockDomainName("igorvilleremixed.gov", False) return self._mockDomainName("igorvilleremixed.gov", True)
elif "top-level-agency.gov" in getattr(_request, "names", None):
return self._mockDomainName("top-level-agency.gov", True)
elif "city.gov" in getattr(_request, "names", None):
return self._mockDomainName("city.gov", True)
elif "city1.gov" in getattr(_request, "names", None):
return self._mockDomainName("city1.gov", True)
elif "errordomain.gov" in getattr(_request, "names", None): elif "errordomain.gov" in getattr(_request, "names", None):
raise RegistryError("Registry cannot find domain availability.") raise RegistryError("Registry cannot find domain availability.")
else: else:

View file

@ -56,7 +56,7 @@ class TestDomainCache(MockEppLib):
self.assertFalse("avail" in domain._cache.keys()) self.assertFalse("avail" in domain._cache.keys())
# using a setter should clear the cache # using a setter should clear the cache
domain.expiration_date = datetime.date.today() domain.registry_expiration_date = datetime.date.today()
self.assertEquals(domain._cache, {}) self.assertEquals(domain._cache, {})
# send should have been called only once # send should have been called only once

View file

@ -110,12 +110,13 @@ class TestURLAuth(TestCase):
# Note that the trailing slash is wobbly depending on how the URL was defined. # Note that the trailing slash is wobbly depending on how the URL was defined.
IGNORE_URLS = [ IGNORE_URLS = [
# These are the OIDC auth endpoints that always need # These are the OIDC auth endpoints that always need
# to be public. # to be public. Use the exact URLs that will be tested.
"/openid/login/", "/openid/login/",
"/openid/logout/", "/openid/logout/",
"/openid/callback", "/openid/callback",
"/openid/callback/login/", "/openid/callback/login/",
"/openid/callback/logout/", "/openid/callback/logout/",
"/api/v1/available/whitehouse.gov",
] ]
def assertURLIsProtectedByAuth(self, url): def assertURLIsProtectedByAuth(self, url):