diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..2e17dd84 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/core/settings.py b/core/settings.py index 8813a6ed..ae416197 100644 --- a/core/settings.py +++ b/core/settings.py @@ -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 diff --git a/home/forms/search.py b/home/forms/search.py index ee4dae15..1939673f 100644 --- a/home/forms/search.py +++ b/home/forms/search.py @@ -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""" @@ -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 diff --git a/home/service/search.py b/home/service/search.py index f6a7afa5..7e4a54c4 100644 --- a/home/service/search.py +++ b/home/service/search.py @@ -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 diff --git a/home/views.py b/home/views.py index 45db819d..5937a65d 100644 --- a/home/views.py +++ b/home/views.py @@ -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 @@ -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) diff --git a/poetry.lock b/poetry.lock index e48a14a1..1b541012 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "acryl-datahub" @@ -710,6 +710,70 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.4.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "croniter" version = "1.3.15" @@ -1344,6 +1408,17 @@ gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "humanfriendly" version = "10.0" @@ -2008,6 +2083,20 @@ files = [ [package.extras] dev = ["black", "mypy", "pytest"] +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "23.2" @@ -2273,6 +2362,18 @@ files = [ {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, ] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "8.0.0" @@ -2293,6 +2394,42 @@ pluggy = ">=1.3.0,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-django" +version = "4.8.0" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, + {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["Django", "django-configurations (>=2.0)"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -2409,6 +2546,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2829,6 +2967,24 @@ botocore = ">=1.33.2,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] +[[package]] +name = "selenium" +version = "4.17.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "selenium-4.17.2-py3-none-any.whl", hash = "sha256:5aee79026c07985dc1b0c909f34084aa996dfe5b307602de9016d7a621a473f2"}, + {file = "selenium-4.17.2.tar.gz", hash = "sha256:d43d6972e516855fb242ef9ce4ce759057b115070e702e7b1c1032fe7b38d87b"}, +] + +[package.dependencies] +certifi = ">=2021.10.8" +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +typing_extensions = ">=4.9.0" +urllib3 = {version = ">=1.26,<3", extras = ["socks"]} + [[package]] name = "sentry-sdk" version = "1.40.0" @@ -2901,6 +3057,28 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "soupsieve" version = "2.5" @@ -3103,6 +3281,40 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "trio" +version = "0.24.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.8" +files = [ + {file = "trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c"}, + {file = "trio-0.24.0.tar.gz", hash = "sha256:ffa09a74a6bf81b84f8613909fb0beaee84757450183a7a2e0b47b455c0cac5d"}, +] + +[package.dependencies] +attrs = ">=20.1.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.11.1" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + +[package.dependencies] +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "typing-compat" version = "0.1.0" @@ -3162,6 +3374,9 @@ files = [ {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, ] +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] @@ -3295,6 +3510,20 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + [[package]] name = "yarl" version = "1.9.4" @@ -3416,4 +3645,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "0ae06695e2ac3dedebe33805f20a25c4cf72a2395ce547f5cbb66f4f1f8cc6e7" +content-hash = "a4d98d014ad783d7aa3e211393315b4bafc61e1fead6d2be706872b1a0858793" diff --git a/pyproject.toml b/pyproject.toml index 68694726..53ed3e6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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"] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..48849efc --- /dev/null +++ b/tests/conftest.py @@ -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}) diff --git a/tests/home/__init__.py b/tests/home/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/home/views/__init__.py b/tests/home/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/home/views/test_views.py b/tests/home/views/test_views.py deleted file mode 100644 index 7d0dddf2..00000000 --- a/tests/home/views/test_views.py +++ /dev/null @@ -1,95 +0,0 @@ -from random import choice -from unittest import skip -from unittest.mock import MagicMock, patch - -from data_platform_catalogue.client import BaseCatalogueClient -from data_platform_catalogue.search_types import (FacetOption, ResultType, - SearchFacets, SearchResponse, - SearchResult) -from django.test import SimpleTestCase -from django.urls import reverse -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 - - -class SearchViewTests(SimpleTestCase): - """ - Test the view renders the correct context depending on query parameters and session - """ - - def setUp(self): - self.patcher = patch("home.views.get_catalogue_client") - mock_fn = self.patcher.start() - self.mock_client = MagicMock(spec=BaseCatalogueClient) - mock_fn.return_value = self.mock_client - self.mock_search_response( - page_results=generate_page(), total_results=100) - self.mock_search_facets_response(domains=generate_options()) - - def tearDown(self): - self.patcher.stop() - - def mock_search_response(self, total_results=0, page_results=()): - search_response = SearchResponse( - total_results=total_results, page_results=page_results - ) - self.mock_client.search.return_value = search_response - - def mock_search_facets_response(self, domains): - self.mock_client.search_facets.return_value = SearchFacets( - {"domains": domains}) - - def test_renders_200(self): - response = self.client.get(reverse("home:search"), data={}) - self.assertEqual(response.status_code, 200) - - def test_exposes_results(self): - response = self.client.get(reverse("home:search"), data={}) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context["results"]), 20) - - def test_exposes_empty_query(self): - response = self.client.get(reverse("home:search"), data={}) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context["query"], "") - - def test_exposes_query(self): - response = self.client.get( - reverse("home:search"), data={"query": "foo"}) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context["query"], "foo") diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 00000000..51addcf6 --- /dev/null +++ b/tests/test_forms.py @@ -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") diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..1f46c9ba --- /dev/null +++ b/tests/test_views.py @@ -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