Skip to content

Commit

Permalink
Merge pull request daleal#15 from daleal/master
Browse files Browse the repository at this point in the history
Release version 0.0.3
  • Loading branch information
daleal authored Mar 20, 2021
2 parents 03c6962 + fae70fd commit f06afd0
Show file tree
Hide file tree
Showing 14 changed files with 336 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = zum/constants.py
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
<p align="center">
<a href="https://github.com/daleal/zum">
<img src="https://zum.daleal.dev/assets/zum-250x250.png">
</a>
</p>

<h1 align="center">Zum</h1>

<p align="center">
Expand Down Expand Up @@ -57,7 +63,7 @@ This indicates to `zum` that the API endpoints are located at `http://localhost:

#### `endpoints`

The `endpoints` key contains every endpoint that you want to be able to use from `zum`. Each endpoint should also have a `route` value, a `method` value and may include a `params` value and a `body` value. Let's see an example:
The `endpoints` key contains every endpoint that you want to be able to access from `zum`. Each endpoint should also have a `route` value, a `method` value and may include a `params` value and a `body` value. Let's see an example:

```toml
[endpoints.my-endpoint-name]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "zum"
version = "0.0.2"
version = "0.0.3"
description = "Stop writing scripts to test your APIs. Call them as CLIs instead."
license = "MIT"
authors = ["Daniel Leal <[email protected]>"]
Expand Down
Empty file added tests/configs/__init__.py
Empty file.
117 changes: 117 additions & 0 deletions tests/configs/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os

import pytest

from zum.configs.core import (
retrieve_config_file,
search_for_config_file,
validate_configs,
validate_endpoints,
validate_metadata,
)
from zum.configs.errors import InvalidConfigFileError, MissingConfigFileError


class TestSearchForConfigFile:
def test_path_is_file(self, tmpdir):
config_file = tmpdir.join("zum.toml")
open(config_file.strpath, "a").close()
search_for_config_file(config_file.strpath)

def test_path_is_folder(self, tmpdir):
with pytest.raises(MissingConfigFileError):
config_file = tmpdir.join("zum.toml")
os.mkdir(config_file.strpath)
search_for_config_file(config_file.strpath)

def test_path_does_not_exist(self, tmpdir):
with pytest.raises(MissingConfigFileError):
config_file = tmpdir.join("zum.toml")
search_for_config_file(config_file.strpath)


class TestRetrieveConfigFile:
def setup_method(self):
self.content = (
"[metadata]\n"
'server = "http://localhost:8000"\n'
"[endpoints.example]\n"
'route = "/example"\n'
'method = "get"\n'
)
self.parsed = {
"metadata": {"server": "http://localhost:8000"},
"endpoints": {"example": {"route": "/example", "method": "get"}},
}

def test_expected_retrieval(self, tmpdir):
config_file = tmpdir.join("zum.toml")
with open(config_file.strpath, "w") as raw_config_file:
raw_config_file.write(self.content)
assert retrieve_config_file(config_file.strpath) == self.parsed


class TestValidateConfigs:
# Just tests that a valid config passes, as the sub-methods are tested
# against errors and invalid formats
def setup_method(self):
self.config = {
"metadata": {"server": "http://localhost:8000"},
"endpoints": {"example": {"path": "/example", "method": "get"}},
}

def test_valid_config(self):
validate_configs(self.config)


class TestValidateMetadata:
def setup_method(self):
self.missing = {"some-key": "some-value"}
self.invalid_type = {"some-key": "some-value", "metadata": "nice"}
self.missing_server = {
"some-key": "some-value",
"metadata": {"some-thing": "some-value"},
}

def test_missing_metadata_key(self):
with pytest.raises(InvalidConfigFileError) as excinfo:
validate_metadata(self.missing)
assert "Missing 'metadata' section" in str(excinfo.value)

def test_invalid_endpoints_key_type(self):
with pytest.raises(InvalidConfigFileError) as excinfo:
validate_metadata(self.invalid_type)
assert ("The 'metadata' section shold be a dictionary or mapping") in str(
excinfo.value
)

def test_empty_endpoints_key(self):
with pytest.raises(InvalidConfigFileError) as excinfo:
validate_metadata(self.missing_server)
assert "Missing 'server' value from the 'metadata' section" in str(
excinfo.value
)


