Frappe API 的界面测试

你可以编写集成测试,也可以直接使用 QUnit 在 JavaScript 中编写测试

QUnit 帮助你使用 QUnit 框架和原生 frappe API 编写 UI 测试。你可能已经猜到了,这是一种更快的测试编写方式。

测试运行器

要编写基于 QUnit 的测试,请将测试添加到应用程序的 tests/ui 文件夹中。测试文件必须以 test_ 开头,并以 .js 扩展名结尾。

要运行你的文件,你可以使用测试运行器。测试运行器提供了一个用户界面,用于加载所有 QUnit 测试并在浏览器中运行它们。

在 CI 中,所有 QUnit 测试都由测试运行器使用 frappe/tests/test_test_runner.py 运行

运行测试

要运行基于测试运行器的测试,请使用 run-ui-tests bench 命令,并传入你想运行的文件名。

bench run-ui-tests –test frappe/tests/ui/test_list.js

这会将文件名传递给 test_test_runner.py,它会在浏览器中加载所需的 JS 并执行测试

调试测试

要调试测试,你可以从 UI 中的测试运行器打开它,并手动运行以查看它究竟在哪里失败。

测试顺序

在 Frappe 中,UI 测试按固定顺序运行以确保依赖关系。

测试运行的顺序将在 tests/ui/tests.txt
文件中。

运行所有 UI 测试

要一起运行应用程序的所有 UI 测试,请运行

bench run-ui-tests –app [app_name]

这将逐一运行 tests/ui 文件夹中的所有文件。

QUnit 测试示例

以下是 QUnit 中 To Do 测试的示例

QUnit.test(“Test quick entry”, function(assert) {
assert.expect(2);
let done = assert.async();
let random_text = frappe.utils.get_random(10);

frappe.run_serially([
() => frappe.set_route(‘List’, ‘ToDo’),
() => frappe.new_doc(‘ToDo’),
() => frappe.quick_entry.dialog.set_value(‘description’, random_text),
() => frappe.quick_entry.insert(),
(doc) => {
assert.ok(doc && !doc.__islocal);
return frappe.set_route(‘Form’, ‘ToDo’, doc.name);
},
() => assert.ok(cur_frm.doc.description.includes(random_text)),

// Delete the created ToDo
() => frappe.tests.click_page_head_item(‘Menu’),
() => frappe.tests.click_dropdown_item(‘Delete’),
() => frappe.tests.click_page_head_item(‘Yes’),

() => done()
]);
});

使用 Promise 编写对测试友好的代码

Promise 是编写对测试友好的代码的好方法。如果你的方法调用了异步调用(ajax),那么你应该返回一个 Promise 对象。在编写测试时,如果你遇到一个不返回 Promise 对象的函数,你应该更新代码以返回一个 Promise 对象。

界面集成测试

你可以使用 Cypress 来编写集成测试。它是一个基于 NodeJS 的全栈测试框架,不依赖 Selenium。

要编写集成测试,请在 cypress/integration 目录中创建一个 .js 文件。

示例

以下是一个用于检查待办事项插入的集成测试示例

context('ToDo', () => {
 before(() => {
 cy.login('Administrator', 'admin');
 cy.visit('/desk');
 });

 it('creates a new todo', () => {
 cy.visit('/app/todo/new-todo-1');
 cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
 cy.get('.page-title').should('contain', 'Not Saved');
 cy.get('.primary-action').click();
 cy.visit('/desk#List/ToDo');
 cy.location('hash').should('eq', '/app/todo');
 cy.get('.list-row').should('contain', 'this is a test todo');
 });
});

在本地运行 Cypress

Cypress 使用你系统上安装的任何基于 Chromium 的浏览器来运行测试。每个应用都有自己的 Cypress 测试套件。
例如,要运行 frappe 应用的测试,请运行以下命令

cd ~/frappe-bench/apps/frappe
yarn cypress:open

这将打开 Cypress Electron 界面,你可以在其中手动运行任何测试,或运行所有测试。

单元测试

1.简介

