@@ -3,6 +3,8 @@ name: Server | |||||
on: | on: | ||||
pull_request: | pull_request: | ||||
workflow_dispatch: | workflow_dispatch: | ||||
push: | |||||
branches: [ develop ] | |||||
jobs: | jobs: | ||||
test: | test: | ||||
@@ -3,6 +3,8 @@ name: UI | |||||
on: | on: | ||||
pull_request: | pull_request: | ||||
workflow_dispatch: | workflow_dispatch: | ||||
push: | |||||
branches: [ develop ] | |||||
jobs: | jobs: | ||||
test: | test: | ||||
@@ -14,18 +14,21 @@ | |||||
</div> | </div> | ||||
<div align="center"> | <div align="center"> | ||||
<a href="https://github.com/frappe/frappe/actions/workflows/ci-tests.yml"> | |||||
<img src="https://github.com/frappe/frappe/actions/workflows/ci-tests.yml/badge.svg?branch=develop"> | |||||
</a> | |||||
<a href='https://frappeframework.com/docs'> | |||||
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/> | |||||
</a> | |||||
<a href="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml"> | |||||
<img src="https://github.com/frappe/frappe/actions/workflows/server-mariadb-tests.yml/badge.svg"> | |||||
</a> | |||||
<a href="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml"> | |||||
<img src="https://github.com/frappe/frappe/actions/workflows/ui-tests.yml/badge.svg?branch=develop"> | |||||
</a> | |||||
<a href='https://frappeframework.com/docs'> | |||||
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/> | |||||
</a> | |||||
<a href='https://www.codetriage.com/frappe/frappe'> | <a href='https://www.codetriage.com/frappe/frappe'> | ||||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'> | <img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'> | ||||
</a> | </a> | ||||
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'> | |||||
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'> | |||||
</a> | |||||
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'> | |||||
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'> | |||||
</a> | |||||
</div> | </div> | ||||
@@ -11,6 +11,7 @@ import frappe.client | |||||
import frappe.handler | import frappe.handler | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils.response import build_response | from frappe.utils.response import build_response | ||||
from frappe.utils.data import sbool | |||||
def handle(): | def handle(): | ||||
@@ -108,25 +109,40 @@ def handle(): | |||||
elif doctype: | elif doctype: | ||||
if frappe.local.request.method == "GET": | if frappe.local.request.method == "GET": | ||||
if frappe.local.form_dict.get('fields'): | |||||
frappe.local.form_dict['fields'] = json.loads(frappe.local.form_dict['fields']) | |||||
frappe.local.form_dict.setdefault('limit_page_length', 20) | |||||
frappe.local.response.update({ | |||||
"data": frappe.call( | |||||
frappe.client.get_list, | |||||
doctype, | |||||
**frappe.local.form_dict | |||||
) | |||||
}) | |||||
# set fields for frappe.get_list | |||||
if frappe.local.form_dict.get("fields"): | |||||
frappe.local.form_dict["fields"] = json.loads(frappe.local.form_dict["fields"]) | |||||
# set limit of records for frappe.get_list | |||||
frappe.local.form_dict.setdefault( | |||||
"limit_page_length", | |||||
frappe.local.form_dict.limit or frappe.local.form_dict.limit_page_length or 20, | |||||
) | |||||
# convert strings to native types - only as_dict and debug accept bool | |||||
for param in ["as_dict", "debug"]: | |||||
param_val = frappe.local.form_dict.get(param) | |||||
if param_val is not None: | |||||
frappe.local.form_dict[param] = sbool(param_val) | |||||
# evaluate frappe.get_list | |||||
data = frappe.call(frappe.client.get_list, doctype, **frappe.local.form_dict) | |||||
# set frappe.get_list result to response | |||||
frappe.local.response.update({"data": data}) | |||||
if frappe.local.request.method == "POST": | if frappe.local.request.method == "POST": | ||||
# fetch data from from dict | |||||
data = get_request_form_data() | data = get_request_form_data() | ||||
data.update({ | |||||
"doctype": doctype | |||||
}) | |||||
frappe.local.response.update({ | |||||
"data": frappe.get_doc(data).insert().as_dict() | |||||
}) | |||||
data.update({"doctype": doctype}) | |||||
# insert document from request data | |||||
doc = frappe.get_doc(data).insert() | |||||
# set response data | |||||
frappe.local.response.update({"data": doc.as_dict()}) | |||||
# commit for POST requests | |||||
frappe.db.commit() | frappe.db.commit() | ||||
else: | else: | ||||
raise frappe.DoesNotExistError | raise frappe.DoesNotExistError | ||||
@@ -4,6 +4,7 @@ | |||||
frappe.ui.form.on('Document Naming Rule', { | frappe.ui.form.on('Document Naming Rule', { | ||||
refresh: function(frm) { | refresh: function(frm) { | ||||
frm.trigger('document_type'); | frm.trigger('document_type'); | ||||
if (!frm.doc.__islocal) frm.trigger("add_update_counter_button"); | |||||
}, | }, | ||||
document_type: (frm) => { | document_type: (frm) => { | ||||
// update the select field options with fieldnames | // update the select field options with fieldnames | ||||
@@ -20,5 +21,44 @@ frappe.ui.form.on('Document Naming Rule', { | |||||
); | ); | ||||
}); | }); | ||||
} | } | ||||
}, | |||||
add_update_counter_button: (frm) => { | |||||
frm.add_custom_button(__('Update Counter'), function() { | |||||
const fields = [{ | |||||
fieldtype: 'Data', | |||||
fieldname: 'new_counter', | |||||
label: __('New Counter'), | |||||
default: frm.doc.counter, | |||||
reqd: 1, | |||||
description: __('Warning: Updating counter may lead to document name conflicts if not done properly') | |||||
}]; | |||||
let primary_action_label = __('Save'); | |||||
let primary_action = (fields) => { | |||||
frappe.call({ | |||||
method: 'frappe.core.doctype.document_naming_rule.document_naming_rule.update_current', | |||||
args: { | |||||
name: frm.doc.name, | |||||
new_counter: fields.new_counter | |||||
}, | |||||
callback: function() { | |||||
frm.set_value("counter", fields.new_counter); | |||||
dialog.hide(); | |||||
} | |||||
}); | |||||
}; | |||||
const dialog = new frappe.ui.Dialog({ | |||||
title: __('Update Counter Value for Prefix: {0}', [frm.doc.prefix]), | |||||
fields, | |||||
primary_action_label, | |||||
primary_action | |||||
}); | |||||
dialog.show(); | |||||
}); | |||||
} | } | ||||
}); | }); |
@@ -30,3 +30,8 @@ class DocumentNamingRule(Document): | |||||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 | counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 | ||||
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) | doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) | ||||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) | frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) | ||||
@frappe.whitelist() | |||||
def update_current(name, new_counter): | |||||
frappe.only_for('System Manager') | |||||
frappe.db.set_value('Document Naming Rule', name, 'counter', new_counter) |
@@ -17,6 +17,7 @@ from frappe.model.workflow import set_workflow_state_on_action | |||||
from frappe.utils.global_search import update_global_search | from frappe.utils.global_search import update_global_search | ||||
from frappe.integrations.doctype.webhook import run_webhooks | from frappe.integrations.doctype.webhook import run_webhooks | ||||
from frappe.desk.form.document_follow import follow_document | from frappe.desk.form.document_follow import follow_document | ||||
from frappe.desk.utils import slug | |||||
from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event | from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event | ||||
# once_only validation | # once_only validation | ||||
@@ -1202,8 +1203,8 @@ class Document(BaseDocument): | |||||
doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.parentfield))) | doc.set(fieldname, flt(doc.get(fieldname), self.precision(fieldname, doc.parentfield))) | ||||
def get_url(self): | def get_url(self): | ||||
"""Returns Desk URL for this document. `/app/Form/{doctype}/{name}`""" | |||||
return "/app/Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name) | |||||
"""Returns Desk URL for this document. `/app/{doctype}/{name}`""" | |||||
return f"/app/{slug(self.doctype)}/{self.name}" | |||||
def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None): | def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None): | ||||
"""Add a comment to this document. | """Add a comment to this document. | ||||
@@ -14,8 +14,10 @@ frappe.ui.form.ControlCheck = class ControlCheck extends frappe.ui.form.ControlD | |||||
</div>`).appendTo(this.parent); | </div>`).appendTo(this.parent); | ||||
} | } | ||||
set_input_areas() { | set_input_areas() { | ||||
this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0); | |||||
this.input_area = this.$wrapper.find(".input-area").get(0); | this.input_area = this.$wrapper.find(".input-area").get(0); | ||||
if (this.only_input) return; | |||||
this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0); | |||||
this.disp_area = this.$wrapper.find(".disp-area").get(0); | this.disp_area = this.$wrapper.find(".disp-area").get(0); | ||||
} | } | ||||
make_input() { | make_input() { | ||||
@@ -372,7 +372,8 @@ frappe.router = { | |||||
strip_prefix(route) { | strip_prefix(route) { | ||||
if (route.substr(0, 1)=='/') route = route.substr(1); // for /app/sub | if (route.substr(0, 1)=='/') route = route.substr(1); // for /app/sub | ||||
if (route.startsWith('app')) route = route.substr(4); // for desk/sub | |||||
if (route.startsWith('app/')) route = route.substr(4); // for desk/sub | |||||
if (route == 'app') route = route.substr(4); // for /app | |||||
if (route.substr(0, 1)=='/') route = route.substr(1); | if (route.substr(0, 1)=='/') route = route.substr(1); | ||||
if (route.substr(0, 1)=='#') route = route.substr(1); | if (route.substr(0, 1)=='#') route = route.substr(1); | ||||
if (route.substr(0, 1)=='!') route = route.substr(1); | if (route.substr(0, 1)=='!') route = route.substr(1); | ||||
@@ -140,7 +140,7 @@ | |||||
.checkbox { | .checkbox { | ||||
margin: 0px; | margin: 0px; | ||||
text-align: center; | text-align: center; | ||||
margin-top: 9px; | |||||
margin-top: var(--padding-sm); | |||||
} | } | ||||
textarea { | textarea { | ||||
@@ -13,6 +13,14 @@ $input-height: 28px !default; | |||||
--text-2xl: 20px; | --text-2xl: 20px; | ||||
--text-3xl: 22px; | --text-3xl: 22px; | ||||
// breakpoints | |||||
--xxl-width: map-get($grid-breakpoints, '2xl'); | |||||
--xl-width: map-get($grid-breakpoints, 'xl'); | |||||
--lg-width: map-get($grid-breakpoints, 'lg'); | |||||
--md-width: map-get($grid-breakpoints, 'md'); | |||||
--sm-width: map-get($grid-breakpoints, 'sm'); | |||||
--xs-width: map-get($grid-breakpoints, 'xs'); | |||||
--text-bold: 500; | --text-bold: 500; | ||||
--navbar-height: 60px; | --navbar-height: 60px; | ||||
@@ -1,178 +1,170 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import unittest, frappe, os | |||||
from frappe.core.doctype.user.user import generate_keys | |||||
from frappe.frappeclient import FrappeClient, FrappeException | |||||
from frappe.utils.data import get_url | |||||
import unittest | |||||
from random import choice | |||||
import requests | import requests | ||||
import base64 | |||||
class TestAPI(unittest.TestCase): | |||||
def test_insert_many(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title in ('Sing','a','song','of','sixpence')") | |||||
frappe.db.commit() | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "Sing"}, | |||||
{"doctype": "Note", "public": True, "title": "a"}, | |||||
{"doctype": "Note", "public": True, "title": "song"}, | |||||
{"doctype": "Note", "public": True, "title": "of"}, | |||||
{"doctype": "Note", "public": True, "title": "sixpence"}, | |||||
]) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'Sing'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'a'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'song'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'of'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'sixpence'})) | |||||
def test_create_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'test_create'") | |||||
frappe.db.commit() | |||||
server.insert({"doctype": "Note", "public": True, "title": "test_create"}) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'test_create'})) | |||||
def test_list_docs(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
doc_list = server.get_list("Note") | |||||
self.assertTrue(len(doc_list)) | |||||
def test_get_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'get_this'") | |||||
frappe.db.commit() | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "get_this"}, | |||||
]) | |||||
doc = server.get_doc("Note", "get_this") | |||||
self.assertTrue(doc) | |||||
def test_get_value(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'get_value'") | |||||
frappe.db.commit() | |||||
test_content = "test get value" | |||||
from semantic_version import Version | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "get_value", "content": test_content}, | |||||
]) | |||||
self.assertEqual(server.get_value("Note", "content", {"title": "get_value"}).get('content'), test_content) | |||||
name = server.get_value("Note", "name", {"title": "get_value"}).get('name') | |||||
import frappe | |||||
from frappe.utils import get_site_url | |||||
# test by name | |||||
self.assertEqual(server.get_value("Note", "content", name).get('content'), test_content) | |||||
self.assertRaises(FrappeException, server.get_value, "Note", "(select (password) from(__Auth) order by name desc limit 1)", {"title": "get_value"}) | |||||
def test_get_single(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
server.set_value('Website Settings', 'Website Settings', 'title_prefix', 'test-prefix') | |||||
self.assertEqual(server.get_value('Website Settings', 'title_prefix', 'Website Settings').get('title_prefix'), 'test-prefix') | |||||
self.assertEqual(server.get_value('Website Settings', 'title_prefix').get('title_prefix'), 'test-prefix') | |||||
frappe.db.set_value('Website Settings', None, 'title_prefix', '') | |||||
def test_update_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title in ('Sing','sing')") | |||||
frappe.db.commit() | |||||
server.insert({"doctype":"Note", "public": True, "title": "Sing"}) | |||||
doc = server.get_doc("Note", 'Sing') | |||||
changed_title = "sing" | |||||
doc["title"] = changed_title | |||||
doc = server.update(doc) | |||||
self.assertTrue(doc["title"] == changed_title) | |||||
def test_update_child_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabContact` where first_name = 'George' and last_name = 'Steevens'") | |||||
frappe.db.sql("delete from `tabContact` where first_name = 'William' and last_name = 'Shakespeare'") | |||||
frappe.db.sql("delete from `tabCommunication` where reference_doctype = 'Event'") | |||||
frappe.db.sql("delete from `tabCommunication Link` where link_doctype = 'Contact'") | |||||
frappe.db.sql("delete from `tabEvent` where subject = 'Sing a song of sixpence'") | |||||
frappe.db.sql("delete from `tabEvent Participants` where reference_doctype = 'Contact'") | |||||
def maintain_state(f): | |||||
def wrapper(*args, **kwargs): | |||||
frappe.db.rollback() | |||||
r = f(*args, **kwargs) | |||||
frappe.db.commit() | frappe.db.commit() | ||||
# create multiple contacts | |||||
server.insert_many([ | |||||
{"doctype": "Contact", "first_name": "George", "last_name": "Steevens"}, | |||||
{"doctype": "Contact", "first_name": "William", "last_name": "Shakespeare"} | |||||
]) | |||||
# create an event with one of the created contacts | |||||
event = server.insert({ | |||||
"doctype": "Event", | |||||
"subject": "Sing a song of sixpence", | |||||
"event_participants": [{ | |||||
"reference_doctype": "Contact", | |||||
"reference_docname": "George Steevens" | |||||
}] | |||||
}) | |||||
# update the event's contact to the second contact | |||||
server.update({ | |||||
"doctype": "Event Participants", | |||||
"name": event.get("event_participants")[0].get("name"), | |||||
"reference_docname": "William Shakespeare" | |||||
}) | |||||
# the change should run the parent document's validations and | |||||
# create a Communication record with the new contact | |||||
self.assertTrue(frappe.db.exists("Communication Link", {"link_name": "William Shakespeare"})) | |||||
def test_delete_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'delete'") | |||||
frappe.db.commit() | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "delete"}, | |||||
]) | |||||
server.delete("Note", "delete") | |||||
self.assertFalse(frappe.db.get_value('Note', {'title': 'delete'})) | |||||
def test_auth_via_api_key_secret(self): | |||||
# generate API key and API secret for administrator | |||||
keys = generate_keys("Administrator") | |||||
frappe.db.commit() | |||||
generated_secret = frappe.utils.password.get_decrypted_password( | |||||
"User", "Administrator", fieldname='api_secret' | |||||
return r | |||||
return wrapper | |||||
class TestResourceAPI(unittest.TestCase): | |||||
SITE_URL = get_site_url(frappe.local.site) | |||||
RESOURCE_URL = f"{SITE_URL}/api/resource" | |||||
DOCTYPE = "ToDo" | |||||
GENERATED_DOCUMENTS = [] | |||||
@classmethod | |||||
@maintain_state | |||||
def setUpClass(self): | |||||
for _ in range(10): | |||||
doc = frappe.get_doc( | |||||
{"doctype": "ToDo", "description": frappe.mock("paragraph")} | |||||
).insert() | |||||
self.GENERATED_DOCUMENTS.append(doc.name) | |||||
@classmethod | |||||
@maintain_state | |||||
def tearDownClass(self): | |||||
for name in self.GENERATED_DOCUMENTS: | |||||
frappe.delete_doc_if_exists(self.DOCTYPE, name) | |||||
@property | |||||
def sid(self): | |||||
if not getattr(self, "_sid", None): | |||||
self._sid = requests.post( | |||||
f"{self.SITE_URL}/api/method/login", | |||||
data={ | |||||
"usr": "Administrator", | |||||
"pwd": frappe.conf.admin_password or "admin", | |||||
}, | |||||
).cookies.get("sid") | |||||
return self._sid | |||||
def get(self, path, params=""): | |||||
return requests.get(f"{self.RESOURCE_URL}/{path}?sid={self.sid}{params}") | |||||
def post(self, path, data): | |||||
return requests.post( | |||||
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data) | |||||
) | ) | ||||
api_key = frappe.db.get_value("User", "Administrator", "api_key") | |||||
header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 200) | |||||
self.assertEqual("Administrator", res.json()["message"]) | |||||
self.assertEqual(keys['api_secret'], generated_secret) | |||||
header = {"Authorization": "Basic {}".format(base64.b64encode(frappe.safe_encode("{}:{}".format(api_key, generated_secret))).decode())} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 200) | |||||
self.assertEqual("Administrator", res.json()["message"]) | |||||
# Valid api key, invalid api secret | |||||
api_secret = "ksk&93nxoe3os" | |||||
header = {"Authorization": "token {}:{}".format(api_key, api_secret)} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 403) | |||||
def put(self, path, data): | |||||
return requests.put( | |||||
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data) | |||||
) | |||||
# random api key and api secret | |||||
api_key = "@3djdk3kld" | |||||
api_secret = "ksk&93nxoe3os" | |||||
header = {"Authorization": "token {}:{}".format(api_key, api_secret)} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 401) | |||||
def delete(self, path): | |||||
return requests.delete(f"{self.RESOURCE_URL}/{path}?sid={self.sid}") | |||||
def test_unauthorized_call(self): | |||||
# test 1: fetch documents without auth | |||||
response = requests.get(f"{self.RESOURCE_URL}/{self.DOCTYPE}") | |||||
self.assertEqual(response.status_code, 403) | |||||
def test_get_list(self): | |||||
# test 2: fetch documents without params | |||||
response = self.get(self.DOCTYPE) | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertIsInstance(response.json(), dict) | |||||
self.assertIn("data", response.json()) | |||||
def test_get_list_limit(self): | |||||
# test 3: fetch data with limit | |||||
response = self.get(self.DOCTYPE, "&limit=2") | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertEqual(len(response.json()["data"]), 2) | |||||
def test_get_list_dict(self): | |||||
# test 4: fetch response as (not) dict | |||||
response = self.get(self.DOCTYPE, "&as_dict=True") | |||||
json = frappe._dict(response.json()) | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertIsInstance(json.data, list) | |||||
self.assertIsInstance(json.data[0], dict) | |||||
response = self.get(self.DOCTYPE, "&as_dict=False") | |||||
json = frappe._dict(response.json()) | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertIsInstance(json.data, list) | |||||
self.assertIsInstance(json.data[0], list) | |||||
def test_get_list_debug(self): | |||||
# test 5: fetch response with debug | |||||
response = self.get(self.DOCTYPE, "&debug=true") | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertIn("exc", response.json()) | |||||
self.assertIsInstance(response.json()["exc"], str) | |||||
self.assertIsInstance(eval(response.json()["exc"]), list) | |||||
def test_get_list_fields(self): | |||||
# test 6: fetch response with fields | |||||
response = self.get(self.DOCTYPE, r'&fields=["description"]') | |||||
self.assertEqual(response.status_code, 200) | |||||
json = frappe._dict(response.json()) | |||||
self.assertIn("description", json.data[0]) | |||||
def test_create_document(self): | |||||
# test 7: POST method on /api/resource to create doc | |||||
data = {"description": frappe.mock("paragraph")} | |||||
response = self.post(self.DOCTYPE, data) | |||||
self.assertEqual(response.status_code, 200) | |||||
docname = response.json()["data"]["name"] | |||||
self.assertIsInstance(docname, str) | |||||
self.GENERATED_DOCUMENTS.append(docname) | |||||
def test_update_document(self): | |||||
# test 8: PUT method on /api/resource to update doc | |||||
generated_desc = frappe.mock("paragraph") | |||||
data = {"description": generated_desc} | |||||
random_doc = choice(self.GENERATED_DOCUMENTS) | |||||
desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description") | |||||
response = self.put(f"{self.DOCTYPE}/{random_doc}", data=data) | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertNotEqual(response.json()["data"]["description"], desc_before_update) | |||||
self.assertEqual(response.json()["data"]["description"], generated_desc) | |||||
def test_delete_document(self): | |||||
# test 9: DELETE method on /api/resource | |||||
doc_to_delete = choice(self.GENERATED_DOCUMENTS) | |||||
response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}") | |||||
self.assertEqual(response.status_code, 202) | |||||
self.assertDictEqual(response.json(), {"message": "ok"}) | |||||
non_existent_doc = frappe.generate_hash(length=12) | |||||
response = self.delete(f"{self.DOCTYPE}/{non_existent_doc}") | |||||
self.assertEqual(response.status_code, 404) | |||||
self.assertDictEqual(response.json(), {}) | |||||
class TestMethodAPI(unittest.TestCase): | |||||
METHOD_URL = f"{get_site_url(frappe.local.site)}/api/method" | |||||
def test_version(self): | |||||
# test 1: test for /api/method/version | |||||
response = requests.get(f"{self.METHOD_URL}/version") | |||||
json = frappe._dict(response.json()) | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertIsInstance(json, dict) | |||||
self.assertIsInstance(json.message, str) | |||||
self.assertEqual(Version(json.message), Version(frappe.__version__)) | |||||
def test_ping(self): | |||||
# test 2: test for /api/method/ping | |||||
response = requests.get(f"{self.METHOD_URL}/ping") | |||||
self.assertEqual(response.status_code, 200) | |||||
self.assertIsInstance(response.json(), dict) | |||||
self.assertEqual(response.json()['message'], "pong") |
@@ -0,0 +1,177 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
import unittest, frappe | |||||
from frappe.core.doctype.user.user import generate_keys | |||||
from frappe.frappeclient import FrappeClient, FrappeException | |||||
from frappe.utils.data import get_url | |||||
import requests | |||||
import base64 | |||||
class TestFrappeClient(unittest.TestCase): | |||||
def test_insert_many(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title in ('Sing','a','song','of','sixpence')") | |||||
frappe.db.commit() | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "Sing"}, | |||||
{"doctype": "Note", "public": True, "title": "a"}, | |||||
{"doctype": "Note", "public": True, "title": "song"}, | |||||
{"doctype": "Note", "public": True, "title": "of"}, | |||||
{"doctype": "Note", "public": True, "title": "sixpence"}, | |||||
]) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'Sing'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'a'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'song'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'of'})) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'sixpence'})) | |||||
def test_create_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'test_create'") | |||||
frappe.db.commit() | |||||
server.insert({"doctype": "Note", "public": True, "title": "test_create"}) | |||||
self.assertTrue(frappe.db.get_value('Note', {'title': 'test_create'})) | |||||
def test_list_docs(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
doc_list = server.get_list("Note") | |||||
self.assertTrue(len(doc_list)) | |||||
def test_get_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'get_this'") | |||||
frappe.db.commit() | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "get_this"}, | |||||
]) | |||||
doc = server.get_doc("Note", "get_this") | |||||
self.assertTrue(doc) | |||||
def test_get_value(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'get_value'") | |||||
frappe.db.commit() | |||||
test_content = "test get value" | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "get_value", "content": test_content}, | |||||
]) | |||||
self.assertEqual(server.get_value("Note", "content", {"title": "get_value"}).get('content'), test_content) | |||||
name = server.get_value("Note", "name", {"title": "get_value"}).get('name') | |||||
# test by name | |||||
self.assertEqual(server.get_value("Note", "content", name).get('content'), test_content) | |||||
self.assertRaises(FrappeException, server.get_value, "Note", "(select (password) from(__Auth) order by name desc limit 1)", {"title": "get_value"}) | |||||
def test_get_single(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
server.set_value('Website Settings', 'Website Settings', 'title_prefix', 'test-prefix') | |||||
self.assertEqual(server.get_value('Website Settings', 'title_prefix', 'Website Settings').get('title_prefix'), 'test-prefix') | |||||
self.assertEqual(server.get_value('Website Settings', 'title_prefix').get('title_prefix'), 'test-prefix') | |||||
frappe.db.set_value('Website Settings', None, 'title_prefix', '') | |||||
def test_update_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title in ('Sing','sing')") | |||||
frappe.db.commit() | |||||
server.insert({"doctype":"Note", "public": True, "title": "Sing"}) | |||||
doc = server.get_doc("Note", 'Sing') | |||||
changed_title = "sing" | |||||
doc["title"] = changed_title | |||||
doc = server.update(doc) | |||||
self.assertTrue(doc["title"] == changed_title) | |||||
def test_update_child_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabContact` where first_name = 'George' and last_name = 'Steevens'") | |||||
frappe.db.sql("delete from `tabContact` where first_name = 'William' and last_name = 'Shakespeare'") | |||||
frappe.db.sql("delete from `tabCommunication` where reference_doctype = 'Event'") | |||||
frappe.db.sql("delete from `tabCommunication Link` where link_doctype = 'Contact'") | |||||
frappe.db.sql("delete from `tabEvent` where subject = 'Sing a song of sixpence'") | |||||
frappe.db.sql("delete from `tabEvent Participants` where reference_doctype = 'Contact'") | |||||
frappe.db.commit() | |||||
# create multiple contacts | |||||
server.insert_many([ | |||||
{"doctype": "Contact", "first_name": "George", "last_name": "Steevens"}, | |||||
{"doctype": "Contact", "first_name": "William", "last_name": "Shakespeare"} | |||||
]) | |||||
# create an event with one of the created contacts | |||||
event = server.insert({ | |||||
"doctype": "Event", | |||||
"subject": "Sing a song of sixpence", | |||||
"event_participants": [{ | |||||
"reference_doctype": "Contact", | |||||
"reference_docname": "George Steevens" | |||||
}] | |||||
}) | |||||
# update the event's contact to the second contact | |||||
server.update({ | |||||
"doctype": "Event Participants", | |||||
"name": event.get("event_participants")[0].get("name"), | |||||
"reference_docname": "William Shakespeare" | |||||
}) | |||||
# the change should run the parent document's validations and | |||||
# create a Communication record with the new contact | |||||
self.assertTrue(frappe.db.exists("Communication Link", {"link_name": "William Shakespeare"})) | |||||
def test_delete_doc(self): | |||||
server = FrappeClient(get_url(), "Administrator", "admin", verify=False) | |||||
frappe.db.sql("delete from `tabNote` where title = 'delete'") | |||||
frappe.db.commit() | |||||
server.insert_many([ | |||||
{"doctype": "Note", "public": True, "title": "delete"}, | |||||
]) | |||||
server.delete("Note", "delete") | |||||
self.assertFalse(frappe.db.get_value('Note', {'title': 'delete'})) | |||||
def test_auth_via_api_key_secret(self): | |||||
# generate API key and API secret for administrator | |||||
keys = generate_keys("Administrator") | |||||
frappe.db.commit() | |||||
generated_secret = frappe.utils.password.get_decrypted_password( | |||||
"User", "Administrator", fieldname='api_secret' | |||||
) | |||||
api_key = frappe.db.get_value("User", "Administrator", "api_key") | |||||
header = {"Authorization": "token {}:{}".format(api_key, generated_secret)} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 200) | |||||
self.assertEqual("Administrator", res.json()["message"]) | |||||
self.assertEqual(keys['api_secret'], generated_secret) | |||||
header = {"Authorization": "Basic {}".format(base64.b64encode(frappe.safe_encode("{}:{}".format(api_key, generated_secret))).decode())} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 200) | |||||
self.assertEqual("Administrator", res.json()["message"]) | |||||
# Valid api key, invalid api secret | |||||
api_secret = "ksk&93nxoe3os" | |||||
header = {"Authorization": "token {}:{}".format(api_key, api_secret)} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 403) | |||||
# random api key and api secret | |||||
api_key = "@3djdk3kld" | |||||
api_secret = "ksk&93nxoe3os" | |||||
header = {"Authorization": "token {}:{}".format(api_key, api_secret)} | |||||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) | |||||
self.assertEqual(res.status_code, 401) |
@@ -622,6 +622,26 @@ def ceil(s): | |||||
def cstr(s, encoding='utf-8'): | def cstr(s, encoding='utf-8'): | ||||
return frappe.as_unicode(s, encoding) | return frappe.as_unicode(s, encoding) | ||||
def sbool(x): | |||||
"""Converts str object to Boolean if possible. | |||||
Example: | |||||
"true" becomes True | |||||
"1" becomes True | |||||
"{}" remains "{}" | |||||
Args: | |||||
x (str): String to be converted to Bool | |||||
Returns: | |||||
object: Returns Boolean or type(x) | |||||
""" | |||||
from distutils.util import strtobool | |||||
try: | |||||
return bool(strtobool(x)) | |||||
except Exception: | |||||
return x | |||||
def rounded(num, precision=0): | def rounded(num, precision=0): | ||||
"""round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3""" | """round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3""" | ||||
precision = cint(precision) | precision = cint(precision) | ||||