class TestValidateEndpoints:
def setup_method(self):
self.missing = {"some-key": "some-value"}
self.invalid_type = {"some-key": "some-value", "endpoints": "nice"}
self.empty = {"some-key": "some-value", "endpoints": {}}

def test_missing_endpoints_key(self):
with pytest.raises(InvalidConfigFileError) as excinfo:
validate_endpoints(self.missing)
assert "Missing 'endpoints' section" in str(excinfo.value)

def test_invalid_endpoints_key_type(self):
with pytest.raises(InvalidConfigFileError) as excinfo:
validate_endpoints(self.invalid_type)
assert ("The 'endpoints' section shold be a dictionary or mapping") in str(
excinfo.value
)

def test_empty_endpoints_key(self):
with pytest.raises(InvalidConfigFileError) as excinfo:
validate_endpoints(self.empty)
assert "At least one endpoint is required" in str(excinfo.value)
Empty file added tests/requests/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions tests/requests/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest

from zum.requests.core import generate_request, reduce_arguments
from zum.requests.errors import MissingEndpointParamsError
from zum.requests.models import Request


class TestReduceArguments:
def setup_method(self):
self.keys = ["first", "second", "third"]
self.short_args = [1, 2]
self.perfect_args = [1, 2, 3]
self.long_args = [1, 2, 3, 4, 5]
self.output = {
"perfect": {
"processed": {"first": 1, "second": 2, "third": 3},
"remaining": [],
},
"long": {
"processed": {"first": 1, "second": 2, "third": 3},
"remaining": [4, 5],
},
}

def test_empty_keys(self):
processed, remaining = reduce_arguments(None, self.perfect_args)
assert processed == {}
assert remaining == self.perfect_args

def test_short_args(self):
with pytest.raises(MissingEndpointParamsError):
processed, remaining = reduce_arguments(self.keys, self.short_args)

def test_perfect_args(self):
processed, remaining = reduce_arguments(self.keys, self.perfect_args)
assert processed == self.output["perfect"]["processed"]
assert remaining == self.output["perfect"]["remaining"]

def test_long_args(self):
processed, remaining = reduce_arguments(self.keys, self.long_args)
assert processed == self.output["long"]["processed"]
assert remaining == self.output["long"]["remaining"]


class TestGenerateRequest:
def setup_method(self):
self.raw_endpoint = {
"route": "/example/{id}?query={query}",
"method": "post",
"params": ["id", "query"],
"body": ["name", "city"],
}
self.params = {"id": 69, "query": "nais"}
self.body = {"name": "Dani", "city": "Barcelona"}
self.arguments = [
self.params["id"],
self.params["query"],
self.body["name"],
self.body["city"],
]
self.expected_route = (
f"/example/{self.params['id']}?query={self.params['query']}"
)

def test_request_generation(self):
request = generate_request(self.raw_endpoint, self.arguments)
assert isinstance(request, Request)
assert request.params == self.params
assert request.body == self.body
assert request.route == self.expected_route
25 changes: 25 additions & 0 deletions tests/requests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from zum.requests.models import Request


class TestRequestModel:
def setup_method(self):
self.simple = {"route": "/example", "method": "get", "params": {}, "body": {}}
self.complex = {
"route": "/example/{id}?query={query}",
"method": "get",
"params": {"query": "nais", "id": 69},
"body": {},
}
self.simple_route = "/example"
self.complex_route = (
f"/example/{self.complex['params']['id']}"
f"?query={self.complex['params']['query']}"
)

def test_simple_request_route_interpolation(self):
request = Request(**self.simple)
assert request.route == self.simple_route

def test_complex_request_route_interpolation(self):
request = Request(**self.complex)
assert request.route == self.complex_route
75 changes: 75 additions & 0 deletions tests/requests/test_valdiations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pytest

from zum.requests.errors import InvalidEndpointDefinitionError
from zum.requests.validations import (
validate_raw_endpoint,
validate_raw_endpoint_method,
validate_raw_endpoint_route,
)


class TestRawEndpointValidation:
# Just tests that a valid raw endpont passes, as the sub-methods are
# tested against errors and invalid formats
def setup_method(self):
self.raw_endpoint = {"method": "get", "route": "/valid"}

def test_valid_raw_endpoint(self):
validate_raw_endpoint(self.raw_endpoint)