Frappe 提供了一些基本工具,可以快速编写自动化测试。以下是一些基本规则:

  1. 测试可以放在仓库中的任何位置,但必须以 test_ 开头,并且应该是一个 .py 文件。
  2. 测试必须在以 test_ 开头的站点上运行。这是为了防止意外丢失数据。
  3. 新建 DocType 时会自动生成测试桩。
  4. Frappe 测试运行器会自动为通过 Link 类型字段(外键)标识的依赖 DocType 构建测试记录。
  5. 可以使用 bench run-tests 来执行测试。
  6. 对于非 DocType 测试,你可以编写简单的单元测试,并在文件名前加上 test_。

为 DocType 编写测试

2.1. 编写 DocType 测试:

  1. 测试用例放在名为 test_[doctype].py 的文件中。
  2. 你必须在测试文件中创建所有依赖项。
  3. 创建一个 Python 模块结构来生成测试数据/依赖项。

示例(针对 test_event.py):

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

import frappe
import frappe.defaults

from frappe.tests.utils import FrappeTestCase

def create_events():
    if frappe.flags.test_events_created:
        return

frappe.set_user("Administrator")
doc = frappe.get_doc({
    "doctype": "Event",
    "subject":"_Test Event 1",
    "starts_on": "2014-01-01",
    "event_type": "Public"
    }).insert()

doc = frappe.get_doc({
    "doctype": "Event",
    "subject":"_Test Event 2",
    "starts_on": "2014-01-01",
    "event_type": "Private"
    }).insert()

doc = frappe.get_doc({
    "doctype": "Event",
    "subject":"_Test Event 3",
    "starts_on": "2014-01-01",
    "event_type": "Public"
    "event_individuals": [{
        "person": "[email protected]"
    }]
    }).insert()

frappe.flags.test_events_created = True

class TestEvent(FrappeTestCase):
    def setUp(self):
        create_events()

    def tearDown(self):
        frappe.set_user("Administrator")

    def test_allowed_public(self):
        frappe.set_user("[email protected]")
        doc = frappe.get_doc("Event", frappe.db.get_value("Event",
        {"subject":"_Test Event 1"}))
        self.assertTrue(frappe.has_permission("Event", doc=doc))

    def test_not_allowed_private(self):
        frappe.set_user("[email protected]")
        doc = frappe.get_doc("Event", frappe.db.get_value("Event",
        {"subject":"_Test Event 2"}))
        self.assertFalse(frappe.has_permission("Event", doc=doc))

    def test_allowed_private_if_in_event_user(self):
        doc = frappe.get_doc("Event", frappe.db.get_value("Event",
        {"subject":"_Test Event 3"}))

        frappe.set_user("[email protected]")
        self.assertTrue(frappe.has_permission("Event", doc=doc))

    def test_event_list(self):
        frappe.set_user("[email protected]")
        res = frappe.get_list("Event", filters=[["Event", "subject", "like", "_Test Event%"]], fields=["name", "subject"])
        self.assertEqual(len(res), 2)
        subjects = [r.subject for r in res]
        self.assertTrue("_Test Event 1" in subjects)
        self.assertTrue("_Test Event 3" in subjects)
        self.assertFalse("_Test Event 2" in subjects)

2. 运行测试

这个函数会构建所有测试依赖项并运行你的测试。
你应该从 “frappe_bench” 文件夹运行测试。如果不加任何选项,所有测试都会被执行。

bench run-tests

如果你需要更多关于测试执行的信息,可以使用 bench 的详细日志级别。

bench –verbose run-tests

选项:

 --app <appname>
 --doctype 
<doctype>
 --test 
<specifictest>
 --module 
<module> (Run a particular module that has tests)
 --profile (Runs a Python profiler on the test)
 --junit-xml-output
<pathtoxml> (The command provides test results in the standard XUnit XML format)
</pathtoxml></module></specifictest></doctype></appname>

2.1. 应用示例:

所有应用都位于文件夹:”~/frappe-bench/apps”。
我们可以为每个应用运行测试。

  • frappe-bench/apps/erpnext/
  • frappe-bench/apps/erpnext_demo/
  • frappe-bench/apps/frappe/
 bench run-tests --app erpnext
 bench run-tests --app erpnext_demo
 bench run-tests --app frappe

