From 55094bbec3aa6c467141af4355e629c04eed405b Mon Sep 17 00:00:00 2001 From: Phil Reese Date: Mon, 12 Feb 2024 11:33:00 -0500 Subject: [PATCH] EREGCSC-2499 -- Enable subject lookup for non-logged in users (#1167) * feat: remove IsAuthenticatedMixin from PolicyRepo django view * feat: disable doc type switcher and check Public if unauthenticated * feat: add navigation guard to remove type query param if unauth * feat: hide internal resource count in subj TOC if unAuth * chore: restrict nav guard to policy-repository * chore: tweak pluralization of resource number label in subject TOC * feat: never cache content-search API responses on policy repository * chore: remove unused import * feat: change content-search response behavior if unAuth internal req * feat: correct logic for response from content-search to unAuth user * test: update content_search test_search tests * test: remove space from string to assert against * feat: add login banner to policy repo sidebar * test: add e2e test coverage when not logged in * refactor: convert confusing nested ternary to if/else blocks * test: a11y check for logged out view * test: update logout test * test: assert that type query param is removed from URL when logged out * chore: update comment --- .../content_search/tests/test_search.py | 6 +- solution/backend/content_search/views.py | 12 +- .../regulations/policy_repository.html | 2 +- .../regulations/views/policy_repository.py | 4 +- .../cypress/e2e/policy-repository.spec.cy.js | 149 +++++++++++++++--- .../css/scss/_application_settings.scss | 8 + .../css/scss/partials/_sidebar_right.scss | 8 - .../ui/regulations/eregs-vite/src/App.vue | 5 + .../DocumentTypeSelector.vue | 42 +++-- .../policy-repository/PolicySidebar.vue | 45 ++++++ .../policy-repository/SubjectTOC.vue | 22 ++- .../ui/regulations/eregs-vite/src/main.js | 12 ++ .../eregs-vite/src/views/PolicyRepository.vue | 25 +-- 13 files changed, 280 insertions(+), 60 deletions(-) diff --git a/solution/backend/content_search/tests/test_search.py b/solution/backend/content_search/tests/test_search.py index 6d9e68aa90..a420bc10ff 100644 --- a/solution/backend/content_search/tests/test_search.py +++ b/solution/backend/content_search/tests/test_search.py @@ -70,14 +70,16 @@ def setUp(self): index_group(UploadedFile.objects.all()) def test_no_query_not_logged_in(self): + response = self.client.get("/v3/content-search/?resource-type=internal") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) response = self.client.get("/v3/content-search/?resource-type=external") self.assertEqual(response.status_code, status.HTTP_200_OK) data = get_paginated_data(response) self.assertEqual(data['count'], 6) - response = self.client.get("/v3/content-search/?resource-type=internal") + response = self.client.get("/v3/content-search/?resource-type=all") data = get_paginated_data(response) self.assertEqual(data['count'], 6) - response = self.client.get("/v3/content-search/?resource-type=all") + response = self.client.get("/v3/content-search/") data = get_paginated_data(response) self.assertEqual(data['count'], 6) diff --git a/solution/backend/content_search/views.py b/solution/backend/content_search/views.py index 0280debe1f..42d4cc02e5 100644 --- a/solution/backend/content_search/views.py +++ b/solution/backend/content_search/views.py @@ -8,6 +8,7 @@ from django.urls import reverse from drf_spectacular.utils import extend_schema from rest_framework import viewsets +from rest_framework.exceptions import NotAuthenticated from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -82,11 +83,16 @@ def list(self, request): repo_category_prefetch = AbstractRepoCategory.objects.all().select_subclasses()\ .select_related("repositorysubcategory__parent") - # If they are not authenticated they csan only get 'external' documents - if not request.user.is_authenticated or resource_type == 'external': - query = query.filter(resource_type='external') + # If they are not authenticated and the resource type is internal, raise an error + if not request.user.is_authenticated: + if resource_type == 'internal': + raise NotAuthenticated() + else: + query = query.filter(resource_type='external') elif resource_type == 'internal': query = query.filter(resource_type='internal') + elif resource_type == 'external': + query = query.filter(resource_type='external') context = self.get_serializer_context() context['content_id'] = True diff --git a/solution/backend/regulations/templates/regulations/policy_repository.html b/solution/backend/regulations/templates/regulations/policy_repository.html index 70f6757fc1..d865110dd8 100644 --- a/solution/backend/regulations/templates/regulations/policy_repository.html +++ b/solution/backend/regulations/templates/regulations/policy_repository.html @@ -18,11 +18,11 @@ {% endblock %} {% block body %} -
{ fixture: "policy-docs.json", }).as("subjectFiles"); cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/"}); + cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); cy.visit("/policy-repository/?q=mock"); cy.injectAxe(); cy.wait("@subjectFiles").then((interception) => { @@ -42,15 +42,67 @@ Cypress.Commands.add("getPolicyDocs", ({ username, password }) => { describe("Policy Repository", () => { beforeEach(_beforeEach); - it("shows the custom eua login screen when you visit /policy-repository/ without logging in", () => { + it("shows the custom eua login screen when you visit /policy-repository/ and click 'sign in'", () => { cy.viewport("macbook-15"); cy.visit("/policy-repository/"); + cy.get(".div__login-sidebar a").click(); cy.url().should("include", "/login"); }); - it("should show the policy repository page when logged in", () => { + it("should show only public items when logged out", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.visit("/policy-repository/"); + + cy.injectAxe(); + + cy.get("#loginIndicator").should("not.be.visible"); + cy.get(".doc-type__toggle fieldset > div") + .eq(0) + .find("input") + .should("be.checked") + .and("be.disabled"); + cy.get(".doc-type__toggle fieldset > div") + .eq(1) + .find("input") + .should("not.be.checked") + .and("be.disabled"); + + cy.checkAccessibility(); + + cy.get( + ".subj-toc__list li[data-testid=subject-toc-li-3] a" + ).scrollIntoView(); + cy.get( + ".subj-toc__list li[data-testid=subject-toc-li-3] div.subj-toc-li__count" + ) + .should("be.visible") + .and("have.text", "0 public resources"); + cy.get( + ".subj-toc__list li[data-testid=subject-toc-li-63] a" + ).scrollIntoView(); + cy.get(".subj-toc__list li[data-testid=subject-toc-li-63] a") + .should("have.text", " Managed Care ") + .click({ force: true }); + cy.url().should("include", "/policy-repository?subjects=63"); + cy.get(".subject__heading") + .should("exist") + .and("have.text", "Managed Care"); + cy.url().should("include", "/policy-repository?subjects=63"); + }) + + it("should strip document-type query parameter from URL when not logged in", () => { + cy.viewport("macbook-15"); + cy.visit("/policy-repository/?type=internal"); + cy.url().should("not.include", "type"); + }) + + it("should show public and internal items when logged in", () => { + cy.viewport("macbook-15"); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.url().should("include", "/policy-repository/"); cy.get("#loginIndicator").should("be.visible"); @@ -62,7 +114,7 @@ describe("Policy Repository", () => { ".subj-toc__list li[data-testid=subject-toc-li-3] div.subj-toc-li__count" ) .should("be.visible") - .and("have.text", "0 public and 1 internal resources "); + .and("have.text", "0 public and 1 internal resources"); cy.get( ".subj-toc__list li[data-testid=subject-toc-li-63] a" ).scrollIntoView(); @@ -139,7 +191,11 @@ describe("Policy Repository", () => { it("should make a successful request to the content-search endpoint", () => { cy.intercept("**/v3/content-search/?**").as("files"); cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.url().should("include", "/policy-repository/"); cy.get(".subj-toc__list li:nth-child(1) a").click({ force: true }); @@ -150,7 +206,11 @@ describe("Policy Repository", () => { it("loads the correct subject and search query when the URL is changed", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.url().should("include", "/policy-repository/"); @@ -271,7 +331,11 @@ describe("Policy Repository", () => { fixture: "policy-docs.json", }).as("subjectFiles"); cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository/"); cy.get(".doc-type__toggle fieldset > div") @@ -304,7 +368,9 @@ describe("Policy Repository", () => { "include", "/policy-repository?type=internal&subjects=3&q=test%20search" ); - cy.get(".search-form .form-helper-text .search-suggestion").should("not.exist"); + cy.get(".search-form .form-helper-text .search-suggestion").should( + "not.exist" + ); cy.get(".document__subjects a") .eq(0) .should("have.text", " Access to Services "); @@ -334,7 +400,11 @@ describe("Policy Repository", () => { it("should display correct subject ID number in the URL if one is included in the URL on load and different one is selected via the Subject Selector", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository/?subjects=77"); cy.url().should("include", "/policy-repository/?subjects=77"); cy.get(`button[data-testid=remove-subject-77]`).should("exist"); @@ -356,7 +426,11 @@ describe("Policy Repository", () => { it("should filter the subject list when a search term is entered into the subject filter", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository/"); cy.injectAxe(); @@ -407,7 +481,11 @@ describe("Policy Repository", () => { it("should display and fetch the correct search query on load if it is included in URL", () => { cy.intercept("**/v3/content-search/?q=test**").as("qFiles"); cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository/?q=test"); cy.wait("@qFiles").then((interception) => { expect(interception.response.statusCode).to.eq(200); @@ -418,7 +496,11 @@ describe("Policy Repository", () => { it("should have a Documents to Show checkbox list", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.get(".doc-type__toggle-container h3").should( "have.text", @@ -448,7 +530,11 @@ describe("Policy Repository", () => { it("should show only the Table of Contents if both or neither checkboxes are checked", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.get(".subj-toc__container").should("exist"); cy.get(".doc-type__toggle fieldset > div") @@ -476,7 +562,11 @@ describe("Policy Repository", () => { it("should not make a request to the content-search endpoint if both checkboxes are checked on load", () => { cy.intercept("**/v3/content-search/**").as("contentSearch"); cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.wait(2000); cy.get("@contentSearch.all").then((interception) => { @@ -486,7 +576,11 @@ describe("Policy Repository", () => { it("goes to another SPA page from the policy repository page", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.clickHeaderLink({ page: "Resources", screen: "wide" }); cy.url().should("include", "/resources"); @@ -548,10 +642,25 @@ describe("Policy Repository", () => { it("returns you to the custom eua login page when you log out", () => { cy.viewport("macbook-15"); - cy.eregsLogin({ username, password, landingPage: "/policy-repository/" }); + cy.eregsLogin({ + username, + password, + landingPage: "/policy-repository/", + }); cy.visit("/policy-repository"); cy.get("#logout").click(); - cy.url().should("include", "/login"); + cy.url().should("include", "/policy-repository"); + cy.get("#loginIndicator").should("not.be.visible"); + cy.get(".doc-type__toggle fieldset > div") + .eq(0) + .find("input") + .should("be.checked") + .and("be.disabled"); + cy.get(".doc-type__toggle fieldset > div") + .eq(1) + .find("input") + .should("not.be.checked") + .and("be.disabled"); }); }); @@ -593,7 +702,9 @@ describe("Policy Repository Search", () => { force: true, }); cy.url().should("include", "/policy-repository/search?q=test%20search"); - cy.get(".search-form .form-helper-text .search-suggestion").should("not.exist"); + cy.get(".search-form .form-helper-text .search-suggestion").should( + "not.exist" + ); cy.wait("@queriedFiles").then((interception) => { expect(interception.response.statusCode).to.eq(200); }); diff --git a/solution/ui/regulations/css/scss/_application_settings.scss b/solution/ui/regulations/css/scss/_application_settings.scss index d4bc6b8e53..417be3d3c5 100644 --- a/solution/ui/regulations/css/scss/_application_settings.scss +++ b/solution/ui/regulations/css/scss/_application_settings.scss @@ -575,3 +575,11 @@ select { height: 32px; padding: 0 12px; } + +.div__login-sidebar { + background-color: $secondary_background_color; + border: 1px solid $border_color; + border-radius: 0.25rem; + padding: 0.5rem; + margin: 1rem 0; +} diff --git a/solution/ui/regulations/css/scss/partials/_sidebar_right.scss b/solution/ui/regulations/css/scss/partials/_sidebar_right.scss index 6066ff4805..76d8b88b44 100644 --- a/solution/ui/regulations/css/scss/partials/_sidebar_right.scss +++ b/solution/ui/regulations/css/scss/partials/_sidebar_right.scss @@ -35,14 +35,6 @@ aside.right-sidebar { margin-bottom: 0px !important; } - .div__login-sidebar { - background-color: $secondary_background_color; - border: 1px solid $border_color; - border-radius: 0.25rem; - padding: 0.5rem; - margin: 1rem 0; - } - .label__container { display: flex; margin-top: 1rem; diff --git a/solution/ui/regulations/eregs-vite/src/App.vue b/solution/ui/regulations/eregs-vite/src/App.vue index 39ad914aff..ace9aba74b 100644 --- a/solution/ui/regulations/eregs-vite/src/App.vue +++ b/solution/ui/regulations/eregs-vite/src/App.vue @@ -13,6 +13,10 @@ export default { type: String, default: "/about/", }, + customLoginUrl: { + type: String, + default: "", + }, customUrl: { type: String, default: "", @@ -54,6 +58,7 @@ export default { -import { ref, onMounted, onUnmounted, watch } from "vue"; +import { ref, inject, onMounted, onUnmounted, watch } from "vue"; import { useRouter, useRoute } from "vue-router/composables"; import _isArray from "lodash/isArray"; @@ -13,20 +13,33 @@ const $router = useRouter(); const { type: typeParams } = $route.query; -// v-model with a ref to control if the checkbox displays as checked or not -const boxesArr = +const isAuthenticated = inject("isAuthenticated"); + +// v-model with a ref to control if the checkbox is displayed as checked or not +let boxesArr; + +if (!isAuthenticated) { + boxesArr = ["external"]; +} else if ( _isUndefined(typeParams) || typeParams === "all" || typeParams.includes("all") - ? [...DOCUMENT_TYPES] - : _isArray(typeParams) - ? typeParams - : [typeParams]; +) { + boxesArr = [...DOCUMENT_TYPES]; +} else if (_isArray(typeParams)) { + boxesArr = typeParams; +} else { + boxesArr = [typeParams]; +} const checkedBoxes = ref(boxesArr); // onClick event to set the $route const toggleDocumentType = (clickedType) => { + if (!isAuthenticated) { + return; + } + const { type: queryCloneType, ...queryClone } = $route.query; const refTypesBeforeClick = checkedBoxes.value; @@ -49,7 +62,11 @@ const toggleDocumentType = (clickedType) => { watch( () => $route.query, - async (newQueryParams, oldQueryParams) => { + async (newQueryParams) => { + if (!isAuthenticated) { + return; + } + const { type: newTypeParams } = newQueryParams; // "all" is only set when clicking a subject chip, so it is safe to use here @@ -61,6 +78,10 @@ watch( // popstate to update the checkbox on back/forward click const onPopState = () => { + if (!isAuthenticated) { + return; + } + const { type: queryTypes } = $route.query; if (_isUndefined(queryTypes) || queryTypes === "all") { @@ -92,13 +113,16 @@ onUnmounted(() => window.removeEventListener("resize", onPopState)); name="checkbox_choices" type="checkbox" :value="type" + :disabled="!isAuthenticated" @click="toggleDocumentType(type)" />
diff --git a/solution/ui/regulations/eregs-vite/src/components/policy-repository/PolicySidebar.vue b/solution/ui/regulations/eregs-vite/src/components/policy-repository/PolicySidebar.vue index 641565ece3..f75cb3a136 100644 --- a/solution/ui/regulations/eregs-vite/src/components/policy-repository/PolicySidebar.vue +++ b/solution/ui/regulations/eregs-vite/src/components/policy-repository/PolicySidebar.vue @@ -1,3 +1,41 @@ + +