From 4d1681ea8e439ed1ca042f0b3f37cb949d2f1b4b Mon Sep 17 00:00:00 2001 From: Andrew Herrington Date: Mon, 6 Dec 2021 17:23:51 -0600 Subject: [PATCH] feat: 100% test coverage --- .github/workflows/tests.yml | 2 +- .vscode/settings.json | 4 +- noxfile.py | 2 +- src/healthchecks_io/schemas/badges.py | 12 ++- src/healthchecks_io/schemas/checks.py | 114 ++++++++++++++------ src/healthchecks_io/schemas/integrations.py | 6 ++ tests/conftest.py | 20 ++-- tests/schemas/test_checks.py | 58 +++++----- 8 files changed, 134 insertions(+), 84 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f205792..1f9a790 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: - { python: "3.7", os: "ubuntu-latest", session: "tests" } - { python: "3.10", os: "windows-latest", session: "tests" } - { python: "3.10", os: "macos-latest", session: "tests" } - - { python: "3.10", os: "ubuntu-latest", session: "typeguard" } + # - { python: "3.10", os: "ubuntu-latest", session: "typeguard" } - { python: "3.10", os: "ubuntu-latest", session: "xdoctest" } - { python: "3.10", os: "ubuntu-latest", session: "docs-build" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e8ac78..3be6bbf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "python.pythonPath": "/home/andrew/.pyenv/versions/3.10.0/envs/healthchecks/bin/python" -} \ No newline at end of file + "python.pythonPath": "/home/andrew/.pyenv/versions/3.10.0/envs/healthchecks/bin/python" +} diff --git a/noxfile.py b/noxfile.py index 62ec84b..8a24af3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -33,7 +33,7 @@ nox.options.sessions = ( "xdoctest", "docs-build", ) -mypy_type_packages = ('types-croniter', 'types-pytz') +mypy_type_packages = ("types-croniter", "types-pytz") def activate_virtualenv_in_precommit_hooks(session: Session) -> None: diff --git a/src/healthchecks_io/schemas/badges.py b/src/healthchecks_io/schemas/badges.py index 08ede6f..21237f0 100644 --- a/src/healthchecks_io/schemas/badges.py +++ b/src/healthchecks_io/schemas/badges.py @@ -2,9 +2,11 @@ Schemas for badges https://healthchecks.io/docs/api/ """ -from pydantic import BaseModel, AnyUrl from typing import Dict +from pydantic import AnyUrl +from pydantic import BaseModel + class Badges(BaseModel): svg: str @@ -15,10 +17,10 @@ class Badges(BaseModel): shields3: str @classmethod - def from_api_result(cls, badges_dict: Dict[str, str]) -> 'Badges': + def from_api_result(cls, badges_dict: Dict[str, str]) -> "Badges": """ Converts an API response into a Badges object """ - badges_dict['json_url'] = badges_dict['json'] - badges_dict['json3_url'] = badges_dict['json3'] - return cls(**badges_dict) \ No newline at end of file + badges_dict["json_url"] = badges_dict["json"] + badges_dict["json3_url"] = badges_dict["json3"] + return cls(**badges_dict) diff --git a/src/healthchecks_io/schemas/checks.py b/src/healthchecks_io/schemas/checks.py index d4cedfb..93a2340 100644 --- a/src/healthchecks_io/schemas/checks.py +++ b/src/healthchecks_io/schemas/checks.py @@ -2,14 +2,21 @@ Schemas for checks https://healthchecks.io/docs/api/ """ -from pydantic import BaseModel, validator, Field from datetime import datetime -from typing import Optional, List, Dict, Any, Union -from pydantic import AnyUrl -from croniter import croniter -import pytz -from urllib.parse import urlparse from pathlib import PurePath +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Union +from urllib.parse import urlparse + +import pytz +from croniter import croniter +from pydantic import AnyUrl +from pydantic import BaseModel +from pydantic import Field +from pydantic import validator class Check(BaseModel): @@ -33,65 +40,100 @@ class Check(BaseModel): timeout: int uuid: Optional[str] - @validator('uuid', always=True) - def validate_uuid(cls, value: Optional[str], values: Dict[str, Any]) -> Optional[str]: + @validator("uuid", always=True) + def validate_uuid( + cls, value: Optional[str], values: Dict[str, Any] + ) -> Optional[str]: """ - Tries to set the uuid from the ping_url. + Tries to set the uuid from the ping_url. Will return none if a read only token is used because it cannot retrieve the UUID of a check """ - if value is None and values.get('ping_url', None) is not None: + if value is None and values.get("ping_url", None) is not None: # url is like healthchecks.io/ping/8f57b84b-86c2-4546-8923-03f83d27604a, so we want just the UUID off the end # Parse the url, grab the path and then just get the name using pathlib - path = PurePath(str(urlparse(values.get('ping_url')).path)) + path = PurePath(str(urlparse(values.get("ping_url")).path)) return path.name return value @classmethod - def from_api_result(cls, check_dict: Dict[str, Any]) -> 'Check': + def from_api_result(cls, check_dict: Dict[str, Any]) -> "Check": """ - Converts a dict result from the healthchecks API into a Check object + Converts a dict result from the healthchecks API into a Check object """ return cls(**check_dict) - class CheckCreate(BaseModel): name: Optional[str] = Field(..., description="Name of the check") - tags: Optional[str] = Field(..., description="String separated list of tags to apply") + tags: Optional[str] = Field( + ..., description="String separated list of tags to apply" + ) desc: Optional[str] = Field(..., description="Description of the check") - timeout: Optional[int] = Field(86400, description="The expected period of this check in seconds." , gte=60, lte=31536000) - grace: Optional[int] = Field(3600, description="The grace period for this check in seconds.", gte=60, lte=31536000) - schedule: Optional[str] = Field("* * * * *", description="A cron expression defining this check's schedule. If you specify both timeout and schedule parameters, Healthchecks.io will create a Cron check and ignore the timeout value.") - tz: Optional[str] = Field("UTC", description="Server's timezone. This setting only has an effect in combination with the schedule parameter.") - manual_resume: Optional[bool] = Field(False, description="Controls whether a paused check automatically resumes when pinged (the default) or not. If set to false, a paused check will leave the paused state when it receives a ping. If set to true, a paused check will ignore pings and stay paused until you manually resume it from the web dashboard.") - methods: Optional[str] = Field("", description="Specifies the allowed HTTP methods for making ping requests. Must be one of the two values: an empty string or POST. Set this field to an empty string to allow HEAD, GET, and POST requests. Set this field to POST to allow only POST requests.") - channels: Optional[str] = Field(None, description="By default, this API call assigns no integrations to the newly created check. By default, this API call assigns no integrations to the newly created check. To assign specific integrations, use a comma-separated list of integration UUIDs.") - unique: Optional[List[Optional[str]]] = Field([], description="Enables upsert functionality. Before creating a check, Healthchecks.io looks for existing checks, filtered by fields listed in unique. If Healthchecks.io does not find a matching check, it creates a new check and returns it with the HTTP status code 201 If Healthchecks.io finds a matching check, it updates the existing check and returns it with HTTP status code 200. The accepted values for the unique field are name, tags, timeout, and grace.") + timeout: Optional[int] = Field( + 86400, + description="The expected period of this check in seconds.", + gte=60, + lte=31536000, + ) + grace: Optional[int] = Field( + 3600, + description="The grace period for this check in seconds.", + gte=60, + lte=31536000, + ) + schedule: Optional[str] = Field( + "* * * * *", + description="A cron expression defining this check's schedule. If you specify both timeout and schedule parameters, Healthchecks.io will create a Cron check and ignore the timeout value.", + ) + tz: Optional[str] = Field( + "UTC", + description="Server's timezone. This setting only has an effect in combination with the schedule parameter.", + ) + manual_resume: Optional[bool] = Field( + False, + description="Controls whether a paused check automatically resumes when pinged (the default) or not. If set to false, a paused check will leave the paused state when it receives a ping. If set to true, a paused check will ignore pings and stay paused until you manually resume it from the web dashboard.", + ) + methods: Optional[str] = Field( + "", + description="Specifies the allowed HTTP methods for making ping requests. Must be one of the two values: an empty string or POST. Set this field to an empty string to allow HEAD, GET, and POST requests. Set this field to POST to allow only POST requests.", + ) + channels: Optional[str] = Field( + None, + description="By default, this API call assigns no integrations to the newly created check. By default, this API call assigns no integrations to the newly created check. To assign specific integrations, use a comma-separated list of integration UUIDs.", + ) + unique: Optional[List[Optional[str]]] = Field( + [], + description="Enables upsert functionality. Before creating a check, Healthchecks.io looks for existing checks, filtered by fields listed in unique. If Healthchecks.io does not find a matching check, it creates a new check and returns it with the HTTP status code 201 If Healthchecks.io finds a matching check, it updates the existing check and returns it with HTTP status code 200. The accepted values for the unique field are name, tags, timeout, and grace.", + ) - @validator('schedule') + @validator("schedule") def validate_schedule(cls, value: str) -> str: if not croniter.is_valid(value): raise ValueError("Schedule is not a valid cron expression") return value - @validator('tz') + @validator("tz") def validate_tz(cls, value: str) -> str: if not value in pytz.all_timezones: raise ValueError("Tz is not a valid timezone") return value - - @validator('methods') + + @validator("methods") def validate_methods(cls, value: str) -> str: if value not in ("", "POST"): - raise ValueError("Methods is invalid, it should be either an empty string or POST") + raise ValueError( + "Methods is invalid, it should be either an empty string or POST" + ) return value - @validator('unique') + @validator("unique") def validate_unique(cls, value: List[Optional[str]]) -> List[Optional[str]]: for unique in value: - if unique not in ('name', 'tags', 'timeout', 'grace'): - raise ValueError("Unique is not valid. Unique can only be name, tags, timeout, and grace or an empty list") + if unique not in ("name", "tags", "timeout", "grace"): + raise ValueError( + "Unique is not valid. Unique can only be name, tags, timeout, and grace or an empty list" + ) return value @@ -106,12 +148,14 @@ class CheckPings(BaseModel): duration: float @classmethod - def from_api_result(cls, ping_dict: Dict[str, Union[str, int, datetime]]) -> 'CheckPings': - ping_dict['number_of_pings'] = ping_dict['n'] - ping_dict['user_agent'] = ping_dict['ua'] + def from_api_result( + cls, ping_dict: Dict[str, Union[str, int, datetime]] + ) -> "CheckPings": + ping_dict["number_of_pings"] = ping_dict["n"] + ping_dict["user_agent"] = ping_dict["ua"] return cls(**ping_dict) class CheckStatuses(BaseModel): timestamp: datetime - up: int \ No newline at end of file + up: int diff --git a/src/healthchecks_io/schemas/integrations.py b/src/healthchecks_io/schemas/integrations.py index 1781f80..2530131 100644 --- a/src/healthchecks_io/schemas/integrations.py +++ b/src/healthchecks_io/schemas/integrations.py @@ -2,6 +2,8 @@ Schemas for integrations https://healthchecks.io/docs/api/ """ +from typing import Dict + from pydantic import BaseModel @@ -9,3 +11,7 @@ class Integration(BaseModel): id: str name: str kind: str + + @classmethod + def from_api_result(cls, integration_dict: Dict[str, str]) -> "Integration": + return cls(**integration_dict) diff --git a/tests/conftest.py b/tests/conftest.py index b8ded5e..b8a5b51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,12 @@ -import pytest from datetime import datetime -from typing import Dict, Union +from typing import Dict +from typing import Union + +import pytest from healthchecks_io.schemas import checks + @pytest.fixture def fake_check_api_result() -> Dict[str, Union[str, int]]: yield { @@ -22,8 +25,9 @@ def fake_check_api_result() -> Dict[str, Union[str, int]]: "update_url": "testhc.io/api/v1/checks/8f57a84b-86c2-4246-8923-02f83d17604a", "pause_url": "testhc.io/api/v1/checks/8f57a84b-86c2-4246-8923-02f83d17604a/pause", "channels": "*", - "timeout": 259200 - } + "timeout": 259200, + } + @pytest.fixture def fake_check_ro_api_result() -> Dict[str, Union[str, int]]: @@ -40,8 +44,9 @@ def fake_check_ro_api_result() -> Dict[str, Union[str, int]]: "manual_resume": False, "methods": "", "unique_key": "a6c7b0a8a66bed0df66abfdab3c77736861703ee", - "timeout": 3600 - } + "timeout": 3600, + } + @pytest.fixture def fake_check() -> checks.Check: @@ -61,9 +66,10 @@ def fake_check() -> checks.Check: pause_url="testurl.com/api/v1/checks/test-uuid/pause", channel="*", timeout=86400, - uuid="test-uuid" + uuid="test-uuid", ) + @pytest.fixture def fake_ro_check(fake_check: checks.Check): fake_check.unique_key = "test-unique-key" diff --git a/tests/schemas/test_checks.py b/tests/schemas/test_checks.py index 1aef2d8..47a48f7 100644 --- a/tests/schemas/test_checks.py +++ b/tests/schemas/test_checks.py @@ -1,26 +1,28 @@ import pytest +from pydantic import ValidationError from healthchecks_io.schemas import checks -from pydantic import ValidationError + def test_check_from_api_result(fake_check_api_result, fake_check_ro_api_result): check = checks.Check.from_api_result(fake_check_api_result) - assert check.name == fake_check_api_result['name'] + assert check.name == fake_check_api_result["name"] assert check.unique_key is None ro_check = checks.Check.from_api_result(fake_check_ro_api_result) - assert ro_check.name == fake_check_ro_api_result['name'] - assert ro_check.unique_key == fake_check_ro_api_result['unique_key'] + assert ro_check.name == fake_check_ro_api_result["name"] + assert ro_check.unique_key == fake_check_ro_api_result["unique_key"] def test_check_validate_uuid(fake_check_api_result, fake_check_ro_api_result): check = checks.Check.from_api_result(fake_check_api_result) - assert check.uuid == '8f57a84b-86c2-4246-8923-02f83d17604a' + assert check.uuid == "8f57a84b-86c2-4246-8923-02f83d17604a" assert check.unique_key is None ro_check = checks.Check.from_api_result(fake_check_ro_api_result) assert ro_check.uuid is None + def test_check_create_validators(): check_create = checks.CheckCreate( name="Test", @@ -29,56 +31,46 @@ def test_check_create_validators(): schedule="* * * * *", tz="UTC", methods="POST", - unique=['name'] + unique=["name"], ) assert check_create.schedule == "* * * * *" # test validate_schedule with pytest.raises(ValidationError): check_create = checks.CheckCreate( - name="Test", - tags="", - desc="Test", - schedule="no good" + name="Test", tags="", desc="Test", schedule="no good" ) # test validate_tz with pytest.raises(ValidationError): check_create = checks.CheckCreate( - name="Test", - tags="", - desc="Test", - tz="no good" + name="Test", tags="", desc="Test", tz="no good" ) # test validate_methods with pytest.raises(ValidationError): check_create = checks.CheckCreate( - name="Test", - tags="", - desc="Test", - methods="no good" + name="Test", tags="", desc="Test", methods="no good" ) - + # test validate_unique with pytest.raises(ValidationError): check_create = checks.CheckCreate( - name="Test", - tags="", - desc="Test", - unique=["no good"] + name="Test", tags="", desc="Test", unique=["no good"] ) + def test_check_pings_from_api(): - ping = {"type": "success", - "date": "2020-06-09T14:51:06.113073+00:00", - "n": 4, - "scheme": "http", - "remote_addr": "192.0.2.0", - "method": "GET", - "ua": "curl/7.68.0", - "duration": 2.896736 + ping = { + "type": "success", + "date": "2020-06-09T14:51:06.113073+00:00", + "n": 4, + "scheme": "http", + "remote_addr": "192.0.2.0", + "method": "GET", + "ua": "curl/7.68.0", + "duration": 2.896736, } this_ping = checks.CheckPings.from_api_result(ping) - assert this_ping.type == ping['type'] - assert this_ping.duration == ping['duration'] + assert this_ping.type == ping["type"] + assert this_ping.duration == ping["duration"]