From 2dca24f330102dd193dded2a224ca8e11a7daef4 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:45:13 -0700 Subject: [PATCH 01/11] Add initial rdap api endpoint --- src/api/views.py | 13 +++++++++++++ src/registrar/config/urls.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/api/views.py b/src/api/views.py index 2199e15ac7..87bb9f5894 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -19,6 +19,7 @@ DOMAIN_FILE_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv" +RDAP_URL = "https://rdap.cloudflareregistry.com/rdap/domain/" DOMAIN_API_MESSAGES = { @@ -99,6 +100,18 @@ def available(request, domain=""): return json_response +@require_http_methods(["GET"]) +@login_not_required +def rdap(request, domain=""): + """TODO: Write description + """ + Domain = apps.get_model("registrar.Domain") + domain = request.GET.get("domain", "") + + rdap_response = requests.get(DOMAIN_FILE_URL, domain) + return rdap_response + + @require_http_methods(["GET"]) @login_not_required def get_current_full(request, file_name="current-full.csv"): diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 9b9ed569ee..483db9da63 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -31,7 +31,7 @@ ) from registrar.views.domains_json import get_domains_json from registrar.views.utility import always_404 -from api.views import available, get_current_federal, get_current_full +from api.views import available, rdap, get_current_federal, get_current_full DOMAIN_REQUEST_NAMESPACE = views.DomainRequestWizard.URL_NAMESPACE @@ -183,6 +183,7 @@ path("openid/", include("djangooidc.urls")), path("request/", include((domain_request_urls, DOMAIN_REQUEST_NAMESPACE))), path("api/v1/available/", available, name="available"), + path("api/v1/rdap/", rdap, name="rdap"), path("api/v1/get-report/current-federal", get_current_federal, name="get-current-federal"), path("api/v1/get-report/current-full", get_current_full, name="get-current-full"), path( From 3f7dbd5f6e77591339b6b9cad96cdabafc5f8c2e Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:57:12 -0700 Subject: [PATCH 02/11] Add updated RDAP API endpoint --- src/api/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 87bb9f5894..5c3b4c525c 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -2,7 +2,7 @@ from django.apps import apps from django.views.decorators.http import require_http_methods -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.utils.safestring import mark_safe from registrar.templatetags.url_helpers import public_site_url @@ -19,7 +19,7 @@ DOMAIN_FILE_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv" -RDAP_URL = "https://rdap.cloudflareregistry.com/rdap/domain/" +RDAP_URL = "https://rdap.cloudflareregistry.com/rdap/domain/{domain}" DOMAIN_API_MESSAGES = { @@ -108,8 +108,8 @@ def rdap(request, domain=""): Domain = apps.get_model("registrar.Domain") domain = request.GET.get("domain", "") - rdap_response = requests.get(DOMAIN_FILE_URL, domain) - return rdap_response + rdap_data = requests.get(RDAP_URL.format(domain=domain)).json() + return JsonResponse(rdap_data) @require_http_methods(["GET"]) From 919fbbfd2fab88b7ad68e2fb310e5e8a3011c2e2 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:01:53 -0700 Subject: [PATCH 03/11] Handle domains without TLD --- src/api/tests/test_rdap.py | 40 ++++++++++++++++++++++++++++++++++++++ src/api/views.py | 4 ++++ 2 files changed, 44 insertions(+) create mode 100644 src/api/tests/test_rdap.py diff --git a/src/api/tests/test_rdap.py b/src/api/tests/test_rdap.py new file mode 100644 index 0000000000..bd442be90f --- /dev/null +++ b/src/api/tests/test_rdap.py @@ -0,0 +1,40 @@ +"""Test the domain rdap lookup API.""" + +import json + +from django.contrib.auth import get_user_model +from django.test import RequestFactory +from django.test import TestCase + +from ..views import available, check_domain_available +from .common import less_console_noise +from registrar.utility.errors import GenericError, GenericErrorCodes +from unittest.mock import call + +from epplibwrapper import ( + commands, +) + +API_BASE_PATH = "/api/v1/rdap/?domain=" + +class RdapAPITest(MockEppLib): + """Test that the RDAP API can be called as expected.""" + + def setUp(self): + super().setUp() + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + title = "title" + phone = "8080102431" + self.user = get_user_model().objects.create( + username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone + ) + + def test_rdap_get(self): + self.client.force_login(self.user) + response = self.client.get(API_BASE_PATH + "whitehouse.gov") + self.assertContains(response, "RDAP") + response_object = json.loads(response.content) + self.assertIn("RDAP", response_object) \ No newline at end of file diff --git a/src/api/views.py b/src/api/views.py index 5c3b4c525c..50a5375178 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -108,6 +108,10 @@ def rdap(request, domain=""): Domain = apps.get_model("registrar.Domain") domain = request.GET.get("domain", "") + # If inputted domain doesn't have a TLD, append .gov to it + if "." not in domain: + domain = f"{domain}.gov" + rdap_data = requests.get(RDAP_URL.format(domain=domain)).json() return JsonResponse(rdap_data) From 5439894fa3bf9834a420864d42889d5ddd241310 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:37:36 -0700 Subject: [PATCH 04/11] Add RDAP tests --- src/api/tests/test_available.py | 2 +- src/api/tests/test_rdap.py | 47 ++++++++++++++++++++++++++++----- src/registrar/tests/common.py | 17 ++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 35b2e39712..6c7a0a6c31 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -135,7 +135,7 @@ def setUp(self): self.user = get_user_model().objects.create( username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone ) - + def test_available_get(self): self.client.force_login(self.user) response = self.client.get(API_BASE_PATH + "nonsense") diff --git a/src/api/tests/test_rdap.py b/src/api/tests/test_rdap.py index bd442be90f..cd7654f08e 100644 --- a/src/api/tests/test_rdap.py +++ b/src/api/tests/test_rdap.py @@ -6,8 +6,9 @@ from django.test import RequestFactory from django.test import TestCase -from ..views import available, check_domain_available +from ..views import available, check_domain_available, rdap from .common import less_console_noise +from registrar.tests.common import MockRdapLib, MockEppLib from registrar.utility.errors import GenericError, GenericErrorCodes from unittest.mock import call @@ -17,10 +18,42 @@ API_BASE_PATH = "/api/v1/rdap/?domain=" -class RdapAPITest(MockEppLib): - """Test that the RDAP API can be called as expected.""" - def setUp(self): +class RDapViewTest(MockRdapLib): + """Test that the RDAP view function works as expected""" + + def setUp(self): + super().setUp() + self.user = get_user_model().objects.create(username="username") + self.factory = RequestFactory() + + def test_rdap_get_no_tld(self): + """RDAP API successfully fetches RDAP for domain without a TLD""" + request = self.factory.get(API_BASE_PATH + "whitehouse") + request.user = self.user + response = rdap(request, domain="whitehouse") + # contains the right text + self.assertContains(response, "rdap") + # can be parsed into JSON with appropriate keys + response_object = json.loads(response.content) + self.assertIn("rdapConformance", response_object) + + def test_rdap_invalid_domain(self): + """RDAP API accepts invalid domain queries and returns JSON response + with appropriate error codes""" + request = self.factory.get(API_BASE_PATH + "whitehouse.com") + request.user = self.user + response = rdap(request, domain="whitehouse.com") + + self.assertContains(response, "errorCode") + response_object = json.loads(response.content) + self.assertIn("errorCode", response_object) + + +class RdapAPITest(MockRdapLib): + """Test that the API can be called as expected.""" + + def setUp(self): super().setUp() username = "test_user" first_name = "First" @@ -33,8 +66,10 @@ def setUp(self): ) def test_rdap_get(self): + """Can call RDAP API""" self.client.force_login(self.user) response = self.client.get(API_BASE_PATH + "whitehouse.gov") - self.assertContains(response, "RDAP") + self.assertContains(response, "rdap") response_object = json.loads(response.content) - self.assertIn("RDAP", response_object) \ No newline at end of file + self.assertIn("rdapConformance", response_object) + diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 4edfbe680c..fd052420e4 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1029,6 +1029,23 @@ def generic_domain_object(domain_type, object_name): return domain_request +class MockRdapLib(TestCase): + class fakedRdapObject(object): + def __init__( + self, + rdapConformance=..., + description=..., + errorCode=..., + title=... + ): + self.rdapConformance = rdapConformance + self.description = description + self.errorCode = errorCode + self.title = title + + + + class MockEppLib(TestCase): class fakedEppObject(object): """""" From 23cb5681eaad213b4674d325936a4eb831391190 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:47:28 -0700 Subject: [PATCH 05/11] Remove unused mock RDAP lib class since RDAP not used in registrar views --- src/api/tests/test_rdap.py | 5 ++--- src/registrar/tests/common.py | 17 ----------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/api/tests/test_rdap.py b/src/api/tests/test_rdap.py index cd7654f08e..b2a4745143 100644 --- a/src/api/tests/test_rdap.py +++ b/src/api/tests/test_rdap.py @@ -8,7 +8,6 @@ from ..views import available, check_domain_available, rdap from .common import less_console_noise -from registrar.tests.common import MockRdapLib, MockEppLib from registrar.utility.errors import GenericError, GenericErrorCodes from unittest.mock import call @@ -19,7 +18,7 @@ API_BASE_PATH = "/api/v1/rdap/?domain=" -class RDapViewTest(MockRdapLib): +class RDapViewTest(TestCase): """Test that the RDAP view function works as expected""" def setUp(self): @@ -50,7 +49,7 @@ def test_rdap_invalid_domain(self): self.assertIn("errorCode", response_object) -class RdapAPITest(MockRdapLib): +class RdapAPITest(TestCase): """Test that the API can be called as expected.""" def setUp(self): diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index fd052420e4..4edfbe680c 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1029,23 +1029,6 @@ def generic_domain_object(domain_type, object_name): return domain_request -class MockRdapLib(TestCase): - class fakedRdapObject(object): - def __init__( - self, - rdapConformance=..., - description=..., - errorCode=..., - title=... - ): - self.rdapConformance = rdapConformance - self.description = description - self.errorCode = errorCode - self.title = title - - - - class MockEppLib(TestCase): class fakedEppObject(object): """""" From 155e7367c15d7ae4dea123e40f885dd9bf61b1d9 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:47:49 -0700 Subject: [PATCH 06/11] Fix linter --- src/api/tests/test_available.py | 2 +- src/api/tests/test_rdap.py | 3 +-- src/api/views.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 6c7a0a6c31..35b2e39712 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -135,7 +135,7 @@ def setUp(self): self.user = get_user_model().objects.create( username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone ) - + def test_available_get(self): self.client.force_login(self.user) response = self.client.get(API_BASE_PATH + "nonsense") diff --git a/src/api/tests/test_rdap.py b/src/api/tests/test_rdap.py index b2a4745143..665e613970 100644 --- a/src/api/tests/test_rdap.py +++ b/src/api/tests/test_rdap.py @@ -63,7 +63,7 @@ def setUp(self): self.user = get_user_model().objects.create( username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone ) - + def test_rdap_get(self): """Can call RDAP API""" self.client.force_login(self.user) @@ -71,4 +71,3 @@ def test_rdap_get(self): self.assertContains(response, "rdap") response_object = json.loads(response.content) self.assertIn("rdapConformance", response_object) - diff --git a/src/api/views.py b/src/api/views.py index 50a5375178..4f06702662 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -103,8 +103,7 @@ def available(request, domain=""): @require_http_methods(["GET"]) @login_not_required def rdap(request, domain=""): - """TODO: Write description - """ + """TODO: Write description""" Domain = apps.get_model("registrar.Domain") domain = request.GET.get("domain", "") From a435eb1f687fc8f65e6400bbdd05fb187d8f5b9a Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:50:57 -0700 Subject: [PATCH 07/11] Fix typo --- src/api/tests/test_rdap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/tests/test_rdap.py b/src/api/tests/test_rdap.py index 665e613970..beb40835b9 100644 --- a/src/api/tests/test_rdap.py +++ b/src/api/tests/test_rdap.py @@ -18,7 +18,7 @@ API_BASE_PATH = "/api/v1/rdap/?domain=" -class RDapViewTest(TestCase): +class RdapViewTest(TestCase): """Test that the RDAP view function works as expected""" def setUp(self): From d6201fc31f56c827f0e27cba6d569cd3be8cb630 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:02:37 -0700 Subject: [PATCH 08/11] Add description to rdap endpoint --- src/api/tests/test_rdap.py | 9 +-------- src/api/views.py | 3 +-- src/registrar/tests/test_url_auth.py | 1 + 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/api/tests/test_rdap.py b/src/api/tests/test_rdap.py index beb40835b9..789a99152f 100644 --- a/src/api/tests/test_rdap.py +++ b/src/api/tests/test_rdap.py @@ -6,14 +6,7 @@ from django.test import RequestFactory from django.test import TestCase -from ..views import available, check_domain_available, rdap -from .common import less_console_noise -from registrar.utility.errors import GenericError, GenericErrorCodes -from unittest.mock import call - -from epplibwrapper import ( - commands, -) +from ..views import rdap API_BASE_PATH = "/api/v1/rdap/?domain=" diff --git a/src/api/views.py b/src/api/views.py index 4f06702662..116762307d 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -103,8 +103,7 @@ def available(request, domain=""): @require_http_methods(["GET"]) @login_not_required def rdap(request, domain=""): - """TODO: Write description""" - Domain = apps.get_model("registrar.Domain") + """Returns JSON dictionary of a domain's RDAP data from Cloudflare API""" domain = request.GET.get("domain", "") # If inputted domain doesn't have a TLD, append .gov to it diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 284ec76381..1cd2d13840 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -116,6 +116,7 @@ class TestURLAuth(TestCase): "/api/v1/available/", "/api/v1/get-report/current-federal", "/api/v1/get-report/current-full", + "/api/v1/rdap/", "/health", ] From a4c4bba610e9d631c00ec32c2ea301cbde47e750 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:10:08 -0700 Subject: [PATCH 09/11] Add cache time --- src/api/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/api/views.py b/src/api/views.py index 116762307d..788bd79f52 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -100,8 +100,11 @@ def available(request, domain=""): return json_response + @require_http_methods(["GET"]) @login_not_required +# Since we cache domain RDAP data, cache time may need to be re-evaluated this if we encounter any memory issues +@ttl_cache(ttl=600) def rdap(request, domain=""): """Returns JSON dictionary of a domain's RDAP data from Cloudflare API""" domain = request.GET.get("domain", "") From ea4b4e2dae385a197d4768e13a6da0bef5e0365f Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:18:47 -0700 Subject: [PATCH 10/11] Add timeout to RDAP request --- src/api/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 788bd79f52..3391b6e1d9 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -100,7 +100,6 @@ def available(request, domain=""): return json_response - @require_http_methods(["GET"]) @login_not_required # Since we cache domain RDAP data, cache time may need to be re-evaluated this if we encounter any memory issues @@ -113,7 +112,7 @@ def rdap(request, domain=""): if "." not in domain: domain = f"{domain}.gov" - rdap_data = requests.get(RDAP_URL.format(domain=domain)).json() + rdap_data = requests.get(RDAP_URL.format(domain=domain), timeout=5).json() return JsonResponse(rdap_data) From 2ab29b9f438db7fde5220390018dad532bdea266 Mon Sep 17 00:00:00 2001 From: Erin Song <121973038+erinysong@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:35:56 -0700 Subject: [PATCH 11/11] Remove old availability URL implementation --- src/api/views.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 3391b6e1d9..a7b4bde756 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -18,7 +18,6 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientHelper -DOMAIN_FILE_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv" RDAP_URL = "https://rdap.cloudflareregistry.com/rdap/domain/{domain}" @@ -42,30 +41,6 @@ } -# this file doesn't change that often, nor is it that big, so cache the result -# in memory for ten minutes -@ttl_cache(ttl=600) -def _domains(): - """Return a list of the current .gov domains. - - Fetch a file from DOMAIN_FILE_URL, parse the CSV for the domain, - lowercase everything and return the list. - """ - DraftDomain = apps.get_model("registrar.DraftDomain") - # 5 second timeout - file_contents = requests.get(DOMAIN_FILE_URL, timeout=5).text - domains = set() - # skip the first line - for line in file_contents.splitlines()[1:]: - # get the domain before the first comma - domain = line.split(",", 1)[0] - # sanity-check the string we got from the file here - if DraftDomain.string_could_be_domain(domain): - # lowercase everything when we put it in domains - domains.add(domain.lower()) - return domains - - def check_domain_available(domain): """Return true if the given domain is available.