Нет описания

test_messages.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import decimal
  2. import json
  3. from datetime import datetime
  4. import pytest
  5. from ocpp.exceptions import (
  6. FormatViolationError,
  7. NotImplementedError,
  8. PropertyConstraintViolationError,
  9. ProtocolError,
  10. TypeConstraintViolationError,
  11. UnknownCallErrorCodeError,
  12. ValidationError,
  13. )
  14. from ocpp.messages import (
  15. Call,
  16. CallError,
  17. CallResult,
  18. MessageType,
  19. _DecimalEncoder,
  20. _validators,
  21. get_validator,
  22. unpack,
  23. validate_payload,
  24. )
  25. from ocpp.v16.enums import Action
  26. def test_unpack_with_invalid_json():
  27. """
  28. Test that correct exception is raised when unpack is called with invalid
  29. JSON.
  30. """
  31. with pytest.raises(FormatViolationError):
  32. unpack(b"\x01")
  33. def test_unpack_without_jsonified_list():
  34. """
  35. OCPP messages are JSONified lists. This test make sure that the correct
  36. exception is raised when input is not a JSONified list.
  37. """
  38. with pytest.raises(ProtocolError):
  39. unpack(json.dumps("3"))
  40. def test_unpack_without_message_type_id_in_json():
  41. """
  42. OCPP must contain the MessageTypeID as first element of the message.
  43. This test validates if correct exception is raised when this is not
  44. the case
  45. """
  46. with pytest.raises(ProtocolError):
  47. unpack(json.dumps([]))
  48. def test_unpack_with_invalid_message_type_id_in_json():
  49. """
  50. OCPP messages only have 3 valid values for MessageTypeID, that is the first
  51. element of the OCPP message. This test validates that correct exception is
  52. raised when this value is invalid.
  53. """
  54. with pytest.raises(PropertyConstraintViolationError):
  55. unpack(json.dumps([5, 1]))
  56. def test_get_validator_with_valid_name():
  57. """
  58. Test if correct validator is returned and if validator is added to cache.
  59. """
  60. schema = get_validator(MessageType.Call, "Reset", ocpp_version="1.6")
  61. assert schema == _validators["Reset_1.6"]
  62. assert schema.schema == {
  63. "$schema": "http://json-schema.org/draft-04/schema#",
  64. "title": "ResetRequest",
  65. "type": "object",
  66. "properties": {
  67. "type": {
  68. "additionalProperties": False,
  69. "type": "string",
  70. "enum": ["Hard", "Soft"],
  71. }
  72. },
  73. "additionalProperties": False,
  74. "required": ["type"],
  75. }
  76. def test_get_validator_with_invalid_name():
  77. """
  78. Test if OSError is raised when schema validation file cannnot be found.
  79. """
  80. with pytest.raises(OSError):
  81. get_validator(MessageType.Call, "non-existing", ocpp_version="1.6")
  82. def test_validate_set_charging_profile_payload():
  83. """ " Test if payloads with floats are validated correctly.
  84. This test uses the value of 21.4, which is internally represented as
  85. 21.39999999999999857891452847979962825775146484375.
  86. You can verify this using `decimal.Decimal(21.4)`
  87. """
  88. message = Call(
  89. unique_id="1234",
  90. action="SetChargingProfile",
  91. payload={
  92. "connectorId": 1,
  93. "csChargingProfiles": {
  94. "chargingProfileId": 1,
  95. "stackLevel": 0,
  96. "chargingProfilePurpose": "TxProfile",
  97. "chargingProfileKind": "Relative",
  98. "chargingSchedule": {
  99. "chargingRateUnit": "A",
  100. "chargingSchedulePeriod": [{"startPeriod": 0, "limit": 21.4}],
  101. },
  102. "transactionId": 123456789,
  103. },
  104. },
  105. )
  106. validate_payload(message, ocpp_version="1.6")
  107. def test_validate_get_composite_profile_payload():
  108. """ " Test if payloads with floats are validated correctly.
  109. This test uses the value of 15.2, which is internally represented as
  110. 15.19999999999999857891452847979962825775146484375.
  111. You can verify this using `decimal.Decimal(15.2)`
  112. """
  113. message = CallResult(
  114. unique_id="1234",
  115. action="GetCompositeSchedule",
  116. payload={
  117. "status": "Accepted",
  118. "connectorId": 1,
  119. "scheduleStart": "2021-06-15T14:01:32Z",
  120. "chargingSchedule": {
  121. "duration": 60,
  122. "chargingRateUnit": "A",
  123. "chargingSchedulePeriod": [{"startPeriod": 0, "limit": 15.2}],
  124. },
  125. },
  126. )
  127. validate_payload(message, ocpp_version="1.6")
  128. @pytest.mark.parametrize("ocpp_version", ["1.6", "2.0"])
  129. def test_validate_payload_with_valid_payload(ocpp_version):
  130. """
  131. Test if validate_payload doesn't return any exceptions when it's
  132. validating a valid payload.
  133. """
  134. message = CallResult(
  135. unique_id="1234",
  136. action="Heartbeat",
  137. payload={"currentTime": datetime.now().isoformat()},
  138. )
  139. validate_payload(message, ocpp_version=ocpp_version)
  140. def test_validate_payload_with_invalid_additional_properties_payload():
  141. """
  142. Test if validate_payload raises FormatViolationError when validation of
  143. payload with unrequested properties fails.
  144. """
  145. message = CallResult(
  146. unique_id="1234",
  147. action="Heartbeat",
  148. payload={"invalid_key": True},
  149. )
  150. with pytest.raises(FormatViolationError):
  151. validate_payload(message, ocpp_version="1.6")
  152. def test_validate_payload_with_invalid_type_payload():
  153. """
  154. Test if validate_payload raises TypeConstraintViolationError when
  155. validation of payload with mismatched type fails.
  156. """
  157. message = Call(
  158. unique_id="1234",
  159. action="StartTransaction",
  160. payload={
  161. "connectorId": 1,
  162. "idTag": "okTag",
  163. "meterStart": "invalid_type",
  164. "timestamp": "2022-01-25T19:18:30.018Z",
  165. },
  166. )
  167. with pytest.raises(TypeConstraintViolationError):
  168. validate_payload(message, ocpp_version="1.6")
  169. def test_validate_payload_with_invalid_missing_property_payload():
  170. """
  171. Test if validate_payload raises ProtocolError when validation of
  172. payload with missing properties fails.
  173. """
  174. message = Call(
  175. unique_id="1234",
  176. action="StartTransaction",
  177. payload={
  178. "connectorId": 1,
  179. "idTag": "okTag",
  180. # meterStart is purposely missing
  181. "timestamp": "2022-01-25T19:18:30.018Z",
  182. },
  183. )
  184. with pytest.raises(ProtocolError):
  185. validate_payload(message, ocpp_version="1.6")
  186. def test_validate_payload_with_invalid_message_type_id():
  187. """
  188. Test if validate_payload raises ValidationError when it is called with
  189. a message type id other than 2, Call, or 3, CallError.
  190. """
  191. with pytest.raises(ValidationError):
  192. validate_payload(dict(), ocpp_version="1.6")
  193. def test_validate_payload_with_non_existing_schema():
  194. """
  195. Test if correct exception is raised when a validation schema cannot be
  196. found.
  197. """
  198. message = CallResult(
  199. unique_id="1234",
  200. action="MagicSpell",
  201. payload={"invalid_key": True},
  202. )
  203. with pytest.raises(NotImplementedError):
  204. validate_payload(message, ocpp_version="1.6")
  205. def test_call_error_representation():
  206. call = CallError(
  207. unique_id=1,
  208. error_code="GenericError",
  209. error_description="Some message",
  210. error_details={},
  211. )
  212. assert (
  213. str(call) == "<CallError - unique_id=1, error_code=GenericError, "
  214. "error_description=Some message, error_details={}>"
  215. )
  216. def test_call_representation():
  217. call = Call(unique_id="1", action=Action.Heartbeat, payload={})
  218. assert str(call) == "<Call - unique_id=1, action=Heartbeat, payload={}>"
  219. def test_call_result_representation():
  220. call = CallResult(
  221. unique_id="1", action=Action.Authorize, payload={"status": "Accepted"}
  222. )
  223. assert (
  224. str(call) == "<CallResult - unique_id=1, action=Authorize, payload={'status': "
  225. "'Accepted'}>"
  226. )
  227. def test_creating_exception_from_call_error():
  228. call_error = CallError(
  229. unique_id="1337",
  230. error_code="ProtocolError",
  231. error_description="Something went wrong",
  232. error_details="Some details about the error",
  233. )
  234. assert call_error.to_exception() == ProtocolError(
  235. description="Something went wrong", details="Some details about the error"
  236. )
  237. def test_creating_exception_from_call_error_with_unknown_error_code():
  238. call_error = CallError(
  239. unique_id="1337",
  240. error_code="418",
  241. error_description="I'm a teapot",
  242. )
  243. with pytest.raises(UnknownCallErrorCodeError):
  244. call_error.to_exception()
  245. def test_serializing_decimal():
  246. assert json.dumps([decimal.Decimal(2.000001)], cls=_DecimalEncoder) == "[2.0]"
  247. def test_serializing_custom_types():
  248. """
  249. validate_payload() raises an exception receives an invalid OCPP message.
  250. This exception contains the Call causing the problem.
  251. The exception is turned into a CallError which in serialized to JSON.
  252. https://github.com/mobilityhouse/ocpp/issues/395 tracks a bug where serialization
  253. would fails because Call is not serializable.
  254. This test verifies that fix for that bug.
  255. """
  256. message = Call(
  257. unique_id="1234",
  258. action="StartTransaction",
  259. payload={
  260. "connectorId": 1,
  261. "idTag": "okTag",
  262. "meterStart": "invalid_type",
  263. "timestamp": "2022-01-25T19:18:30.018Z",
  264. },
  265. )
  266. try:
  267. validate_payload(message, ocpp_version="1.6")
  268. except TypeConstraintViolationError as error:
  269. # Before the fix, this call would fail with a TypError. Lack of any error
  270. # makes this test pass.
  271. _ = message.create_call_error(error).to_json()
  272. def test_validate_meter_values_hertz():
  273. """
  274. Tests that a unit of measure called "Hertz" is permitted in validation.
  275. This was missing from the original 1.6 spec, but was added as an errata
  276. (see the OCPP 1.6 Errata sheet, v4.0 Release, 2019-10-23, page 34).
  277. """
  278. message = Call(
  279. unique_id="1234",
  280. action="MeterValues",
  281. payload={
  282. "connectorId": 1,
  283. "transactionId": 123456789,
  284. "meterValue": [
  285. {
  286. "timestamp": "2020-02-21T13:48:45.459756Z",
  287. "sampledValue": [
  288. {
  289. "value": "50.0",
  290. "measurand": "Frequency",
  291. "unit": "Hertz",
  292. }
  293. ],
  294. }
  295. ],
  296. },
  297. )
  298. validate_payload(message, ocpp_version="1.6")
  299. def test_validate_set_maxlength_violation_payload():
  300. """
  301. Test if payloads that violate maxLength raise a
  302. TypeConstraintViolationError
  303. """
  304. message = Call(
  305. unique_id="1234",
  306. action="StartTransaction",
  307. payload={
  308. "idTag": "012345678901234567890",
  309. "connectorId": 1,
  310. },
  311. )
  312. with pytest.raises(TypeConstraintViolationError):
  313. validate_payload(message, ocpp_version="1.6")