Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace mocks with VCR casettes for client testing #14

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.9', '3.10', '3.11']
django: ['3.2', '4.0']
python: ['3.11', '3.12']
django: ['4.2']
name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }})

steps:
Expand Down Expand Up @@ -51,7 +51,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
python-version: '3.11'

- name: Build sdist and wheel
run: |
Expand Down
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Open Forms Client (for Django)
:Version: 0.4.0
:Source: https://github.com/open-formulieren/open-forms-client-django
:Keywords: Open Forms, Client, Django
:PythonVersion: 3.9 - 3.11
:DjangoVersion: 3.2 - 4.0
:PythonVersion: 3.11+
:DjangoVersion: 4.2

|build-status| |code-quality| |black| |coverage|

Expand Down Expand Up @@ -37,8 +37,8 @@ Installation
Requirements
------------

* Python 3.9 or newer
* Django 3.2 or newer
* Python 3.11 or newer
* Django 4.2


Install
Expand Down
671 changes: 671 additions & 0 deletions fixtures/cassettes/forms.yaml

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions fixtures/cassettes/invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.32.3
method: GET
uri: https://open-forms.test.maykin.opengem.nl/api/v2/public/forms
response:
body:
string: '{"type":"https://open-forms.test.maykin.opengem.nl/fouten/AuthenticationFailed/","code":"authentication_failed","title":"Ongeldige
authenticatiegegevens.","status":401,"detail":"Ongeldige token.","instance":"urn:uuid:b3f40157-9442-486c-aaba-037f9342c703"}'
headers:
Allow:
- GET, HEAD, OPTIONS
Cache-Control:
- max-age=0, no-cache, no-store, must-revalidate, private
Connection:
- keep-alive
Content-Language:
- nl
Content-Length:
- '255'
Content-Type:
- application/json
Cross-Origin-Opener-Policy:
- same-origin
Date:
- Wed, 31 Jul 2024 08:05:44 GMT
Expires:
- Wed, 31 Jul 2024 08:05:44 GMT
Pragma:
- no-cache
Referrer-Policy:
- same-origin
Set-Cookie:
- csrftoken=rDnuLzLrAUGpIgp2gYxelwj7UGZEqCV6; expires=Wed, 30 Jul 2025 08:05:44
GMT; Max-Age=31449600; Path=/; SameSite=None; Secure
- openforms_sessionid=8ev8ldwpo3vi2qsnw0l176htbcrcbg54; expires=Wed, 31 Jul
2024 08:10:44 GMT; HttpOnly; Max-Age=300; Path=/; SameSite=None; Secure
Strict-Transport-Security:
- max-age=63072000
Vary:
- Cookie, Origin, Accept-Language
WWW-Authenticate:
- Token
X-CSRFToken:
- 5dZg56rok3RheSgkWWqoIhnpkBNVnweDmGcAGv2FKNnwMYvc2KNsTDwm47CpDYZz
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
X-Session-Expires-In:
- '300'
status:
code: 401
message: Unauthorized
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.32.3
method: GET
uri: https://open-forms.test.maykin.opengem.nl/api/v2/forms/9c65ab49-2635-4c8d-ac22-aaca67044a6c
response:
body:
string: '{"type":"https://open-forms.test.maykin.opengem.nl/fouten/AuthenticationFailed/","code":"authentication_failed","title":"Ongeldige
authenticatiegegevens.","status":401,"detail":"Ongeldige token.","instance":"urn:uuid:d545fdc9-8186-456a-999b-c0c388fe922a"}'
headers:
Allow:
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Cache-Control:
- max-age=0, no-cache, no-store, must-revalidate, private
Connection:
- keep-alive
Content-Language:
- nl
Content-Length:
- '255'
Content-Type:
- application/json
Cross-Origin-Opener-Policy:
- same-origin
Date:
- Wed, 31 Jul 2024 08:05:45 GMT
Expires:
- Wed, 31 Jul 2024 08:05:45 GMT
Pragma:
- no-cache
Referrer-Policy:
- same-origin
Set-Cookie:
- csrftoken=erCNNjLn8PKuLjVPK1ZDw3a4Rv7PUQS9; expires=Wed, 30 Jul 2025 08:05:45
GMT; Max-Age=31449600; Path=/; SameSite=None; Secure
- openforms_sessionid=qm2mjv9kxo2xcckzvb0flhy5nt5d3zbz; expires=Wed, 31 Jul
2024 08:10:45 GMT; HttpOnly; Max-Age=300; Path=/; SameSite=None; Secure
Strict-Transport-Security:
- max-age=63072000
Vary:
- Cookie, Origin, Accept-Language
WWW-Authenticate:
- Token
X-CSRFToken:
- SDRW8cYDR5y2Ti1Ihv4D8Uns3RlWJ44eWUjzLlzQPK8murMnRmT6uNnmKciBtKMd
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
X-Session-Expires-In:
- '300'
status:
code: 401
message: Unauthorized
version: 1
27 changes: 17 additions & 10 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ install_requires =
django-solo
requests
tests_require =
requests-mock
pytest
pytest-django
tox
isort
black
flake8
isort
pytest
pytest-django
python-decouple
pyyaml
requests
requests-mock
time-machine
tox
vcrpy