2.2. DocType 示例:

 frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost"
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.008s

 OK

2.3. 测试示例:

运行 User 中的特定用例:

 frappe@erpnext:~/frappe-bench$ bench run-tests --doctype User --test test_get_value
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.005s

 OK

2.4. 模块示例:

如果我们想运行某个模块中的测试:

/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py

我们应该这样使用模块名(相对于应用文件夹)

erpnext.support.doctype.issue.test_issue

示例:
 frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry"
 ...........................
 ----------------------------------------------------------------------
 Ran 27 tests in 30.549s

2.5. Profile 示例:

 frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.010s

 OK
 9133 function calls (8912 primitive calls) in 0.011 seconds

 Ordered by: cumulative time

 ncalls tottime percall cumtime percall filename:lineno(function)
 2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert)
 1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate)
 13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql)
 255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get)
 12 0.000 0.000 0.002 0.000

2.6 不创建测试数据或 before_tests 钩子来运行测试

  • 当你在开发功能时,使用 --skip-test-records 可以在不构建测试依赖项(即为关联对象构建测试数据)的情况下编写测试,这很有用。
  • 你也可以使用 --skip-before-tests 跳过测试初始化脚本。

示例

bench --site school.erpnext.local run-tests --doctype "Student Group" --skip-test-records --skip-before-tests

3.0 FrappeTestCase

FrappeTestCase 是 Frappe 框架特有的 TestCase 类,继承自 unittest.TestCase。在你的测试中继承这个类可以确保:

  1. frappe.local.flags 和其他最常用的本地代理在测试用例运行后会被重置。
  2. 数据库——在测试用例开始前会启动一个新的数据库事务,并在测试完成后回滚。

用法

# app/doctype/dt/test_dt.py  

from frappe.tests.utils import FrappeTestCase  

class TestDt(FrappeTestCase):  
 @classmethod  
 def setUpClass(cls):  
 super().setUpClass() # important to call super() methods when extending TestCase.   
 ...

编写 XUnit XML 测试

如何运行:
bench run-tests --junit-xml-output=/reports/junit_test.xml
测试报告示例:
 <testsuite tests="3">
 <testcase classname="foo1" name="ASuccessfulTest">
 <testcase classname="foo2" name="AnotherSuccessfulTest">
 <testcase classname="foo3" name="AFailingTest">
 <failure type="NotEnoughFoo"> details about failure </failure>
 </testcase>
 </testcase></testcase></testsuite>

它是为 CI Jenkins 设计的,但也适用于任何其他能理解 XUnit 格式 XML 测试结果表示的工具。

Jenkins 配置支持:

  1. 你应该安装 xUnit 插件——https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin
  2. 安装完成后,打开 Jenkins 任务配置,在 “Post-build Actions” 下点击名为 “Publish JUnit test result report” 的复选框,然后输入 XML 报告的路径:

(示例:reports/.xml*)

自动化测试

Frappe 为你提供了一个测试框架,用于编写和执行测试,这些测试可以直接在像 Travis 这样的持续集成工具上运行

你可以编写服务器端单元测试或 UI 测试

从从库/辅助MySQL系统设置读取操作

正常情况下,系统会在同一个数据库上进行读写操作。随着数据量增加或并发活动增多,你可能会开始遇到操作延迟的问题。

避免这种延迟的一个直接解决方案是,将读写操作分开,即从副本/从数据库读取所有数据,将所有信息写入主/主数据库。

现在,在 frappe 中,你可以在主数据库和副本之间拆分读写操作。

设置只读环境的步骤

  1. 前提条件:
    你应该已经搭建好 MariaDB 主从或集群环境。

  2. 只读副本的配置:

在你的 site_config.json 中,添加以下键以启用从副本/从系统读取。

...
"read_from_replica" : 1/0 # to enable disable read from replica
"different_credentials_for_replica" : 1/0 #if database creadetials are different on replica then set 1 else 0
"replica_host" : "IP address for replica" ,
"replica_db_port" : "Replica DB port",
"replica_db_name" : "Replica DB name",
"replica_db_password" : "Replica DB password",
...

