You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

264 lines
9.2 KiB

  1. import sys
  2. import unittest
  3. from contextlib import contextmanager
  4. from random import choice
  5. from threading import Thread
  6. from typing import Dict, Optional, Tuple
  7. from unittest.mock import patch
  8. import requests
  9. from semantic_version import Version
  10. from werkzeug.test import TestResponse
  11. import frappe
  12. from frappe.utils import get_site_url, get_test_client
  13. try:
  14. _site = frappe.local.site
  15. except Exception:
  16. _site = None
  17. authorization_token = None
  18. @contextmanager
  19. def suppress_stdout():
  20. """Supress stdout for tests which expectedly make noise
  21. but that you don't need in tests"""
  22. sys.stdout = None
  23. try:
  24. yield
  25. finally:
  26. sys.stdout = sys.__stdout__
  27. def make_request(target: str, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None) -> TestResponse:
  28. t = ThreadWithReturnValue(target=target, args=args, kwargs=kwargs)
  29. t.start()
  30. t.join()
  31. return t._return
  32. def patch_request_header(key, *args, **kwargs):
  33. if key == "Authorization":
  34. return f"token {authorization_token}"
  35. class ThreadWithReturnValue(Thread):
  36. def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
  37. Thread.__init__(self, group, target, name, args, kwargs)
  38. self._return = None
  39. def run(self):
  40. if self._target is not None:
  41. with patch("frappe.app.get_site_name", return_value=_site):
  42. header_patch = patch("frappe.get_request_header", new=patch_request_header)
  43. if authorization_token:
  44. header_patch.start()
  45. self._return = self._target(*self._args, **self._kwargs)
  46. if authorization_token:
  47. header_patch.stop()
  48. def join(self, *args):
  49. Thread.join(self, *args)
  50. return self._return
  51. class FrappeAPITestCase(unittest.TestCase):
  52. SITE = frappe.local.site
  53. SITE_URL = get_site_url(SITE)
  54. RESOURCE_URL = f"{SITE_URL}/api/resource"
  55. TEST_CLIENT = get_test_client()
  56. @property
  57. def sid(self) -> str:
  58. if not getattr(self, "_sid", None):
  59. from frappe.auth import CookieManager, LoginManager
  60. from frappe.utils import set_request
  61. set_request(path="/")
  62. frappe.local.cookie_manager = CookieManager()
  63. frappe.local.login_manager = LoginManager()
  64. frappe.local.login_manager.login_as('Administrator')
  65. self._sid = frappe.session.sid
  66. return self._sid
  67. def get(self, path: str, params: Optional[Dict] = None, **kwargs) -> TestResponse:
  68. return make_request(target=self.TEST_CLIENT.get, args=(path, ), kwargs={"data": params, **kwargs})
  69. def post(self, path, data, **kwargs) -> TestResponse:
  70. return make_request(target=self.TEST_CLIENT.post, args=(path, ), kwargs={"data": data, **kwargs})
  71. def put(self, path, data, **kwargs) -> TestResponse:
  72. return make_request(target=self.TEST_CLIENT.put, args=(path, ), kwargs={"data": data, **kwargs})
  73. def delete(self, path, **kwargs) -> TestResponse:
  74. return make_request(target=self.TEST_CLIENT.delete, args=(path, ), kwargs=kwargs)
  75. class TestResourceAPI(FrappeAPITestCase):
  76. DOCTYPE = "ToDo"
  77. GENERATED_DOCUMENTS = []
  78. @classmethod
  79. def setUpClass(cls):
  80. for _ in range(10):
  81. doc = frappe.get_doc(
  82. {"doctype": "ToDo", "description": frappe.mock("paragraph")}
  83. ).insert()
  84. cls.GENERATED_DOCUMENTS.append(doc.name)
  85. frappe.db.commit()
  86. @classmethod
  87. def tearDownClass(cls):
  88. for name in cls.GENERATED_DOCUMENTS:
  89. frappe.delete_doc_if_exists(cls.DOCTYPE, name)
  90. frappe.db.commit()
  91. def test_unauthorized_call(self):
  92. # test 1: fetch documents without auth
  93. response = requests.get(f"{self.RESOURCE_URL}/{self.DOCTYPE}")
  94. self.assertEqual(response.status_code, 403)
  95. def test_get_list(self):
  96. # test 2: fetch documents without params
  97. response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid})
  98. self.assertEqual(response.status_code, 200)
  99. self.assertIsInstance(response.json, dict)
  100. self.assertIn("data", response.json)
  101. def test_get_list_limit(self):
  102. # test 3: fetch data with limit
  103. response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "limit": 2})
  104. self.assertEqual(response.status_code, 200)
  105. self.assertEqual(len(response.json["data"]), 2)
  106. def test_get_list_dict(self):
  107. # test 4: fetch response as (not) dict
  108. response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": True})
  109. json = frappe._dict(response.json)
  110. self.assertEqual(response.status_code, 200)
  111. self.assertIsInstance(json.data, list)
  112. self.assertIsInstance(json.data[0], dict)
  113. response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": False})
  114. json = frappe._dict(response.json)
  115. self.assertEqual(response.status_code, 200)
  116. self.assertIsInstance(json.data, list)
  117. self.assertIsInstance(json.data[0], list)
  118. def test_get_list_debug(self):
  119. # test 5: fetch response with debug
  120. response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "debug": True})
  121. self.assertEqual(response.status_code, 200)
  122. self.assertIn("exc", response.json)
  123. self.assertIsInstance(response.json["exc"], str)
  124. self.assertIsInstance(eval(response.json["exc"]), list)
  125. def test_get_list_fields(self):
  126. # test 6: fetch response with fields
  127. response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "fields": '["description"]'})
  128. self.assertEqual(response.status_code, 200)
  129. json = frappe._dict(response.json)
  130. self.assertIn("description", json.data[0])
  131. def test_create_document(self):
  132. # test 7: POST method on /api/resource to create doc
  133. data = {"description": frappe.mock("paragraph"), "sid": self.sid}
  134. response = self.post(f"/api/resource/{self.DOCTYPE}", data)
  135. self.assertEqual(response.status_code, 200)
  136. docname = response.json["data"]["name"]
  137. self.assertIsInstance(docname, str)
  138. self.GENERATED_DOCUMENTS.append(docname)
  139. def test_update_document(self):
  140. # test 8: PUT method on /api/resource to update doc
  141. generated_desc = frappe.mock("paragraph")
  142. data = {"description": generated_desc, "sid": self.sid}
  143. random_doc = choice(self.GENERATED_DOCUMENTS)
  144. desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description")
  145. response = self.put(f"/api/resource/{self.DOCTYPE}/{random_doc}", data=data)
  146. self.assertEqual(response.status_code, 200)
  147. self.assertNotEqual(response.json["data"]["description"], desc_before_update)
  148. self.assertEqual(response.json["data"]["description"], generated_desc)
  149. def test_delete_document(self):
  150. # test 9: DELETE method on /api/resource
  151. doc_to_delete = choice(self.GENERATED_DOCUMENTS)
  152. response = self.delete(f"/api/resource/{self.DOCTYPE}/{doc_to_delete}")
  153. self.assertEqual(response.status_code, 202)
  154. self.assertDictEqual(response.json, {"message": "ok"})
  155. self.GENERATED_DOCUMENTS.remove(doc_to_delete)
  156. non_existent_doc = frappe.generate_hash(length=12)
  157. with suppress_stdout():
  158. response = self.delete(f"/api/resource/{self.DOCTYPE}/{non_existent_doc}")
  159. self.assertEqual(response.status_code, 404)
  160. self.assertDictEqual(response.json, {})
  161. def test_run_doc_method(self):
  162. # test 10: Run whitelisted method on doc via /api/resource
  163. # status_code is 403 if no other tests are run before this - it's not logged in
  164. self.post("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
  165. response = self.get("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
  166. self.assertIn(response.status_code, (403, 200))
  167. if response.status_code == 403:
  168. self.assertTrue(set(response.json.keys()) == {'exc_type', 'exception', 'exc', '_server_messages'})
  169. self.assertEqual(response.json.get('exc_type'), 'PermissionError')
  170. self.assertEqual(response.json.get('exception'), 'frappe.exceptions.PermissionError: Not permitted')
  171. self.assertIsInstance(response.json.get('exc'), str)
  172. elif response.status_code == 200:
  173. data = response.json.get("data")
  174. self.assertIsInstance(data, list)
  175. self.assertIsInstance(data[0], dict)
  176. class TestMethodAPI(FrappeAPITestCase):
  177. METHOD_PATH = "/api/method"
  178. def setUp(self):
  179. if self._testMethodName == "test_auth_cycle":
  180. from frappe.core.doctype.user.user import generate_keys
  181. generate_keys("Administrator")
  182. frappe.db.commit()
  183. def test_version(self):
  184. # test 1: test for /api/method/version
  185. response = self.get(f"{self.METHOD_PATH}/version")
  186. json = frappe._dict(response.json)
  187. self.assertEqual(response.status_code, 200)
  188. self.assertIsInstance(json, dict)
  189. self.assertIsInstance(json.message, str)
  190. self.assertEqual(Version(json.message), Version(frappe.__version__))
  191. def test_ping(self):
  192. # test 2: test for /api/method/ping
  193. response = self.get(f"{self.METHOD_PATH}/ping")
  194. self.assertEqual(response.status_code, 200)
  195. self.assertIsInstance(response.json, dict)
  196. self.assertEqual(response.json["message"], "pong")
  197. def test_get_user_info(self):
  198. # test 3: test for /api/method/frappe.realtime.get_user_info
  199. response = self.get(f"{self.METHOD_PATH}/frappe.realtime.get_user_info")
  200. self.assertEqual(response.status_code, 200)
  201. self.assertIsInstance(response.json, dict)
  202. self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest"))
  203. def test_auth_cycle(self):
  204. # test 4: Pass authorization token in request
  205. global authorization_token
  206. user = frappe.get_doc("User", "Administrator")
  207. api_key, api_secret = user.api_key, user.get_password("api_secret")
  208. authorization_token = f"{api_key}:{api_secret}"
  209. response = self.get("/api/method/frappe.auth.get_logged_user")
  210. self.assertEqual(response.status_code, 200)
  211. self.assertEqual(response.json["message"], "Administrator")
  212. authorization_token = None