Skip to main content

openlark_workflow/
service.rs

1// approval v4 用户级接口(用户态,需 user_access_token)
2
3// approval v4 用户级接口的公开类型重新导出
4// 这些 Request/Body/Response 类型供用户直接 new() + builder + execute() 使用
5// (用户态接口需 user_access_token,不适合封装成 service helper)
6pub use crate::approval::approval::v4::instance::add_cc::{
7    AddCcInstanceBodyV4, AddCcInstanceRequestV4, AddCcInstanceResponseV4,
8};
9pub use crate::approval::approval::v4::instance::detail::{
10    DetailInstanceRequestV4, DetailInstanceResponseV4, DetailInstanceTaskV4,
11};
12pub use crate::approval::approval::v4::instance::initiated::{
13    InitiatedInstanceItemV4, InitiatedInstanceRequestV4, InitiatedInstanceResponseV4,
14    InstanceSummaryV4,
15};
16pub use crate::approval::approval::v4::instance::recall::{
17    RecallInstanceBodyV4, RecallInstanceRequestV4, RecallInstanceResponseV4,
18};
19pub use crate::approval::approval::v4::instance::remind::{
20    RemindInstanceBodyV4, RemindInstanceRequestV4, RemindInstanceResponseV4,
21};
22pub use crate::approval::approval::v4::task::add_sign::{
23    AddSignTaskBodyV4, AddSignTaskRequestV4, AddSignTaskResponseV4,
24};
25pub use crate::approval::approval::v4::task::forward::{
26    ForwardTaskBodyV4, ForwardTaskRequestV4, ForwardTaskResponseV4,
27};
28pub use crate::approval::approval::v4::task::list::{
29    ListTaskItemV4, ListTaskRequestV4, ListTaskResponseV4, TaskSummaryV4,
30};
31pub use crate::approval::approval::v4::task::pass::{
32    PassTaskBodyV4, PassTaskRequestV4, PassTaskResponseV4,
33};
34pub use crate::approval::approval::v4::task::refuse::{
35    RefuseTaskBodyV4, RefuseTaskRequestV4, RefuseTaskResponseV4,
36};
37pub use crate::approval::approval::v4::task::rollback::{
38    RollbackTaskBodyV4, RollbackTaskRequestV4, RollbackTaskResponseV4,
39};
40
41use openlark_core::{SDKResult, config::Config};
42use std::sync::Arc;
43
44use crate::common::constants::MAX_PAGE_SIZE;
45
46/// 任务列表查询 helper。
47///
48/// 用于封装常见的任务列表过滤条件,并让 helper 统一处理分页。
49#[derive(Debug, Clone, Default, PartialEq)]
50pub struct WorkflowTaskListQuery {
51    /// 任务清单 GUID。
52    pub tasklist_guid: Option<String>,
53    /// 分组 GUID。
54    pub section_guid: Option<String>,
55    /// 过滤条件。
56    pub filter: Option<String>,
57    /// 排序条件。
58    pub sort: Option<serde_json::Value>,
59    /// 用户 ID 类型。
60    pub user_type: Option<String>,
61    /// 分页大小。
62    pub page_size: Option<i32>,
63}
64
65impl WorkflowTaskListQuery {
66    /// 为指定任务清单创建查询条件。
67    pub fn for_tasklist(tasklist_guid: impl Into<String>) -> Self {
68        Self {
69            tasklist_guid: Some(tasklist_guid.into()),
70            ..Self::default()
71        }
72    }
73
74    /// 设置分组 GUID。
75    pub fn section_guid(mut self, section_guid: impl Into<String>) -> Self {
76        self.section_guid = Some(section_guid.into());
77        self
78    }
79
80    /// 设置过滤条件。
81    pub fn filter(mut self, filter: impl Into<String>) -> Self {
82        self.filter = Some(filter.into());
83        self
84    }
85
86    /// 设置排序条件。
87    pub fn sort(mut self, sort: serde_json::Value) -> Self {
88        self.sort = Some(sort);
89        self
90    }
91
92    /// 设置用户 ID 类型。
93    pub fn user_type(mut self, user_type: impl Into<String>) -> Self {
94        self.user_type = Some(user_type.into());
95        self
96    }
97
98    /// 设置分页大小。
99    pub fn page_size(mut self, page_size: i32) -> Self {
100        self.page_size = Some(page_size);
101        self
102    }
103}
104
105/// 任务变更 helper。
106///
107/// 只覆盖高频可变字段,不试图替代完整 typed request。
108#[derive(Debug, Clone, Default, PartialEq)]
109pub struct WorkflowTaskMutation {
110    /// 任务标题。
111    pub summary: Option<String>,
112    /// 任务描述。
113    pub description: Option<String>,
114    /// 截止时间。
115    pub due: Option<String>,
116    /// 优先级。
117    pub priority: Option<i32>,
118    /// 执行者。
119    pub assignee: Option<String>,
120    /// 状态。
121    pub status: Option<String>,
122}
123
124impl WorkflowTaskMutation {
125    /// 创建空的任务变更描述。
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    /// 设置任务标题。
131    pub fn summary(mut self, summary: impl Into<String>) -> Self {
132        self.summary = Some(summary.into());
133        self
134    }
135
136    /// 设置任务描述。
137    pub fn description(mut self, description: impl Into<String>) -> Self {
138        self.description = Some(description.into());
139        self
140    }
141
142    /// 设置截止时间。
143    pub fn due(mut self, due: impl Into<String>) -> Self {
144        self.due = Some(due.into());
145        self
146    }
147
148    /// 设置优先级。
149    pub fn priority(mut self, priority: i32) -> Self {
150        self.priority = Some(priority);
151        self
152    }
153
154    /// 设置执行者。
155    pub fn assignee(mut self, assignee: impl Into<String>) -> Self {
156        self.assignee = Some(assignee.into());
157        self
158    }
159
160    /// 设置审批状态。
161    /// 设置任务状态。
162    pub fn status(mut self, status: impl Into<String>) -> Self {
163        self.status = Some(status.into());
164        self
165    }
166}
167
168/// 审批任务查询 helper。
169///
170/// 用于封装审批待办的常见筛选条件。
171#[derive(Debug, Clone, Default, PartialEq)]
172pub struct ApprovalTaskQuery {
173    /// 用户 ID。
174    pub user_id: String,
175    /// 审批主题。
176    pub topic: String,
177    /// 用户 ID 类型。
178    pub user_id_type: Option<String>,
179    /// 审批状态。
180    pub status: Option<String>,
181    /// 实例编码。
182    pub instance_code: Option<String>,
183    /// 分页大小。
184    pub page_size: Option<i32>,
185}
186
187impl ApprovalTaskQuery {
188    /// 创建审批任务查询条件。
189    pub fn new(user_id: impl Into<String>, topic: impl Into<String>) -> Self {
190        Self {
191            user_id: user_id.into(),
192            topic: topic.into(),
193            ..Self::default()
194        }
195    }
196
197    /// 设置用户 ID 类型。
198    pub fn user_id_type(mut self, user_id_type: impl Into<String>) -> Self {
199        self.user_id_type = Some(user_id_type.into());
200        self
201    }
202
203    /// 设置审批状态。
204    /// 设置任务状态。
205    pub fn status(mut self, status: impl Into<String>) -> Self {
206        self.status = Some(status.into());
207        self
208    }
209
210    /// 设置实例编码。
211    pub fn instance_code(mut self, instance_code: impl Into<String>) -> Self {
212        self.instance_code = Some(instance_code.into());
213        self
214    }
215
216    /// 设置分页大小。
217    pub fn page_size(mut self, page_size: i32) -> Self {
218        self.page_size = Some(page_size);
219        self
220    }
221}
222
223/// 审批任务操作 helper。
224///
225/// 统一高频审批动作的 `task_id + comment` 组合。
226#[derive(Debug, Clone, Default, PartialEq)]
227pub struct ApprovalTaskAction {
228    /// 审批定义编码。
229    pub approval_code: String,
230    /// 审批实例编码。
231    pub instance_code: String,
232    /// 操作人用户 ID。
233    pub user_id: String,
234    /// 审批任务 ID。
235    pub task_id: String,
236    /// 用户 ID 类型。
237    pub user_id_type: Option<String>,
238    /// 备注。
239    pub comment: Option<String>,
240    /// 表单内容。
241    pub form: Option<String>,
242}
243
244impl ApprovalTaskAction {
245    /// 创建审批任务动作参数。
246    pub fn new(
247        approval_code: impl Into<String>,
248        instance_code: impl Into<String>,
249        user_id: impl Into<String>,
250        task_id: impl Into<String>,
251    ) -> Self {
252        Self {
253            approval_code: approval_code.into(),
254            instance_code: instance_code.into(),
255            user_id: user_id.into(),
256            task_id: task_id.into(),
257            user_id_type: None,
258            comment: None,
259            form: None,
260        }
261    }
262
263    /// 设置备注。
264    pub fn comment(mut self, comment: impl Into<String>) -> Self {
265        self.comment = Some(comment.into());
266        self
267    }
268
269    /// 设置用户 ID 类型。
270    pub fn user_id_type(mut self, user_id_type: impl Into<String>) -> Self {
271        self.user_id_type = Some(user_id_type.into());
272        self
273    }
274
275    /// 设置表单内容。
276    pub fn form(mut self, form: impl Into<String>) -> Self {
277        self.form = Some(form.into());
278        self
279    }
280}
281
282/// 审批任务条目类型别名。
283pub type ApprovalTaskItem = crate::approval::approval::v4::task::query::TaskItemV4;
284
285/// WorkflowService:工作流服务的统一入口
286///
287/// 提供对任务、审批、看板 API 的访问能力
288#[derive(Clone)]
289pub struct WorkflowService {
290    config: Arc<Config>,
291}
292
293impl WorkflowService {
294    /// 创建工作流服务入口。
295    pub fn new(config: Config) -> Self {
296        Self {
297            config: Arc::new(config),
298        }
299    }
300
301    #[cfg(feature = "v1")]
302    /// 返回 v1 任务服务入口。
303    pub fn v1(&self) -> crate::v1::TaskV1 {
304        crate::v1::TaskV1::new(self.config.clone())
305    }
306
307    #[cfg(feature = "v2")]
308    /// 返回 v2 任务服务入口。
309    pub fn v2(&self) -> crate::v2::TaskV2 {
310        crate::v2::TaskV2::new(self.config.clone())
311    }
312
313    /// 列取任务并自动处理分页。
314    #[cfg(feature = "v2")]
315    pub async fn list_tasks_all(
316        &self,
317        query: WorkflowTaskListQuery,
318    ) -> SDKResult<Vec<crate::v2::task::models::TaskItem>> {
319        use crate::v2::task::list::ListTasksRequest;
320
321        let mut items = Vec::new();
322        let mut page_token: Option<String> = None;
323
324        loop {
325            let mut request = ListTasksRequest::new(self.config.clone())
326                .page_size(query.page_size.unwrap_or(MAX_PAGE_SIZE));
327
328            if let Some(tasklist_guid) = &query.tasklist_guid {
329                request = request.tasklist_guid(tasklist_guid.clone());
330            }
331            if let Some(section_guid) = &query.section_guid {
332                request = request.section_guid(section_guid.clone());
333            }
334            if let Some(filter) = &query.filter {
335                request = request.filter(filter.clone());
336            }
337            if let Some(sort) = &query.sort {
338                request = request.sort(sort.clone());
339            }
340            if let Some(user_type) = &query.user_type {
341                request = request.user_type(user_type.clone());
342            }
343            if let Some(token) = &page_token {
344                request = request.page_token(token.clone());
345            }
346
347            let response = request.execute().await?;
348            items.extend(response.items);
349
350            if !response.has_more {
351                break;
352            }
353            page_token = response.page_token;
354        }
355
356        Ok(items)
357    }
358
359    /// 使用 helper 风格更新任务高频字段。
360    #[cfg(feature = "v2")]
361    pub async fn mutate_task(
362        &self,
363        task_guid: impl Into<String>,
364        mutation: WorkflowTaskMutation,
365    ) -> SDKResult<crate::v2::task::models::UpdateTaskResponse> {
366        use crate::v2::task::update::UpdateTaskRequest;
367
368        let mut request = UpdateTaskRequest::new(self.config.clone(), task_guid.into());
369        if let Some(summary) = mutation.summary {
370            request = request.summary(summary);
371        }
372        if let Some(description) = mutation.description {
373            request = request.description(description);
374        }
375        if let Some(due) = mutation.due {
376            request = request.due(due);
377        }
378        if let Some(priority) = mutation.priority {
379            request = request.priority(priority);
380        }
381        if let Some(assignee) = mutation.assignee {
382            request = request.assignee(assignee);
383        }
384        if let Some(status) = mutation.status {
385            request = request.status(status);
386        }
387
388        request.execute().await
389    }
390
391    /// 完成任务 helper。
392    #[cfg(feature = "v2")]
393    pub async fn complete_task(
394        &self,
395        task_guid: impl Into<String>,
396    ) -> SDKResult<crate::v2::task::models::CompleteTaskResponse> {
397        use crate::v2::task::complete::CompleteTaskRequest;
398
399        CompleteTaskRequest::new(self.config.clone(), task_guid.into())
400            .execute()
401            .await
402    }
403
404    /// 重新打开任务 helper。
405    #[cfg(feature = "v2")]
406    pub async fn reopen_task(
407        &self,
408        task_guid: impl Into<String>,
409    ) -> SDKResult<crate::v2::task::models::UncompleteTaskResponse> {
410        use crate::v2::task::uncomplete::UncompleteTaskRequest;
411
412        UncompleteTaskRequest::new(self.config.clone(), task_guid.into())
413            .execute()
414            .await
415    }
416
417    /// 查询审批任务,并支持按状态/实例做本地筛选。
418    pub async fn query_approval_tasks(
419        &self,
420        query: ApprovalTaskQuery,
421    ) -> SDKResult<Vec<ApprovalTaskItem>> {
422        let mut items = Vec::new();
423        let mut page_token: Option<String> = None;
424
425        loop {
426            let mut request = crate::approval::approval::v4::task::query::QueryTaskRequestV4::new(
427                self.config.clone(),
428            )
429            .user_id(query.user_id.clone())
430            .topic(query.topic.clone())
431            .page_size(query.page_size.unwrap_or(MAX_PAGE_SIZE));
432
433            if let Some(user_id_type) = &query.user_id_type {
434                request = request.user_id_type(user_id_type.clone());
435            }
436            if let Some(token) = &page_token {
437                request = request.page_token(token.clone());
438            }
439
440            let response = request.execute().await?;
441            items.extend(response.tasks);
442
443            if !response.has_more.unwrap_or(false) {
444                break;
445            }
446            page_token = response.page_token;
447        }
448
449        if let Some(status) = &query.status {
450            items.retain(|item| item.status == *status);
451        }
452        if let Some(instance_code) = &query.instance_code {
453            items.retain(|item| item.instance_code == *instance_code);
454        }
455
456        Ok(items)
457    }
458
459    /// 同意审批任务 helper。
460    ///
461    /// 成功/失败由 `SDKResult` 表达:飞书 approval v4 同意接口响应 data 为空,
462    /// 不再伪造恒为 `true` 的 `success` 字段(#350 P9 接口形状撒谎修正)。
463    pub async fn approve_task(&self, action: ApprovalTaskAction) -> SDKResult<()> {
464        let mut request = crate::approval::approval::v4::task::approve::ApproveTaskRequestV4::new(
465            self.config.clone(),
466        )
467        .approval_code(action.approval_code)
468        .instance_code(action.instance_code)
469        .user_id(action.user_id)
470        .task_id(action.task_id);
471        if let Some(user_id_type) = action.user_id_type {
472            request = request.user_id_type(user_id_type);
473        }
474        if let Some(comment) = action.comment {
475            request = request.comment(comment);
476        }
477        if let Some(form) = action.form {
478            request = request.form(form);
479        }
480        request.execute().await?;
481        Ok(())
482    }
483
484    /// 拒绝审批任务 helper。
485    ///
486    /// 成功/失败由 `SDKResult` 表达;响应 data 为空时不伪造 `success: true`。
487    pub async fn reject_task(&self, action: ApprovalTaskAction) -> SDKResult<()> {
488        let mut request = crate::approval::approval::v4::task::reject::RejectTaskRequestV4::new(
489            self.config.clone(),
490        )
491        .approval_code(action.approval_code)
492        .instance_code(action.instance_code)
493        .user_id(action.user_id)
494        .task_id(action.task_id);
495        if let Some(user_id_type) = action.user_id_type {
496            request = request.user_id_type(user_id_type);
497        }
498        if let Some(comment) = action.comment {
499            request = request.comment(comment);
500        }
501        if let Some(form) = action.form {
502            request = request.form(form);
503        }
504        request.execute().await?;
505        Ok(())
506    }
507
508    /// 重新提交审批任务 helper。
509    ///
510    /// 成功/失败由 `SDKResult` 表达;响应 data 为空时不伪造 `success: true`。
511    pub async fn resubmit_task(&self, action: ApprovalTaskAction) -> SDKResult<()> {
512        let mut request =
513            crate::approval::approval::v4::task::resubmit::ResubmitTaskRequestV4::new(
514                self.config.clone(),
515            )
516            .approval_code(action.approval_code)
517            .instance_code(action.instance_code)
518            .user_id(action.user_id)
519            .task_id(action.task_id);
520        if let Some(user_id_type) = action.user_id_type {
521            request = request.user_id_type(user_id_type);
522        }
523        if let Some(comment) = action.comment {
524            request = request.comment(comment);
525        }
526        if let Some(form) = action.form {
527            request = request.form(form);
528        }
529        request.execute().await?;
530        Ok(())
531    }
532}
533
534#[cfg(test)]
535#[allow(unused_imports)]
536mod tests {
537    use super::*;
538    use serde_json::json;
539
540    #[test]
541    fn test_task_list_query_builder() {
542        let query = WorkflowTaskListQuery::for_tasklist("tasklist_123")
543            .section_guid("section_456")
544            .filter("status = incomplete")
545            .sort(json!([{"field": "due", "order": "asc"}]))
546            .user_type("open_id")
547            .page_size(50);
548
549        assert_eq!(query.tasklist_guid.as_deref(), Some("tasklist_123"));
550        assert_eq!(query.section_guid.as_deref(), Some("section_456"));
551        assert_eq!(query.filter.as_deref(), Some("status = incomplete"));
552        assert_eq!(query.user_type.as_deref(), Some("open_id"));
553        assert_eq!(query.page_size, Some(50));
554    }
555
556    #[test]
557    fn test_task_mutation_builder() {
558        let mutation = WorkflowTaskMutation::new()
559            .summary("完成项目文档")
560            .description("补齐 workflow helper")
561            .due("2026-09-30T23:59:59Z")
562            .priority(3)
563            .assignee("ou_xxx")
564            .status("in_progress");
565
566        assert_eq!(mutation.summary.as_deref(), Some("完成项目文档"));
567        assert_eq!(
568            mutation.description.as_deref(),
569            Some("补齐 workflow helper")
570        );
571        assert_eq!(mutation.due.as_deref(), Some("2026-09-30T23:59:59Z"));
572        assert_eq!(mutation.priority, Some(3));
573        assert_eq!(mutation.assignee.as_deref(), Some("ou_xxx"));
574        assert_eq!(mutation.status.as_deref(), Some("in_progress"));
575    }
576
577    #[test]
578    fn test_approval_task_query_builder() {
579        let query = ApprovalTaskQuery::new("ou_xxx", "1")
580            .user_id_type("open_id")
581            .status("PENDING")
582            .instance_code("instance_123")
583            .page_size(100);
584
585        assert_eq!(query.user_id, "ou_xxx");
586        assert_eq!(query.topic, "1");
587        assert_eq!(query.user_id_type.as_deref(), Some("open_id"));
588        assert_eq!(query.status.as_deref(), Some("PENDING"));
589        assert_eq!(query.instance_code.as_deref(), Some("instance_123"));
590        assert_eq!(query.page_size, Some(100));
591    }
592
593    #[test]
594    fn test_approval_task_action_builder() {
595        let action =
596            ApprovalTaskAction::new("approval_code", "instance_code", "ou_xxx", "task_123")
597                .user_id_type("open_id")
598                .comment("已确认")
599                .form("[{}]");
600
601        assert_eq!(action.approval_code, "approval_code");
602        assert_eq!(action.instance_code, "instance_code");
603        assert_eq!(action.user_id, "ou_xxx");
604        assert_eq!(action.task_id, "task_123");
605        assert_eq!(action.user_id_type.as_deref(), Some("open_id"));
606        assert_eq!(action.comment.as_deref(), Some("已确认"));
607        assert_eq!(action.form.as_deref(), Some("[{}]"));
608    }
609
610    /// #350:approve/reject/resubmit 成功时返回 `Ok(())`,不再伪造恒真 `success`。
611    #[tokio::test]
612    async fn test_approve_reject_resubmit_helpers_return_unit_on_success() {
613        use wiremock::matchers::{method, path};
614        use wiremock::{Mock, MockServer, ResponseTemplate};
615
616        let server = MockServer::start().await;
617        let paths = [
618            "/open-apis/approval/v4/tasks/approve",
619            "/open-apis/approval/v4/tasks/reject",
620            "/open-apis/approval/v4/tasks/resubmit",
621        ];
622        for p in paths {
623            Mock::given(method("POST"))
624                .and(path(p))
625                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
626                    "code": 0,
627                    "msg": "success",
628                    "data": {}
629                })))
630                .mount(&server)
631                .await;
632        }
633
634        let service = WorkflowService::new(
635            Config::builder()
636                .app_id("ci_app_id")
637                .app_secret("ci_app_secret")
638                .base_url(server.uri())
639                .enable_token_cache(false)
640                .build(),
641        );
642
643        let action =
644            ApprovalTaskAction::new("approval_code", "instance_code", "ou_xxx", "task_123")
645                .user_id_type("open_id")
646                .comment("ok")
647                .form("[]");
648
649        service
650            .approve_task(action.clone())
651            .await
652            .expect("approve_task 应在飞书成功响应时返回 Ok(())");
653        service
654            .reject_task(action.clone())
655            .await
656            .expect("reject_task 应在飞书成功响应时返回 Ok(())");
657        service
658            .resubmit_task(action)
659            .await
660            .expect("resubmit_task 应在飞书成功响应时返回 Ok(())");
661
662        let received = server.received_requests().await.unwrap_or_default();
663        assert_eq!(
664            received.len(),
665            3,
666            "三个 helper 应各打一次飞书 approval v4 端点"
667        );
668        let hit: Vec<_> = received.iter().map(|r| r.url.path().to_string()).collect();
669        for p in paths {
670            assert!(
671                hit.iter().any(|h| h == p),
672                "missing request to {p}; got {hit:?}"
673            );
674        }
675    }
676
677    /// #350:底层失败时 helper 传播 Err,而非恒真 success。
678    ///
679    /// `ApproveTaskRequestV4` 对飞书 `code != 0` 且无 `data` 的响应走
680    /// `missing_response_data`(Validation),而不是把 `msg` 映射成 `CoreError::Api`。
681    /// 本测试锁定 helper 契约:`Err` 必须向上抛出,不能伪装 `Ok(())`。
682    #[tokio::test]
683    async fn test_approve_task_helper_propagates_api_error() {
684        use openlark_core::error::CoreError;
685        use wiremock::matchers::{method, path};
686        use wiremock::{Mock, MockServer, ResponseTemplate};
687
688        let server = MockServer::start().await;
689        Mock::given(method("POST"))
690            .and(path("/open-apis/approval/v4/tasks/approve"))
691            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
692                "code": 99991400,
693                "msg": "invalid approval task"
694            })))
695            .mount(&server)
696            .await;
697
698        let service = WorkflowService::new(
699            Config::builder()
700                .app_id("ci_app_id")
701                .app_secret("ci_app_secret")
702                .base_url(server.uri())
703                .enable_token_cache(false)
704                .build(),
705        );
706
707        let err = service
708            .approve_task(ApprovalTaskAction::new(
709                "approval_code",
710                "instance_code",
711                "ou_xxx",
712                "task_123",
713            ))
714            .await
715            .expect_err("飞书失败响应应传播为 Err,不得伪装 Ok(())");
716        assert!(
717            matches!(err, CoreError::Validation { .. } | CoreError::Api(_)),
718            "expected Validation (missing data on non-zero code) or Api, got {err:?}"
719        );
720        let msg = err.to_string();
721        assert!(
722            msg.contains("服务器没有返回有效的数据") || msg.contains("invalid approval task"),
723            "error should surface leaf validation or Feishu msg, got: {err}"
724        );
725
726        let received = server.received_requests().await.unwrap_or_default();
727        assert_eq!(received.len(), 1);
728        assert_eq!(
729            received[0].url.path(),
730            "/open-apis/approval/v4/tasks/approve"
731        );
732    }
733}