注意:如果你已启用 MariaDB 主从环境,那么两个系统上的数据库名称和数据库密码是相同的。

  1. 在从/从系统上授予主主机访问权限。

操作文档类型

文档类型(DocType)是一种特定类型的文档,例如:客户、员工或物料。

文档名称(DocumentName)是文档的唯一标识符,例如:CUST-00001、EMP-00001 或 ITEM-00001。

以下示例中缺少身份验证。更多信息请参阅[基本身份验证]和[OAuth2]。

GET /api/resource/{DocType}

获取该文档类型下的文档列表。

路径参数:

  • DocType(字符串)

你想要获取的文档类型。例如:’Customer’。

查询参数:

  • fields []

默认情况下,只返回 ‘name’ 字段。要添加更多字段,可以传入 fields 参数。例如:fields=[“name”,”country”]

  • filters [[(string)]]

你可以通过在 filters 参数中传入 SQL 条件来筛选列表。每个条件的格式为 [{doctype}, {field}, {operator}, {value}]。例如:filters=[[“Customer”, “country”, “=”, “India”]]

  • limit_page_length(整数)

所有列表都会分页显示。通过此参数可以更改页面大小(一次返回多少条记录)。默认值:20。

  • limit_start(整数)

要请求后续页面,请传入 limit_page_length 的倍数作为 limit_start。例如,要请求第二页,请将 limit_start 设为 20。

示例:

获取所有电话号码为 4915227058038 的客户的最多 20 个名称(ID)。

curl -X GET https://{your frappe instance}/api/resource/Customer?fields=["name"]\
 &filters=[["Customer","phone","=","4915227058038"]]

返回值:

 {
 "data": [
 {
 "name": "CUST-00001"
 },
 ]
 }

POST /api/resource/{DocType}

创建该文档类型的新文档。

路径参数:

  • DocType(字符串)

你想要创建的文档类型。例如:’Customer’。

Content-Type:application/json

请求体:{"fieldname": value}

示例:

创建一个名为 Mustermann 的新线索。

curl -X POST https://{your frappe instance}/api/resource/Lead \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -d '{"lead_name":"Mustermann"}'

GET /api/resource/{DocType}/{DocumentName}

按名称(ID)获取特定文档。

路径参数:

  • DocType(字符串)

你想要获取的文档类型。例如:’Customer’。

  • DocumentName(字符串)

你想要获取的文档名称(ID)。例如:’EMP-00001’。

示例:

获取名称为(ID)CUST-00001 的客户。

curl -X GET https://{your frappe instance}/api/resource/Customer/CUST-00001

PUT /api/resource/{DocType}/{DocumentName}

更新特定文档。这类似于 PATCH HTTP 请求,你不需要发送整个文档,只需发送要更改的部分。

路径参数:

  • DocType(字符串)

你想要更新的文档类型。例如:’Customer’。

  • DocumentName(字符串)

你想要更新的文档名称(ID)。例如:’EMP-00001’。

Content-Type:application/json

请求体:{"fieldname": value}

示例:

更新下次联系日期。

curl -X PUT https://{your frappe instance}/api/resource/Lead/LEAD-00001 \
 -H 'Accept: application/json' \
 -H 'Content-Type: application/json' \
 -d '{"contact_date":"2018-10-08"}'

返回值:

{
 "data": {
 "doctype": "Lead",
 "name": "LEAD-00001",
 "contact_date": "2018-10-08",
 "...": "..."
 }
}

DELETE /api/resource/{DocType}/{DocumentName}

路径参数:

  • DocType(字符串)

你想要删除的文档类型。例如:’Customer’。

  • DocumentName(字符串)

你想要删除的文档名称(ID)。例如:’EMP-00001’。

延伸阅读

HTTP 请求头:

  • Content-Type
  • Accept
  • Authorization

作者:Raffael Meyer([email protected])

列出文档

要列出文档,请向 /api/resource/{doctype} 发送 GET 请求。

GET /api/resource/Person

所有列表默认每页返回 20 条记录。要更改每页大小,可以传递查询参数 limit_page_length。要请求后续页面,请传递 limit_start。

响应以 JSON 对象形式返回,列表是一个数组,键为 data。