class TestRawEndpointRouteValidation:
def setup_method(self):
self.missing = {"some": "thing"}
self.invalid_type = {"route": 4}
self.valid = {"route": "/valid"}

def test_missing_route(self):
with pytest.raises(InvalidEndpointDefinitionError) as excinfo:
validate_raw_endpoint_route(self.missing)
assert "Missing 'route' attribute" in str(excinfo.value)

def test_invalid_type_route(self):
with pytest.raises(InvalidEndpointDefinitionError) as excinfo:
validate_raw_endpoint_route(self.invalid_type)
assert "The 'route' attribute of the endpoint must be a string" in str(
excinfo.value
)

def test_valid_route(self):
validate_raw_endpoint_route(self.valid)


class TestRawEndpointMethodValidation:
def setup_method(self):
self.missing = {"some": "thing"}
self.invalid_type = {"method": 4}
self.invalid_content = {"method": "not valid"}
self.spaced_valid = {"method": " get "}
self.uppercased_valid = {"method": "GET"}

def test_missing_method(self):
with pytest.raises(InvalidEndpointDefinitionError) as excinfo:
validate_raw_endpoint_method(self.missing)
assert "Missing 'method' attribute" in str(excinfo.value)

def test_invalid_type_method(self):
with pytest.raises(InvalidEndpointDefinitionError) as excinfo:
validate_raw_endpoint_method(self.invalid_type)
assert "The 'method' attribute for the endpoint must be a string" in str(
excinfo.value
)

def test_invalid_content_method(self):
with pytest.raises(InvalidEndpointDefinitionError) as excinfo:
validate_raw_endpoint_method(self.invalid_content)
assert (
"Invalid 'method' value for the endpoint (not a valid HTTP method)"
in str(excinfo.value)
)

def test_spaced_valid_method(self):
validate_raw_endpoint_method(self.spaced_valid)

def test_uppercased_valid_method(self):
validate_raw_endpoint_method(self.uppercased_valid)
File renamed without changes.
2 changes: 1 addition & 1 deletion zum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
Init file for the zum library.
"""

version_info = (0, 0, 2)
version_info = (0, 0, 3)
__version__ = ".".join([str(x) for x in version_info])
15 changes: 10 additions & 5 deletions zum/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ def dispatcher(*args: Any, **kwargs: Any) -> None:

def generate_parser(actions_list: List[str]) -> ArgumentParser:
"""Generates the action parser."""
warning = "Beware! No config file was found!" if not actions_list else ""

# Create parser
parser = ArgumentParser(description="Command line interface tool for zum.")
parser = ArgumentParser(
description="Command line interface tool for zum.", epilog=warning
)

# Add version command
parser.add_argument(
Expand All @@ -36,11 +40,12 @@ def generate_parser(actions_list: List[str]) -> ArgumentParser:
version=f"zum version {zum.__version__}",
)

# Add action argument
parser.add_argument("action", choices=actions_list, nargs=1)
if actions_list:
# Add action argument
parser.add_argument("action", choices=actions_list, nargs=1)

# Add params
parser.add_argument("params", nargs="*")
# Add params
parser.add_argument("params", nargs="*")

return parser

Expand Down
10 changes: 9 additions & 1 deletion zum/configs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def validate_metadata(configs: Dict[str, Any]) -> None:
"""Validate that the metadata key of the configs are not malformed."""
if "metadata" not in configs:
raise InvalidConfigFileError("Missing 'metadata' section of the config file")
if not isinstance(configs["metadata"], dict):
raise InvalidConfigFileError(
"The 'metadata' section shold be a dictionary or mapping."
)
if "server" not in configs["metadata"]:
raise InvalidConfigFileError(
"Missing 'server' value from the 'metadata' section of the config file"
Expand All @@ -43,7 +47,11 @@ def validate_endpoints(configs: Dict[str, Any]) -> None:
"""Validate that the endpoints key of the configs are not malformed."""
if "endpoints" not in configs:
raise InvalidConfigFileError("Missing 'endpoints' section of the config file")
if len(configs["endpoints"]) == 0:
if not isinstance(configs["endpoints"], dict):
raise InvalidConfigFileError(
"The 'endpoints' section shold be a dictionary or mapping."
)
if len(configs["endpoints"].keys()) == 0:
raise InvalidConfigFileError(
"At least one endpoint is required on the config file"
)
Loading

0 comments on commit f06afd0

Please sign in to comment.