From 1c6abce9b9770f408565e4c95fe8e408385e23d5 Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Thu, 18 Jul 2024 18:44:50 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E2=80=94=20Add=20new=20check=20typ?= =?UTF-8?q?es:=20json-contains,=20json-has=20and=20json-is=20(fix=20#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- argos/checks/checks.py | 90 +++++++++++++++++++++++++++++++++++++++ argos/config-example.yaml | 16 +++++++ docs/checks.md | 13 +++++- pyproject.toml | 1 + 5 files changed, 121 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2681e..8308fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,11 @@ - βœ… β€” Add mypy test - ✨ β€” Add new check type: status-in - 🩹 β€” Close menu after rescheduling non-ok checks (#55) -- ✨ β€” Add new check types: headers-contain and headers-have +- ✨ β€” Add new check types: headers-contain and headers-have (#56) - ✨ β€” Add command to test email configuration (!66) - πŸ’„ β€” Enhance the mobile view (!67) - ✨ β€” Allow to run Argos in a subfolder (i.e. not on /) (#59) +- ✨ β€” Add new check types: json-contains, json-has and json-is (#57) ## 0.2.2 diff --git a/argos/checks/checks.py b/argos/checks/checks.py index 8894c84..15c038f 100644 --- a/argos/checks/checks.py +++ b/argos/checks/checks.py @@ -3,6 +3,8 @@ import json from datetime import datetime +from jsonpointer import resolve_pointer, JsonPointerException + from argos.checks.base import ( BaseCheck, ExpectedIntValue, @@ -121,6 +123,94 @@ class HTTPBodyContains(BaseCheck): return self.response(status=self.expected in response.text) +class HTTPJsonContains(BaseCheck): + """Checks that JSON response contains the expected structure + (without checking the value)""" + + config = "json-contains" + expected_cls = ExpectedStringValue + + async def run(self) -> dict: + # XXX Get the method from the task + task = self.task + response = await self.http_client.request( + method="get", url=task.url, timeout=60 + ) + + obj = response.json() + + status = True + for pointer in json.loads(self.expected): + try: + resolve_pointer(obj, pointer) + except JsonPointerException: + status = False + break + + return self.response( + status=status, + expected=self.expected, + retrieved=json.dumps(obj), + ) + + +class HTTPJsonHas(BaseCheck): + """Checks that JSON response contains the expected structure and values""" + + config = "json-has" + expected_cls = ExpectedStringValue + + async def run(self) -> dict: + # XXX Get the method from the task + task = self.task + response = await self.http_client.request( + method="get", url=task.url, timeout=60 + ) + + obj = response.json() + + status = True + for pointer, exp_value in json.loads(self.expected).items(): + try: + value = resolve_pointer(obj, pointer) + if value != exp_value: + status = False + break + except JsonPointerException: + status = False + break + + return self.response( + status=status, + expected=self.expected, + retrieved=json.dumps(obj), + ) + + +class HTTPJsonIs(BaseCheck): + """Checks that JSON response is the exact expected JSON object""" + + config = "json-is" + expected_cls = ExpectedStringValue + + async def run(self) -> dict: + # XXX Get the method from the task + task = self.task + response = await self.http_client.request( + method="get", url=task.url, timeout=60 + ) + + obj = response.json() + + status = response.json() == json.loads(self.expected) + + return self.response( + status=status, + expected=self.expected, + retrieved=json.dumps(obj), + ) + + class SSLCertificateExpiration(BaseCheck): """Checks that the SSL certificate will not expire soon.""" diff --git a/argos/config-example.yaml b/argos/config-example.yaml index 286b9fc..b5558ee 100644 --- a/argos/config-example.yaml +++ b/argos/config-example.yaml @@ -99,10 +99,26 @@ websites: - 401 - 301 # Check that the response contains this headers and values + # It’s VERY important to respect the 4 spaces indentation here! # The name of the headers is case insensitive - headers-have: content-encoding: "gzip" content-type: "text/html" + - path: "/my-stats.json" + checks: + # Check that JSON response contains the expected structure + - json-contains: + - /foo/bar/0 + - /foo/bar/1 + - /timestamp + # Check that JSON response contains the expected structure and values + # It’s VERY important to respect the 4 spaces indentation here! + - json-has: + /maintenance: false + /productname: "Nextcloud" + # Check that JSON response is the exact expected JSON object + # The order of the items in the object does not matter. + - json-is: '{"foo": "bar", "baz": 42}' - domain: "https://munin.example.org" frequency: "20m" paths: diff --git a/docs/checks.md b/docs/checks.md index 2175f56..0b62ad9 100644 --- a/docs/checks.md +++ b/docs/checks.md @@ -12,7 +12,10 @@ These checks are the most basic ones. They simply check that the response from t | `status-in` | Check that the returned status code is in the list of codes you expect. |
status-in:
- 200
- 302
| | `body-contains` | Check that the returned body contains a given string. | `body-contains: "Hello world"` | | `headers-contain` | Check that the response contains the expected headers. |
headers-contain:
- "content-encoding"
- "content-type"
| -| `headers-have` | Check that the response contains the expected headers. |
headers-have:
content-encoding: "gzip"
content-type: "text/html"
| +| `headers-have` | Check that the response contains the expected headers with the expected value. |
headers-have:
content-encoding: "gzip"
content-type: "text/html"
| +| `json-contains` | Check that JSON response contains the expected structure. |
json-contains:
- /foo/bar/0
- /timestamp
| +| `json-has` | Check that JSON response contains the expected structure and values. |
json-has:
/maintenance: false
/productname: "Nextcloud"
| +| `json-is` | Check that JSON response is the exact expected JSON object | `json-is: '{"foo": "bar", "baz": 42}'`| ```{code-block} yaml --- @@ -36,6 +39,14 @@ caption: argos-config.yaml - headers-have: content-encoding: "gzip" content-type: "text/html" + - json-contains: + - /foo/bar/0 + - /timestamp + # It’s VERY important to respect the 4 spaces indentation here! + - json-has: + /maintenance: false + /productname: "Nextcloud" + - json-is: '{"foo": "bar", "baz": 42}' ``` ## SSL certificate expiration diff --git a/pyproject.toml b/pyproject.toml index 4fcd402..119f643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "fastapi-login>=1.10.0,<2", "httpx>=0.25,<0.27.0", "Jinja2>=3.0,<4", + "jsonpointer>=3.0,<4", "passlib>=1.7.4,<2", "psycopg2-binary>=2.9,<3", "pydantic[email]>=2.4,<3",