响应:

{
 "data": [
 {
 "name": "000000012"
 },
 {
 "name": "000000008"
 }
 ]
}

默认情况下,列表中只包含 name 字段。要添加更多字段,可以在 GET 请求中传递 fields 参数。fields 必须是一个包含字段名的 JSON 数组。

GET /api/resource/Person/?fields=[“name”,”first_name”]

响应:

{
 "data": [
 {
 "first_name": "Jane",
 "name": "000000012"
 },
 {
 "first_name": "John",
 "name": "000000008"
 }
 ]
}

你可以通过传递查询参数 filters 来使用 SQL 条件筛选列表。filters 必须是一个包含一个或多个过滤条件的 JSON 数组。每个条件的格式为 [{doctype}, {field}, {operator}, {operand}]。

例如,获取所有名字为 “Jane” 的人员的 name(id):

GET /api/resource/Person?filters=[[“Person”,”first_name”,”=”,”Jane”]]

响应:

 { "data": [ { "name": "000000012" } ] }

作者:Rushabh Mehta ([email protected])、Raffael Meyer ([email protected])

OAuth 2

使用请求头 Authorization: Bearer <access_token></access_token> 来执行需要身份验证的请求。你可以通过组合以下两个请求来获取一个持有者令牌(bearer token)。

这里有一篇非常棒的 OAuth 入门介绍:OAuth 2.0 和 OpenID Connect(通俗易懂版)

GET /api/method/frappe.integrations.oauth2.authorize

获取用户的授权码,以便代表用户访问 ERPNext。

参数(在查询字符串中):

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • state(字符串)

你的客户端应用程序用来在请求和回调之间维护状态的任意值。授权服务器在将用户代理重定向回客户端时会包含这个值。该参数应用于防止跨站请求伪造。

  • response_type(字符串)

“code”

  • scope(字符串)

应授予你的应用程序的访问范围。

  • redirect_uri(字符串)

应用程序授权后,用户将被重定向到的回调 URI。然后可以从参数中提取授权码。

  • code_challenge_method(字符串)

(可选)可以是 s256 或 plain 中的一个。

  • code_challenge(字符串)

(可选)当 code_challenge_method=s256 时可以是 base64encode(sha256(random_string)),或者当 code_challenge_method=plain 时可以是 random_string。参考 https://tools.ietf.org/html/rfc7636#appendix-A

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.authorize \
 --data-urlencode 'client_id=511cb2ac2d' \
 --data-urlencode 'state=444' \
 # base64encode(sha256('420')) => 21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0
 # --data-urlencode 'code_verifier=21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0' \
 --data-urlencode 'response_type=code'
 --data-urlencode 'scope=openid%20all' \
 --data-urlencode 'redirect_uri=https://app.getpostman.com/oauth2/callback'

返回:

  • HTTP 状态码:200
  • text/html

这将打开授权页面,然后会将你重定向到 redirect_uri。

如果用户点击”允许”,重定向 URI 将被调用,并在查询参数中附带授权码:

https://{redirect uri}?code=plkj2mqDLwaLJAgDBAkyR1W8Co08Ud&state=444

如果用户点击”拒绝”,你将收到一个错误:

https://{redirect uri}?error=access_denied

使用 ID Token 的授权码授予的令牌交换

POST /api/method/frappe.integrations.oauth2.get_token
Header Content-Type: application/x-www-form-urlencoded

注意:此端点也可用于获取刷新的访问令牌。只需在请求体中发送 refresh_token 即可。

用(上面获取的)授权码换取访问令牌。

参数(在请求体中):

  • grant_type(字符串)

“authorization_code”

  • code(字符串)

在重定向 URI 中收到的授权码。

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • redirect_uri(字符串)

客户端应用注册的重定向 URI

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token \
 -H 'Content-Type: application/x-www-form-urlencoded' \
 -H 'Accept: application/json' \
 -d 'grant_type=authorization_code&code=wa1YuQMff2ZXEAu2ZBHLpJRQXcGZdr
 &redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&client_id=af615c2d3a'

出于测试目的,你也可以像这样在 URL 中传递参数(并在浏览器中打开):

