Skip to content

Commit

Permalink
Merge pull request #62 from ministryofjustice/dp-3182-views-and-forms…
Browse files Browse the repository at this point in the history
…-tests

Dp-3182-views-and-forms-tests
  • Loading branch information
murdo-moj authored Feb 13, 2024
2 parents 851be05 + fe15015 commit 64ab1dc
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 112 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: test
# based on https://jacobian.org/til/github-actions-poetry/

on:
pull_request:
types: [opened, edited, reopened, synchronize]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v2
with:
python-version: 3.11.1
- name: cache poetry install
uses: actions/cache@v2
with:
path: ~/.local
key: poetry-1.7.1-0
- uses: snok/install-poetry@v1
with:
version: 1.7.1
virtualenvs-create: true
virtualenvs-in-project: true
- name: cache deps
id: cache-deps
uses: actions/cache@v2
with:
path: .venv
key: pydeps-${{ hashFiles('**/poetry.lock') }}
- run: poetry install --no-interaction --no-root
if: steps.cache-deps.outputs.cache-hit != 'true'
- run: poetry install --no-interaction
- name: test with coverage
run: poetry run pytest --cov
4 changes: 0 additions & 4 deletions core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@
WSGI_APPLICATION = "core.wsgi.application"


# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases


# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

Expand Down
6 changes: 0 additions & 6 deletions home/forms/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
from django import forms
from urllib.parse import urlencode

# from home.helper import get_domain_list


def get_domain_choices():
"""Make API call to obtain domain choices"""
Expand Down Expand Up @@ -60,10 +58,6 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial["sort"] = "relevance"

def clean_query(self):
"""Example clean method to apply custom validation to input fields"""
return str(self.cleaned_data["query"]).capitalize()

def encode_without_filter(self, filter_to_remove):
"""Preformat hrefs to drop individual filters"""
# Deepcopy the cleaned data dict to avoid modifying it inplace
Expand Down
2 changes: 1 addition & 1 deletion home/service/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _get_context(self) -> dict[str, Any]:
if self.form.is_bound:
label_clear_href = {
filter.split(":")[-1]: self.form.encode_without_filter(filter)
for filter in self.form.cleaned_data.get("domains")
for filter in self.form.cleaned_data.get("domains", [])
}
else:
label_clear_href = None
Expand Down
7 changes: 3 additions & 4 deletions home/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.http import Http404, HttpResponseBadRequest
from django.shortcuts import render

from home.forms.search import SearchForm
Expand Down Expand Up @@ -31,8 +31,7 @@ def search_view(request, page: str = "1"):
# Populated search scenario
form = SearchForm(request.GET)
if not form.is_valid():
print("form error on validation")
print(form.errors)
return HttpResponseBadRequest(form.errors)

search_service = SearchService(form=form, page=page)
return render(request, "search.html", search_service.context)
233 changes: 231 additions & 2 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ ministryofjustice-data-platform-catalogue = "^0.10.0"
markdown = "^3.5.2"
python-dotenv = "^1.0.1"
faker = "^22.6.0"
selenium = "^4.17.2"
pytest-django = "^4.8.0"
pytest-cov = "^4.1.0"

[tool.poetry.group.dev] # dev group definition

Expand All @@ -25,3 +28,7 @@ pre-commit = "^3.6.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "core.settings"
python_files = ["test_*.py", "*_test.py", "testing/python/*.py"]
Empty file removed tests/__init__.py
Empty file.
78 changes: 78 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from random import choice
from unittest.mock import MagicMock, patch

import pytest
from data_platform_catalogue.client import BaseCatalogueClient
from data_platform_catalogue.search_types import (FacetOption, ResultType,
SearchFacets, SearchResponse,
SearchResult)
from django.test import Client
from faker import Faker

fake = Faker()


def generate_page(page_size=20):
"""
Generate a fake search page
"""
results = []
for _ in range(page_size):
results.append(
SearchResult(
id=fake.unique.name(),
result_type=choice(
(ResultType.DATA_PRODUCT, ResultType.TABLE)),
name=fake.name(),
description=fake.paragraphs(),
)
)
return results