[options.packages.find]
include =
Expand All @@ -49,14 +53,17 @@ include =

[options.extras_require]
tests =
requests-mock
pytest
pytest-django
tox
isort
black
flake8
isort
pytest
pytest-django
python-decouple
pyyaml
requests-mock
time-machine
tox
vcrpy
pep8 = flake8
coverage = pytest-cov
docs =
Expand Down
17 changes: 17 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
from decouple import config

from openformsclient.client import Client

API_ROOT = config("OFC_API_ROOT", "https://open-forms.test.maykin.opengem.nl/api/v2/")
API_TOKEN = config("OFC_API_TOKEN", "hush-hush")


@pytest.fixture
def client():
return Client(api_root=API_ROOT, api_token=API_TOKEN, client_timeout=2)


@pytest.fixture
def bogus_client():
return Client(api_root=API_ROOT, api_token="bogus", client_timeout=2)
Empty file added tests/data/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions tests/data/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
test_form = {
"uuid": "9c65ab49-2635-4c8d-ac22-aaca67044a6c",
"name": "Open Inwoner ZGW Dev",
"internalName": "",
"slug": "open-inwoner-zgw-dev",
}
152 changes: 67 additions & 85 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,73 @@
from unittest.mock import patch
from urllib.parse import urljoin

from django.test import TestCase

import requests_mock
import pytest
import vcr
from requests.exceptions import HTTPError

from openformsclient.client import Client

from .data.forms import test_form

CASSETTE_PATH_FORMS = "fixtures/cassettes/forms.yaml"
CASSETTE_PATH_INVALID = "fixtures/cassettes/invalid.yaml"
VCR_DEFAULTS = {
"record_mode": "none", # use new_episodes to record casettes
"filter_headers": ["authorization"],
}


def test_client_has_config(client):
bogus_client = Client("", "", "")

assert client.has_config()
assert not bogus_client.has_config()


@vcr.use_cassette(CASSETTE_PATH_FORMS, **VCR_DEFAULTS)
def test_client_is_healthy(client):
health, msg = client.is_healthy()
assert health
assert msg == ""


@vcr.use_cassette(CASSETTE_PATH_FORMS, **VCR_DEFAULTS)
def test_get_forms(client):
results = client.get_forms()["results"]

assert test_form in results


@vcr.use_cassette(CASSETTE_PATH_INVALID, **VCR_DEFAULTS)
def test_get_forms_unauthorized(bogus_client):
url = urljoin(bogus_client.api_root, "public/forms")
msg = f"401 Client Error: Unauthorized for url: {url}"