https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token?grant_type=authorization_code&code=A1KBRoYAN1uxrLAcdGLmvPKsRQLvzj&client_id=511cb2ac2d&redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback

返回:

 {
 "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1",
 "scope": "openid all",
 "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
 }

使用 ID Token 的授权码授予的令牌交换(PKCE)

POST /api/method/frappe.integrations.oauth2.get_token
Header Content-Type: application/x-www-form-urlencoded

用(上面获取的)授权码换取访问令牌。

参数(在请求体中):

  • grant_type(字符串)

“authorization_code”

  • code(字符串)

在重定向 URI 中收到的授权码。

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • redirect_uri(字符串)

客户端应用注册的重定向 URI

  • code_verifier(字符串)

在 Authorization Request 期间与 code_challenge_method 和 code_challenge 一起使用的 random_string。

Content-Type: application/x-www-form-urlencoded

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token \
 -H 'Content-Type: application/x-www-form-urlencoded' \
 -H 'Accept: application/json' \
 -d 'grant_type=authorization_code&code=wa1YuQMff2ZXEAu2ZBHLpJRQXcGZdr
 &redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&client_id=af615c2d3a&code_verifier=420'

出于测试目的,你也可以像这样在 URL 中传递参数(并在浏览器中打开):

https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token?grant_type=authorization_code&code=A1KBRoYAN1uxrLAcdGLmvPKsRQLvzj&client_id=511cb2ac2d&redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&code_verifier=420

返回:

{
 "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1",
 "scope": "openid all",
 "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
}

撤销令牌端点

POST /api/method/frappe.integrations.oauth2.revoke_token
Header: Content-Type: application/x-www-form-urlencoded

撤销令牌端点。

参数:

  • token

要撤销的访问令牌。

返回:

始终返回空响应,HTTP 状态码为 200。

 {}

Open ID Connect id_token

ID Token 是一个 JWT。

  • aud 声明包含已注册的 client_id。
  • iss 声明包含 frappe 服务器 URL。
  • sub 声明包含 Frappe 用户的 userid。
  • roles 声明包含用户角色。
  • exp 声明包含过期时间。
  • iat 声明包含签发时间。

示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkouIERvZSIsImVtYWlsIjoiakBkb2UuY29tIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDIwMjIsImF1ZCI6ImNsaWVudF9pZCJ9.ZEdnrHjLbArahVTN19b4zoRFoBO5a2BakRkR82O1VU8

使用 PyJWT 验证并提取它。

import jwt

id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkouIERvZSIsImVtYWlsIjoiakBkb2UuY29tIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDIwMjIsImF1ZCI6ImNsaWVudF9pZCIsInJvbGVzIjpbIlN5c3RlbSBNYW5hZ2VyIiwiU2FsZXMgTWFuYWdlciJdLCJub25jZSI6Ijc4OTEyMyIsImlzcyI6Imh0dHBzOi8vZXJwLmV4YW1wbGUuY29tIn0.F8Wbh5dtD1loZPltJLj_sqF9DZNeBvEbo-ITtf3UPqk"

client_id = 'client_id'
client_secret = 'client_secret'

payload = jwt.decode(
 id_token,
 audience=client_id,
 key=client_secret,
 algorithm="HS256",
 options={"verify_exp": False}, # Enabled by default to verify expiration time
)

print(payload)
# Output

{'sub': '1234567890', 'name': 'J. Doe', 'email': '[email protected]', 'iat': 1516239022, 'exp': 1516242022, 'aud': 'client_id', 'roles': ['System Manager', 'Sales Manager'], 'nonce': '789123', 'iss': 'https://erp.example.com'}

OpenID 用户信息端点

请求

GET /api/method/frappe.integrations.oauth2.openid_profile
Header: Authorization: Bearer valid_access_token

响应

{
 "sub": "1234567890",
 "name": "J. Doe",
 "given_name": "J",
 "family_name": "Doe",
 "iss": "https://erp.example.com",
 "picture": "https://erp.example.com/files/jdoe.jpg",
 "email": "[email protected]",
 "iat": 1516239022,
 "exp": 1516242022,
 "aud": "client_id",
 "roles": ["System Manager", "Sales Manager"]
}