def generate_options(num_options=5):
"""
Generate a list of options for the search facets
"""
results = []
for _ in range(num_options):
results.append(
FacetOption(
value=fake.name(),
label=fake.name(),
count=fake.random_int(min=0, max=100),
)
)
return results


@pytest.fixture(autouse=True)
def client():
client = Client()
return client


@pytest.fixture(autouse=True)
def mock_catalogue():
patcher = patch("home.service.base.GenericService._get_catalogue_client")
mock_fn = patcher.start()
mock_catalogue = MagicMock(spec=BaseCatalogueClient)
mock_fn.return_value = mock_catalogue
mock_search_response(
mock_catalogue, page_results=generate_page(), total_results=100)
mock_search_facets_response(mock_catalogue, domains=generate_options())

yield mock_catalogue

patcher.stop()


def mock_search_response(mock_catalogue, total_results=0, page_results=()):
search_response = SearchResponse(
total_results=total_results, page_results=page_results)
mock_catalogue.search.return_value = search_response


def mock_search_facets_response(mock_catalogue, domains):
mock_catalogue.search_facets.return_value = SearchFacets(
{"domains": domains})
Empty file removed tests/home/__init__.py
Empty file.
Empty file removed tests/home/views/__init__.py
Empty file.
95 changes: 0 additions & 95 deletions tests/home/views/test_views.py

This file was deleted.

47 changes: 47 additions & 0 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from home.forms.search import SearchForm
import pytest


@pytest.fixture
def valid_form():
valid_form = SearchForm(
data={
"query": "test",
"domains": ["urn:li:domain:HMCTS"],
"sort": "ascending",
"clear_filter": False,
"clear_label": False,
}
)
assert valid_form.is_valid()

return valid_form


class TestSearchForm:
def test_query_field_length(self):
over_100_characters = "a" * 101
assert not SearchForm(data={"query": over_100_characters}).is_valid()

def test_domain_is_from_domain_list_false(self):
assert not SearchForm(data={"domains": ["fake"]}).is_valid()

def test_sort_is_from_sort_list_false(self):
assert not SearchForm(data={"sort": ["fake"]}).is_valid()

def test_all_fields_nullable(self):
assert SearchForm(data={}).is_valid()

def test_form_encode_without_filter_for_one_filter(self, valid_form):
assert (valid_form.encode_without_filter("urn:li:domain:HMCTS") ==
"?query=test&sort=ascending&clear_filter=False&clear_label=False")

def test_form_encode_without_filter_for_two_filters(self):
two_filter_form = SearchForm(data={
"query": "test",
"domains": ["urn:li:domain:HMCTS", "urn:li:domain:HMPPS"]
})
two_filter_form.is_valid()

assert (two_filter_form.encode_without_filter("urn:li:domain:HMCTS") ==
"?query=test&domains=urn%3Ali%3Adomain%3AHMPPS&sort=&clear_filter=False&clear_label=False")
48 changes: 48 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

from data_platform_catalogue.search_types import SearchResponse
from django.urls import reverse


class TestSearchView:
"""
Test the view renders the correct context depending on query parameters and session
"""

def test_renders_200(self, client):
response = client.get(reverse("home:search"), data={})
assert response.status_code == 200

def test_exposes_results(self, client):
response = client.get(reverse("home:search"), data={})
assert response.status_code == 200
assert len(response.context["results"]) == 20

def test_exposes_empty_query(self, client):
response = client.get(reverse("home:search"), data={})
assert response.status_code == 200
assert response.context["form"].cleaned_data["query"] == ""

def test_exposes_query(self, client):
response = client.get(reverse("home:search"), data={"query": "foo"})
assert response.status_code == 200
assert response.context["form"].cleaned_data["query"] == "foo"

def test_bad_form(self, client):
response = client.get(reverse("home:search"), data={"domains": "fake"})
assert response.status_code == 400


class TestDetailsView:
def test_details(self, client):
response = client.get(
reverse("home:details", kwargs={
"id": "urn:li:dataProduct:common-platform"})
)
assert response.status_code == 200

def test_details_not_found(self, client, mock_catalogue):
mock_catalogue.search.return_value = SearchResponse(
total_results=0, page_results=[]
)
response = client.get(reverse("home:details", kwargs={"id": "fake"}))
assert response.status_code == 404

0 comments on commit 64ab1dc

Please sign in to comment.