with pytest.raises(HTTPError, match=msg):
bogus_client.get_forms()


@vcr.use_cassette(CASSETTE_PATH_FORMS, **VCR_DEFAULTS)
def test_get_single_form(client):
result = client.get_form(uuid_or_slug=test_form["uuid"])

assert result["uuid"] == test_form["uuid"]
assert result["name"] == test_form["name"]


@vcr.use_cassette(CASSETTE_PATH_FORMS, **VCR_DEFAULTS)
def test_get_single_form_not_found(client):
bogus_uuid = "aaaa-bbbb-cccc-dddd"
url = urljoin(client.api_root, f"forms/{bogus_uuid}")
msg = f"404 Client Error: Not Found for url: {url}"

with pytest.raises(HTTPError, match=msg):
client.get_form(uuid_or_slug=bogus_uuid)


@vcr.use_cassette(CASSETTE_PATH_INVALID, **VCR_DEFAULTS)
def test_get_single_form_unauthorized(bogus_client):
url = urljoin(bogus_client.api_root, f"forms/{test_form['uuid']}")
msg = f"401 Client Error: Unauthorized for url: {url}"

@requests_mock.Mocker()
class ClientTests(TestCase):
def setUp(self):
self.api_root = "https://example.com/api/v2/"
self.api_token = "token"
self.client_timeout = 2
self.client = Client(self.api_root, self.api_token, self.client_timeout)

def test_has_config(self, m):
self.assertTrue(self.client.has_config())
self.assertFalse(Client("", "", "").has_config())

def test_is_healthy(self, m):
m.head(
f"{self.api_root}public/forms",
request_headers={"Authorization": f"Token {self.api_token}"},
)

health, msg = self.client.is_healthy()
self.assertTrue(health)
self.assertEqual(msg, "")

def test_is_healthy_invalid_response(self, m):
m.head(f"{self.api_root}public/forms", status_code=500) # Doesn't really matter
m.get(f"{self.api_root}public/forms", text="Woops")

health, msg = self.client.is_healthy()
self.assertFalse(health)
self.assertEqual(msg, "Server did not return a valid response (HTTP 500).")

def test_is_healthy_invalid_token(self, m):
m.head(f"{self.api_root}public/forms", status_code=401)
m.get(
f"{self.api_root}public/forms",
json={
"type": "https://example.com/fouten/AuthenticationFailed/",
"code": "authentication_failed",
"title": "Ongeldige authenticatiegegevens.",
"status": 401,
"detail": "Ongeldige token.",
"instance": "urn:uuid:8dddcb04-a412-451a-a7c5-f77d1aef36f5",
},
)

health, msg = self.client.is_healthy()
self.assertFalse(health)
self.assertEqual(msg, "Ongeldige token.")

def test_get_forms(self, m):
m.get(f"{self.api_root}public/forms", json=[])

result = self.client.get_forms()
self.assertListEqual(result, [])

def test_get_forms_with_error(self, m):
m.get(f"{self.api_root}public/forms", status_code=401)

with self.assertRaises(HTTPError):
self.client.get_forms()

def test_get_form(self, m):
m.get(f"{self.api_root}forms/myform", json={})

result = self.client.get_form("myform")
self.assertDictEqual(result, {})

def test_get_form_with_error(self, m):
m.get(f"{self.api_root}forms/myform", status_code=401)

with self.assertRaises(HTTPError):
self.client.get_form("myform")

def test_request_uses_configured_timeout(self, m):
with patch("openformsclient.client.requests.request") as mock_request:
self.client.get_form("myform")
mock_request.assert_called_with(
"get",
"https://example.com/api/v2/forms/myform",
headers={"Authorization": "Token token"},
timeout=2,
)
with pytest.raises(HTTPError, match=msg):
bogus_client.get_form(uuid_or_slug=test_form["uuid"])
Loading
Loading