内省令牌端点

POST /api/method/frappe.integrations.oauth2.introspect_token
Header: Content-Type: application/x-www-form-urlencoded

撤销令牌端点。

参数:

  • token_type_hint

access_token 或 refresh_token,如果未提供则默认为 access_token

  • token

要内省的访问令牌或刷新令牌。取决于 token_type_hint

返回:

{
 "client_id": "511cb2ac2d",
 "trusted_client": 1,
 "active": true,
 "exp": 1619523326,
 "scope": "openid all",
 "sub": "1234567890",
 "name": "J. Doe",
 "given_name": "J",
 "family_name": "Doe",
 "iss": "https://erp.example.com",
 "picture": "https://erp.example.com/files/jdoe.jpg",
 "email": "[email protected]",
 "iat": 1516239022,
 "exp": 1516242022,
 "aud": "511cb2ac2d",
 "roles": ["System Manager", "Sales Manager"]
}

或者

{
 "active": false,
 "_server_messages": "..."
}

进一步阅读

请查看 Guides / Integration / How To Set Up Oauth 了解如何创建新的 OAuth 2 客户端。

  • Content-Type 请求头,
  • Authorization 请求头,
  • OAuth 2 规范,
  • 持有者令牌(Bearer token)。

作者:

  • Raffael Meyer ([email protected])
  • Revant Nandgaonkar ([email protected])

基于令牌的身份验证

自 v11.0.3 版本起可用

HTTP Authorization 请求头包含用于向服务器验证用户身份的凭据。它由授权类型(token 或 Basic)和相应的令牌组成。

Authorization: <type> <token>
</token></type>

该令牌由 api-key 和 api-secret 通过冒号连接而成。

生成 API 密钥和 API 机密

  1. 前往用户列表并打开一个用户。
  2. 点击“设置”选项卡。(如果看不到选项卡,请跳过此步骤)
  3. 展开 API 访问部分,然后点击“生成密钥”。
  4. 您将看到一个包含 API 机密的弹窗。请复制此值并妥善保管(例如密码管理器)。
  5. 您还会在此部分看到另一个字段“API 密钥”。

现在,使用这两个密钥即可对 API 请求进行身份验证。使用这些密钥发出的每个请求都将记录到您在步骤 1 中选择的用户名下。这也意味着系统将针对该用户进行角色权限检查。您也可以专门为 API 调用创建一个新用户。

令牌

HTTP 请求头:

Authorization: token <api_key>:<api_secret>
</api_secret></api_key>

Python 示例:

import requests

url = "http://frappe.local:8000/api/method/frappe.auth.get_logged_user"
headers = {
    'Authorization': "token 
<api_key>:<api_secret>"
}
response = requests.request("GET", url, headers=headers)
</api_secret></api_key>

基本认证

如果使用“基本”认证方案,凭据由 api_key 和 api_secret 组合而成,构造方式如下:

  1. 将两个值用冒号连接:<api_key>:<api_secret></api_secret></api_key>
  2. 将生成的字符串进行 base64 编码:base64encode(<api_key>:<api_secret>)</api_secret></api_key>

HTTP 请求头:

Authorization: Basic base64encode("<api_key>:<api_secret>")
</api_secret></api_key>

Python 示例:

import requests
import base64

url = "http://frappe.local:8000**/api/method/frappe.auth.get_logged_user**"
headers = {
    'Authorization': "Basic %s" % base64.b64encode(
<api_key>:<api_secret>)
}
response = requests.request("GET", url, headers=headers)
</api_secret></api_key>

访问令牌

如果使用 OAuth 2 访问令牌来验证请求,该令牌是 Frappe Server 在设置 OAuth 2 并生成令牌后提供的不透明 access_token 字符串。请参阅 Guides / Integration / How To Use OAuth 2

HTTP 请求头:

Authorization: Bearer access_token

Python 示例:

import requests
import base64

url = "http://frappe.local:8000**/api/method/frappe.auth.get_logged_user**"
headers = {
    "Authorization": "Bearer %s" % access_token
}
response = requests.request("GET", url, headers=headers)

更多资源

  • 授权请求头