# encoding=utf-8
from abc import ABCMeta
from enum import Enum
from typing import Mapping, Optional, Union
from furl import furl
from mbtest.imposters.base import Injecting, JsonSerializable, JsonStructure
[docs]class BasePredicate(JsonSerializable, metaclass=ABCMeta):
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "BasePredicate": # noqa: C901
if "and" in structure:
return AndPredicate.from_structure(structure)
elif "or" in structure:
return OrPredicate.from_structure(structure)
elif "not" in structure:
return NotPredicate.from_structure(structure)
elif "contains" in structure and "data" in structure["contains"]:
return TcpPredicate.from_structure(structure)
elif "inject" in structure:
return InjectionPredicate.from_structure(structure)
elif set(structure.keys()).intersection(
{o.value for o in Predicate.Operator}
): # pragma: no cover
return Predicate.from_structure(structure)
raise NotImplementedError() # pragma: no cover
[docs]class LogicallyCombinablePredicate(BasePredicate, metaclass=ABCMeta):
def __and__(self, other: "BasePredicate") -> "AndPredicate":
return AndPredicate(self, other)
def __or__(self, other: "BasePredicate") -> "OrPredicate":
return OrPredicate(self, other)
def __invert__(self) -> "NotPredicate":
return NotPredicate(self)
[docs]class Predicate(LogicallyCombinablePredicate):
"""Represents a `Mountebank predicate <http://www.mbtest.org/docs/api/predicates>`_.
A predicate can be thought of as a trigger, which may or may not match a request.
:param path: URL path.
:param method: HTTP method.
:param query: Query arguments, keys and values.
:param body: Body text. Can be a string, or a JSON serialisable data structure.
:param headers: Headers, keys and values.
:param xpath: xpath query
:param jsonpath: jsonpath query
:param operator:
:param case_sensitive:
"""
[docs] class InvalidPredicateOperator(Exception):
pass
[docs] class Method(Enum):
"""Predicate HTTP method."""
DELETE = "DELETE"
GET = "GET"
HEAD = "HEAD"
POST = "POST"
PUT = "PUT"
PATCH = "PATCH"
[docs] class Operator(Enum):
"""`Predicate operator <http://www.mbtest.org/docs/api/predicates>`_."""
EQUALS = "equals"
DEEP_EQUALS = "deepEquals"
CONTAINS = "contains"
STARTS_WITH = "startsWith"
ENDS_WITH = "endsWith"
MATCHES = "matches"
EXISTS = "exists"
[docs] @classmethod
def has_value(cls, name: str) -> bool:
return any(name == item.value for item in cls)
def __init__(
self,
path: Optional[Union[str, furl]] = None,
method: Optional[Union[Method, str]] = None,
query: Optional[Mapping[str, Union[str, int, bool]]] = None,
body: Optional[Union[str, JsonStructure]] = None,
headers: Optional[Mapping[str, str]] = None,
xpath: Optional[str] = None,
jsonpath: Optional[str] = None,
operator: Union[Operator, str] = Operator.EQUALS,
case_sensitive: bool = True,
) -> None:
self.path = path
self.method = (
method
if isinstance(method, Predicate.Method)
else Predicate.Method(method)
if method
else None
)
self.query = query
self.body = body
self.headers = headers
self.xpath = xpath
self.jsonpath = jsonpath
self.operator = (
operator if isinstance(operator, Predicate.Operator) else Predicate.Operator(operator)
)
self.case_sensitive = case_sensitive
[docs] def as_structure(self) -> JsonStructure:
predicate = {
self.operator.value: self.fields_as_structure(),
"caseSensitive": self.case_sensitive,
}
if self.xpath:
predicate["xpath"] = {"selector": self.xpath}
if self.jsonpath:
predicate["jsonpath"] = {"selector": self.jsonpath}
return predicate
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "Predicate":
operators = tuple(filter(Predicate.Operator.has_value, structure.keys()))
if len(operators) != 1:
raise Predicate.InvalidPredicateOperator(
"Each predicate must define exactly one operator."
)
operator = operators[0]
predicate = cls(operator=operator, case_sensitive=structure.get("caseSensitive", True))
predicate.fields_from_structure(structure[operator])
if "xpath" in structure:
predicate.xpath = structure["xpath"]["selector"]
if "jsonpath" in structure:
predicate.jsonpath = structure["jsonpath"]["selector"]
return predicate
[docs] def fields_from_structure(self, inner):
self.set_if_in_dict(inner, "path", "path")
self.set_if_in_dict(inner, "query", "query")
self.set_if_in_dict(inner, "body", "body")
self.set_if_in_dict(inner, "headers", "headers")
if "method" in inner:
self.method = Predicate.Method(inner["method"])
[docs] def fields_as_structure(self):
fields = {}
self.add_if_true(fields, "path", self.path)
self.add_if_true(fields, "query", self.query)
self.add_if_true(fields, "body", self.body)
self.add_if_true(fields, "headers", self.headers)
if self.method:
fields["method"] = self.method.value
return fields
[docs]class AndPredicate(LogicallyCombinablePredicate):
def __init__(self, left: BasePredicate, right: BasePredicate) -> None:
self.left = left
self.right = right
[docs] def as_structure(self) -> JsonStructure:
return {"and": [self.left.as_structure(), self.right.as_structure()]}
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "AndPredicate":
return cls(
BasePredicate.from_structure(structure["and"][0]),
BasePredicate.from_structure(structure["and"][1]),
)
[docs]class OrPredicate(LogicallyCombinablePredicate):
def __init__(self, left: BasePredicate, right: BasePredicate) -> None:
self.left = left
self.right = right
[docs] def as_structure(self) -> JsonStructure:
return {"or": [self.left.as_structure(), self.right.as_structure()]}
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "OrPredicate":
return cls(
BasePredicate.from_structure(structure["or"][0]),
BasePredicate.from_structure(structure["or"][1]),
)
[docs]class NotPredicate(LogicallyCombinablePredicate):
def __init__(self, inverted: BasePredicate) -> None:
self.inverted = inverted
[docs] def as_structure(self) -> JsonStructure:
return {"not": self.inverted.as_structure()}
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "NotPredicate":
return cls(BasePredicate.from_structure(structure["not"]))
[docs]class TcpPredicate(LogicallyCombinablePredicate):
"""Represents a `Mountebank TCP predicate <http://www.mbtest.org/docs/protocols/tcp>`_.
A predicate can be thought of as a trigger, which may or may not match a request.
:param data: Data to match the request.
"""
def __init__(self, data: str) -> None:
self.data = data
[docs] def as_structure(self) -> JsonStructure:
return {"contains": {"data": self.data}}
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "TcpPredicate":
return cls(structure["contains"]["data"])
[docs]class InjectionPredicate(BasePredicate, Injecting):
"""Represents a `Mountebank injection predicate <http://www.mbtest.org/docs/api/injection>`_.
A predicate can be thought of as a trigger, which may or may not match a request.
Injection requires Mountebank version 2.0 or higher.
:param inject: JavaScript function to inject.
"""
[docs] @classmethod
def from_structure(cls, structure: JsonStructure) -> "InjectionPredicate":
return cls(inject=structure["inject"])