diff --git a/app/api/groups/get_participant_progress.feature b/app/api/groups/get_participant_progress.feature index 498d1cb45..5e3dcb027 100644 --- a/app/api/groups/get_participant_progress.feature +++ b/app/api/groups/get_participant_progress.feature @@ -574,11 +574,11 @@ Feature: Display the current progress of a participant on children of an item (g When I send a GET request to "/items/1020/participant-progress?as_team_id=14" Then the response code should be 200 And the response at $.item.item_id should be "1020" - And the response should not be defined at $.children + And the response at $.children should be "" Scenario: Should not return the children when the current user doesn't have a started result on the requested item with watched_group_id Given I am the user with id "22" When I send a GET request to "/items/210/participant-progress?watched_group_id=67" Then the response code should be 200 And the response at $.item.item_id should be "210" - And the response should not be defined at $.children + And the response at $.children should be "" diff --git a/app/api/groups/get_permissions.feature b/app/api/groups/get_permissions.feature index 001fb6901..f8833d59a 100644 --- a/app/api/groups/get_permissions.feature +++ b/app/api/groups/get_permissions.feature @@ -82,27 +82,32 @@ Feature: Get permissions for a group "computed": { "can_view": "none", "can_grant_view": "none", "can_edit": "none", "can_watch": "none", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "none", "can_grant_view": "none", "can_edit": "none", "can_watch": "none", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "none", "can_grant_view": "none", "can_edit": "none", "can_watch": "none", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "none", "can_grant_view": "none", "can_edit": "none", "can_watch": "none", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "none", "can_grant_view": "none", "can_edit": "none", "can_watch": "none", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -141,27 +146,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content_with_descendants", "can_grant_view": "solution", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -200,27 +210,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -259,27 +274,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -318,27 +338,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -377,27 +402,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -436,27 +466,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -495,27 +530,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] } } """ @@ -554,27 +594,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] } } """ @@ -613,27 +658,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "9999-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] } } """ @@ -672,27 +722,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2021-12-31T23:59:59Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "2022-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "2024-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "2026-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2028-12-31T23:59:59Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] } } """ @@ -731,27 +786,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2021-12-31T23:59:59Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "2027-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "2025-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "2023-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2021-12-31T23:59:59Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] } } """ @@ -790,27 +850,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2022-12-31T23:59:59Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "2028-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "2026-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "2024-12-31T23:59:59Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2022-12-31T23:59:59Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] } } """ @@ -849,27 +914,32 @@ Feature: Get permissions for a group "computed": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] }, "granted_via_group_membership": { "can_view": "content", "can_grant_view": "content", "can_edit": "children", "can_watch": "result", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_item_unlocking": { "can_view": "info", "can_grant_view": "enter", "can_edit": "all", "can_watch": "answer", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_self": { "can_view": "content", "can_grant_view": "content_with_descendants", "can_edit": "children", "can_watch": "result", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": false, "is_owner": false + "can_make_session_official": false, "is_owner": false, + "can_request_help_to": [] }, "granted_via_other": { "can_view": "solution", "can_grant_view": "solution_with_grant", "can_edit": "all_with_grant", "can_watch": "answer_with_grant", "can_enter_from": "2019-07-16T22:02:28Z", - "can_make_session_official": true, "is_owner": true + "can_make_session_official": true, "is_owner": true, + "can_request_help_to": [] } } """ diff --git a/app/api/groups/get_permissions.go b/app/api/groups/get_permissions.go index 5da7340ea..e9663665a 100644 --- a/app/api/groups/get_permissions.go +++ b/app/api/groups/get_permissions.go @@ -5,11 +5,21 @@ import ( "github.com/go-chi/render" + "github.com/France-ioi/AlgoreaBackend/app/database" "github.com/France-ioi/AlgoreaBackend/app/domain" "github.com/France-ioi/AlgoreaBackend/app/service" "github.com/France-ioi/AlgoreaBackend/app/structures" ) +const ( + OriginGroupMembership = "group_membership" + OriginItemUnlocking = "item_unlocking" + OriginSelf = "self" + OriginOther = "other" + OriginComputed = "computed" + OriginGranted = "granted" +) + type permissionsStruct struct { structures.ItemPermissions // required: true @@ -17,12 +27,15 @@ type permissionsStruct struct { } // The group which can be asked for help. -// either (ID, Name) is set, or IsAllUsersGroup is set and equal to true. type canRequestHelpTo struct { - ID int64 `json:"id,string,omitempty"` - Name string `json:"name,omitempty"` - // Whether the group is the "all-users" group - IsAllUsersGroup bool `json:"is_all_users_group,omitempty"` + // required: true + ID int64 `json:"id,string"` + // The name is present only if the group is visible to the current user. + // required: false + Name *string `json:"name,omitempty"` + // Whether the group is the "all-users" group. + // required: true + IsAllUsersGroup bool `json:"is_all_users_group"` } // Permissions granted directly to the group via `origin` = 'group_membership' and `source_group_id` = `{source_group_id}`. @@ -37,32 +50,46 @@ type grantedPermissionsStruct struct { CanRequestHelpTo *canRequestHelpTo `json:"can_request_help_to"` } -type permissionsWithCanEnterFrom struct { +type aggregatedPermissionsWithCanEnterFromStruct struct { permissionsStruct // The next time the group can enter the item (>= NOW()) // required: true CanEnterFrom string `json:"can_enter_from"` + // required: true + CanRequestHelpTo []canRequestHelpTo `json:"can_request_help_to"` } // Computed permissions for the group // (respecting permissions of its ancestors). -type computedPermissions struct{ permissionsWithCanEnterFrom } +// It combines the aggregation of permissions from the given group and its ancestors, +// with the propagation of permissions from all ancestor items computed in `permissions_generated`. +type computedPermissions struct { + aggregatedPermissionsWithCanEnterFromStruct +} // Permissions granted to the group or its ancestors // via `origin` = 'group_membership' excluding the row from `granted`. -type permissionsGrantedViaGroupMembership struct{ permissionsWithCanEnterFrom } +type permissionsGrantedViaGroupMembership struct { + aggregatedPermissionsWithCanEnterFromStruct +} // Permissions granted to the group or its ancestors // via `origin` = 'item_unlocking'. -type permissionsGrantedViaItemUnlocking struct{ permissionsWithCanEnterFrom } +type permissionsGrantedViaItemUnlocking struct { + aggregatedPermissionsWithCanEnterFromStruct +} // Permissions granted to the group or its ancestors // via `origin` = 'self'. -type permissionsGrantedViaSelf struct{ permissionsWithCanEnterFrom } +type permissionsGrantedViaSelf struct { + aggregatedPermissionsWithCanEnterFromStruct +} // Permissions granted to the group or its ancestors // via `origin` = 'other'. -type permissionsGrantedViaOther struct{ permissionsWithCanEnterFrom } +type permissionsGrantedViaOther struct { + aggregatedPermissionsWithCanEnterFromStruct +} // swagger:model permissionsViewResponse type permissionsViewResponse struct { @@ -80,6 +107,15 @@ type permissionsViewResponse struct { GrantedViaOther permissionsGrantedViaOther `json:"granted_via_other"` } +type canRequestHelpToPermissionsRaw struct { + Origin string + SourceGroupID int64 + PermissionGroupID int64 + PermissionItemID int64 + GroupID int64 + GroupName string +} + // swagger:operation GET /groups/{source_group_id}/permissions/{group_id}/{item_id} groups permissionsView // // --- @@ -161,6 +197,8 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi var permissions []map[string]interface{} + const canMakeSessionOfficialColumn = "IFNULL(MAX(can_make_session_official), 0) AS can_make_session_official" + // This sub-query retrieves a single row from permissions_granted because it does a "where" on the four columns // composing the primary key (group_id, item_id, source_group_id, origin). // @@ -172,49 +210,20 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi // then the bigger query this query is a part of would return nothing as well, and we don't want that, // because we need the values from the other parts of the bigger query. // - // The solution retained is to UNION this query with the default values, and LIMIT the result to one row. - // It is implemented in the construction of the bigger query. - // - // The method previously used was to select "IFNULL(MAX(permission_value), default_value)", but we cannot - // use an aggregation function anymore because can_request_help_to.name cannot be aggregated. - // - // Another method would have been to retrieve the group in another sub-query, - // but we would have had to ensure that the sub-query returns a row even if there is no group to fetch, - // the problem would have been the same as stated above. - // Additionally, it would have added further complexity to the bigger query. - // - // grantedPermissionsDefaultValues contains "AS" statements. - // Those are not necessary for the query to work, but are present for readability. + // We use "IFNULL(MAX(permission_value), default_value)" to return default values when there is no matching row. grantedPermissions := store.PermissionsGranted(). - Joins("LEFT JOIN `groups` AS can_request_help_to_group ON can_request_help_to_group.id = permissions_granted.can_request_help_to"). Where("group_id = ?", groupID). Where("item_id = ?", itemID). Where("source_group_id = ?", sourceGroupID). - Where("origin = 'group_membership'"). + Where("origin = ?", OriginGroupMembership). Select(` - can_view_value, - can_grant_view_value, - can_watch_value, - can_edit_value, - can_enter_from, - can_enter_until, - is_owner, - can_make_session_official, - can_request_help_to_group.id AS can_request_help_to_group_id, - can_request_help_to_group.name AS can_request_help_to_group_name - `) - grantedPermissionsDefaultValues := ` - 1 AS can_view_value, - 1 AS can_grant_view_value, - 1 AS can_watch_value, - 1 AS can_edit_value, - "9999-12-31 23:59:59" AS can_enter_from, - "9999-12-31 23:59:59" AS can_enter_until, - 0 AS is_owner, - 0 AS can_make_session_official, - NULL AS can_request_help_to_group_id, - NULL AS can_request_help_to_group_name - ` + IFNULL(MAX(can_view_value), 1) AS can_view_value, + IFNULL(MAX(can_grant_view_value), 1) AS can_grant_view_value, + IFNULL(MAX(can_watch_value), 1) AS can_watch_value, + IFNULL(MAX(can_edit_value), 1) AS can_edit_value, + IFNULL(MAX(can_enter_from), '9999-12-31 23:59:59') AS can_enter_from, + IFNULL(MAX(can_enter_until), '9999-12-31 23:59:59') AS can_enter_until, + IFNULL(MAX(is_owner), 0) AS is_owner, ` + canMakeSessionOfficialColumn) generatedPermissions := store.Permissions(). Joins("JOIN groups_ancestors_active AS ancestors ON ancestors.ancestor_group_id = permissions.group_id"). @@ -229,10 +238,9 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi ancestorPermissions := store.PermissionsGranted(). Joins("JOIN groups_ancestors_active ON groups_ancestors_active.ancestor_group_id = permissions_granted.group_id"). - Where("groups_ancestors_active.child_group_id = ?", groupID). - Where("item_id = ?", itemID) + Where("groups_ancestors_active.child_group_id = ?", groupID) + ancestorPermissionsOnItem := ancestorPermissions.Where("item_id = ?", itemID) - const canMakeSessionOfficialColumn = "IFNULL(MAX(can_make_session_official), 0) AS can_make_session_official" const canEnterFromColumn = ` IFNULL(MIN(IF( NOW() BETWEEN can_enter_from AND can_enter_until, @@ -240,7 +248,7 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi IF(can_enter_from BETWEEN NOW() AND can_enter_until, can_enter_from, '9999-12-31 23:59:59') )), '9999-12-31 23:59:59') AS can_enter_from` - grantedPermissionsWithAncestors := ancestorPermissions. + grantedPermissionsWithAncestors := ancestorPermissionsOnItem. Select(` IFNULL(MAX(can_view_value), 1) AS can_view_value, IFNULL(MAX(can_grant_view_value), 1) AS can_grant_view_value, @@ -248,14 +256,14 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi IFNULL(MAX(can_edit_value), 1) AS can_edit_value, IFNULL(MAX(is_owner), 0) AS is_owner, ` + canMakeSessionOfficialColumn + ", " + canEnterFromColumn) - aggregatedPermissions := ancestorPermissions.Select(canEnterFromColumn + ", " + canMakeSessionOfficialColumn) + aggregatedPermissions := ancestorPermissionsOnItem.Select(canEnterFromColumn + ", " + canMakeSessionOfficialColumn) grantedPermissionsGroupMembership := grantedPermissionsWithAncestors. - Where("origin = 'group_membership'"). + Where("origin = ?", OriginGroupMembership). Where("NOT (group_id = ? AND source_group_id = ?)", groupID, sourceGroupID) - grantedPermissionsItemUnlocking := grantedPermissionsWithAncestors.Where("origin = 'item_unlocking'") - grantedPermissionsSelf := grantedPermissionsWithAncestors.Where("origin = 'self'") - grantedPermissionsOther := grantedPermissionsWithAncestors.Where("origin = 'other'") + grantedPermissionsItemUnlocking := grantedPermissionsWithAncestors.Where("origin = ?", OriginItemUnlocking) + grantedPermissionsSelf := grantedPermissionsWithAncestors.Where("origin = ?", OriginSelf) + grantedPermissionsOther := grantedPermissionsWithAncestors.Where("origin = ?", OriginOther) err = store. Raw(` @@ -264,8 +272,6 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi grp.can_watch_value AS granted_directly_can_watch_value, grp.can_edit_value AS granted_directly_can_edit_value, grp.can_make_session_official AS granted_directly_can_make_session_official, grp.can_enter_from AS granted_directly_can_enter_from, grp.can_enter_until AS granted_directly_can_enter_until, grp.is_owner AS granted_directly_is_owner, - grp.can_request_help_to_group_id AS granted_directly_can_request_help_to__id, - grp.can_request_help_to_group_name AS granted_directly_can_request_help_to__name, gep.can_view_generated_value AS generated_can_view_value, gep.can_grant_view_generated_value AS generated_can_grant_view_value, gep.can_watch_generated_value AS generated_can_watch_value, gep.can_edit_generated_value AS generated_can_edit_value, @@ -300,7 +306,7 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi grp_other.can_make_session_official AS granted_anc_other_can_make_session_official, grp_other.can_enter_from AS granted_anc_other_can_enter_from, grp_other.is_owner AS granted_anc_other_is_owner - FROM (? UNION (SELECT `+grantedPermissionsDefaultValues+`) LIMIT 1) AS grp, + FROM ? AS grp, ? AS gep, ? AS grp_membership, ? AS grp_unlocking, ? AS grp_self, ? AS grp_other, ? AS grp_aggregated`, grantedPermissions.SubQuery(), generatedPermissions.SubQuery(), grantedPermissionsGroupMembership.SubQuery(), grantedPermissionsItemUnlocking.SubQuery(), grantedPermissionsSelf.SubQuery(), grantedPermissionsOther.SubQuery(), @@ -311,23 +317,18 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi permissionsRow := permissions[0] permissionsGrantedStore := store.PermissionsGranted() - var canRequestHelpToPermission *canRequestHelpTo + allUsersGroupID := domain.ConfigFromContext(r.Context()).AllUsersGroupID + canRequestHelpToByOrigin := getCanRequestHelpToByOrigin(ancestorPermissions, store, groupID, itemID, sourceGroupID, allUsersGroupID, user) - if permissionsRow["granted_directly_can_request_help_to__id"] != nil { - canRequestHelpToGroupID := permissionsRow["granted_directly_can_request_help_to__id"].(int64) - if domain.ConfigFromContext(r.Context()).AllUsersGroupID == canRequestHelpToGroupID { - canRequestHelpToPermission = &canRequestHelpTo{ - IsAllUsersGroup: true, - } - } else if store.Groups().IsVisibleFor(canRequestHelpToGroupID, user) { - canRequestHelpToPermission = &canRequestHelpTo{ - ID: canRequestHelpToGroupID, - Name: permissionsRow["granted_directly_can_request_help_to__name"].(string), - } - } + // Filter on "granted" can have a maximum of one match because it is filtered on the primary key. + // (item_id, group_id, origin, source_group_id). + // If there is none, we want to return nil. + var canRequestHelpToPermission *canRequestHelpTo + if len(canRequestHelpToByOrigin[OriginGranted]) > 0 { + canRequestHelpToPermission = &canRequestHelpToByOrigin[OriginGranted][0] } - render.Respond(w, r, &permissionsViewResponse{ + response := permissionsViewResponse{ Granted: grantedPermissionsStruct{ permissionsStruct: permissionsStruct{ ItemPermissions: structures.ItemPermissions{ @@ -343,7 +344,7 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi CanEnterUntil: service.ConvertDBTimeToJSONTime(permissionsRow["granted_directly_can_enter_until"]), CanRequestHelpTo: canRequestHelpToPermission, }, - Computed: computedPermissions{permissionsWithCanEnterFrom{ + Computed: computedPermissions{aggregatedPermissionsWithCanEnterFromStruct{ permissionsStruct: permissionsStruct{ ItemPermissions: structures.ItemPermissions{ CanView: permissionsGrantedStore.ViewNameByIndex(int(permissionsRow["generated_can_view_value"].(int64))), @@ -354,9 +355,10 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi }, CanMakeSessionOfficial: permissionsRow["generated_can_make_session_official"].(int64) == 1, }, - CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["generated_can_enter_from"]), + CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["generated_can_enter_from"]), + CanRequestHelpTo: canRequestHelpToByOrigin[OriginComputed], }}, - GrantedViaGroupMembership: permissionsGrantedViaGroupMembership{permissionsWithCanEnterFrom{ + GrantedViaGroupMembership: permissionsGrantedViaGroupMembership{aggregatedPermissionsWithCanEnterFromStruct{ permissionsStruct: permissionsStruct{ ItemPermissions: structures.ItemPermissions{ CanView: permissionsGrantedStore.ViewNameByIndex(int(permissionsRow["granted_anc_membership_can_view_value"].(int64))), @@ -367,9 +369,10 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi }, CanMakeSessionOfficial: permissionsRow["granted_anc_membership_can_make_session_official"].(int64) == 1, }, - CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_membership_can_enter_from"]), + CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_membership_can_enter_from"]), + CanRequestHelpTo: canRequestHelpToByOrigin[OriginGroupMembership], }}, - GrantedViaItemUnlocking: permissionsGrantedViaItemUnlocking{permissionsWithCanEnterFrom{ + GrantedViaItemUnlocking: permissionsGrantedViaItemUnlocking{aggregatedPermissionsWithCanEnterFromStruct{ permissionsStruct: permissionsStruct{ ItemPermissions: structures.ItemPermissions{ CanView: permissionsGrantedStore.ViewNameByIndex(int(permissionsRow["granted_anc_unlocking_can_view_value"].(int64))), @@ -380,9 +383,10 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi }, CanMakeSessionOfficial: permissionsRow["granted_anc_unlocking_can_make_session_official"].(int64) == 1, }, - CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_unlocking_can_enter_from"]), + CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_unlocking_can_enter_from"]), + CanRequestHelpTo: canRequestHelpToByOrigin[OriginItemUnlocking], }}, - GrantedViaSelf: permissionsGrantedViaSelf{permissionsWithCanEnterFrom{ + GrantedViaSelf: permissionsGrantedViaSelf{aggregatedPermissionsWithCanEnterFromStruct{ permissionsStruct: permissionsStruct{ ItemPermissions: structures.ItemPermissions{ CanView: permissionsGrantedStore.ViewNameByIndex(int(permissionsRow["granted_anc_self_can_view_value"].(int64))), @@ -393,9 +397,10 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi }, CanMakeSessionOfficial: permissionsRow["granted_anc_self_can_make_session_official"].(int64) == 1, }, - CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_self_can_enter_from"]), + CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_self_can_enter_from"]), + CanRequestHelpTo: canRequestHelpToByOrigin[OriginSelf], }}, - GrantedViaOther: permissionsGrantedViaOther{permissionsWithCanEnterFrom{ + GrantedViaOther: permissionsGrantedViaOther{aggregatedPermissionsWithCanEnterFromStruct{ permissionsStruct: permissionsStruct{ ItemPermissions: structures.ItemPermissions{ CanView: permissionsGrantedStore.ViewNameByIndex(int(permissionsRow["granted_anc_other_can_view_value"].(int64))), @@ -406,8 +411,149 @@ func (srv *Service) getPermissions(w http.ResponseWriter, r *http.Request) servi }, CanMakeSessionOfficial: permissionsRow["granted_anc_other_can_make_session_official"].(int64) == 1, }, - CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_other_can_enter_from"]), + CanEnterFrom: service.ConvertDBTimeToJSONTime(permissionsRow["granted_anc_other_can_enter_from"]), + CanRequestHelpTo: canRequestHelpToByOrigin[OriginOther], }}, - }) + } + + render.Respond(w, r, &response) + return service.NoError } + +// getCanRequestHelpToByOrigin returns a map of canRequestHelpTo permissions by origin. +// We first get all the can_request_help_to groups, and then we filter them by origin. +func getCanRequestHelpToByOrigin( + ancestorPermissions *database.DB, + store *database.DataStore, + groupID int64, + itemID int64, + sourceGroupID int64, + allUsersGroupID int64, + user *database.User, +) map[string][]canRequestHelpTo { + itemAncestorsRequestHelpPropagationQuery := store.Items().GetAncestorsRequestHelpPropagatedQuery(itemID) + + var canRequestHelpToPermissions []canRequestHelpToPermissionsRaw + err := ancestorPermissions. + Joins("JOIN `groups` AS can_request_help_to_group ON can_request_help_to_group.id = permissions_granted.can_request_help_to"). + Select(` + permissions_granted.origin AS origin, + permissions_granted.source_group_id AS source_group_id, + permissions_granted.group_id AS permission_group_id, + permissions_granted.item_id AS permission_item_id, + can_request_help_to_group.id AS group_id, + can_request_help_to_group.name AS group_name + `). + Where("item_id IN (?)", itemAncestorsRequestHelpPropagationQuery.SubQuery()). + Scan(&canRequestHelpToPermissions). + Error() + service.MustNotBeError(err) + + canRequestHelpToByOrigin := make(map[string][]canRequestHelpTo) + for _, origin := range []string{OriginGroupMembership, OriginItemUnlocking, OriginSelf, OriginOther, OriginComputed, OriginGranted} { + canRequestHelpToByOrigin[origin] = filterCanRequestHelpTo( + store, + canRequestHelpToPermissions, + origin, + groupID, + itemID, + sourceGroupID, + user.GroupID, + allUsersGroupID, + ) + } + + return canRequestHelpToByOrigin +} + +// filterCanRequestHelpTo filters the canRequestHelpTo permissions to only keep the ones matching the wanted origin. +func filterCanRequestHelpTo( + store *database.DataStore, + permissions []canRequestHelpToPermissionsRaw, + origin string, + groupID int64, + itemID int64, + sourceGroupID int64, + visibleGroupID int64, + allUsersGroupID int64, +) []canRequestHelpTo { + results := make([]canRequestHelpTo, 0) + + for _, canRequestHelpToPermission := range permissions { + if canRequestHelpToShouldBeAdded(canRequestHelpToPermission, origin, groupID, itemID, sourceGroupID) { + results = append(results, canRequestHelpToForUser(canRequestHelpToPermission, store, visibleGroupID, allUsersGroupID)) + } + } + + return uniqueCanRequestHelpTo(results) +} + +// canRequestHelpToShouldBeAdded checks whether a canRequestHelpToPermission should be added to the results of a given origin. +func canRequestHelpToShouldBeAdded( + canRequestHelpToPermission canRequestHelpToPermissionsRaw, + origin string, + groupID int64, + itemID int64, + sourceGroupID int64, +) bool { + // Permissions granted on ancestor items are only present in "computed". + if origin != OriginComputed && canRequestHelpToPermission.PermissionItemID != itemID { + return false + } + + // The canRequestHelpToPermission matching "group_membership" origin as well as GroupID and SourceGroupID + // is a special case that goes into "granted" and "computed", and not into "group_membership". + if canRequestHelpToPermission.Origin == OriginGroupMembership && + canRequestHelpToPermission.PermissionGroupID == groupID && + canRequestHelpToPermission.SourceGroupID == sourceGroupID { + if origin == OriginGranted || origin == OriginComputed { + return true + } + + return false + } + + if origin == OriginComputed || canRequestHelpToPermission.Origin == origin { + // Otherwise, we want everything in "computed", or everything matching the origin. + return true + } + + return false +} + +// canRequestHelpToForUser converts a canRequestHelpToPermissionsRaw to a canRequestHelpTo returned to the user. +func canRequestHelpToForUser( + permission canRequestHelpToPermissionsRaw, + store *database.DataStore, + visibleGroupID int64, + allUsersGroupID int64, +) canRequestHelpTo { + curCanRequestHelpTo := canRequestHelpTo{ + ID: permission.GroupID, + } + + if allUsersGroupID == permission.GroupID { + curCanRequestHelpTo.IsAllUsersGroup = true + curCanRequestHelpTo.Name = &permission.GroupName + } else if store.Groups().IsVisibleForGroup(permission.GroupID, visibleGroupID) { + curCanRequestHelpTo.Name = &permission.GroupName + } + + return curCanRequestHelpTo +} + +// uniqueCanRequestHelpTo removes duplicates from the canRequestHelpTo slice. +func uniqueCanRequestHelpTo(canRequestHelpTos []canRequestHelpTo) []canRequestHelpTo { + hasID := make(map[int64]bool) + result := make([]canRequestHelpTo, 0) + + for _, entry := range canRequestHelpTos { + if _, value := hasID[entry.ID]; !value { + hasID[entry.ID] = true + result = append(result, entry) + } + } + + return result +} diff --git a/app/api/groups/get_permissions_can_request_help_to.feature b/app/api/groups/get_permissions_can_request_help_to.feature index 5b807c83b..8b3f73386 100644 --- a/app/api/groups/get_permissions_can_request_help_to.feature +++ b/app/api/groups/get_permissions_can_request_help_to.feature @@ -11,14 +11,26 @@ Feature: Get permissions can_request_help_to for a group Background: Given allUsersGroup is defined as the group @AllUsers And there are the following groups: - | group | parent | members | - | @AllUsers | | | - | @School | | @Teacher | - | @Class | | | + | group | parent | members | + | @AllUsers | | | + | @School | | @Teacher | + | @ClassParentParentParent | | | + | @ClassParentParent | @ClassParentParentParent | | + | @ClassParent | @ClassParentParent | | + | @Class | @ClassParent | | + | @OtherSourceGroup | | | And @Teacher is a manager of the group @Class and can grant group access - And there are the following tasks: - | item | - | @Item | + And there are the following items: + | item | type | + | @ChapterParent | Chapter | + | @ChapterParentNoPropagation | Chapter | + | @Chapter | Chapter | + | @Item | Task | + And there are the following item relations: + | item | parent | request_help_propagation | + | @Item | @Chapter | true | + | @Chapter | @ChapterParent | true | + | @Chapter | @ChapterParentNoPropagation | false | And there are the following item permissions: | item | group | is_owner | can_request_help_to | | @Item | @Teacher | true | | @@ -33,11 +45,16 @@ Feature: Get permissions can_request_help_to for a group | @Item | @Class | false | @HelperGroup | When I send a GET request to "/groups/@Class/permissions/@Class/@Item" Then the response code should be 200 - And the response at $.granted.can_request_help_to should be: - | id | name | - | @HelperGroup | Group HelperGroup | + And the response at $.granted.can_request_help_to in JSON should be: + """ + { + "id": "@HelperGroup", + "name": "Group HelperGroup", + "is_all_users_group": false + } + """ - Scenario: Should not return helper group when set and not visible by the current user + Scenario: Should return helper group without the name when set and not visible by the current user Given I am @Teacher And there is a group @HelperGroup And there are the following item permissions: @@ -45,7 +62,13 @@ Feature: Get permissions can_request_help_to for a group | @Item | @Class | false | @HelperGroup | When I send a GET request to "/groups/@Class/permissions/@Class/@Item" Then the response code should be 200 - And the response at $.granted.can_request_help_to should be "null" + And the response at $.granted.can_request_help_to in JSON should be: + """ + { + "id": "@HelperGroup", + "is_all_users_group": false + } + """ Scenario: Should return helper group as "AllUsers" group when set to its value Given I am @Teacher @@ -54,6 +77,135 @@ Feature: Get permissions can_request_help_to for a group | @Item | @Class | false | @AllUsers | When I send a GET request to "/groups/@Class/permissions/@Class/@Item" Then the response code should be 200 - And the response at $.granted.can_request_help_to should be: - | is_all_users_group | - | true | + And the response at $.granted.can_request_help_to in JSON should be: + """ + { + "id": "@AllUsers", + "name": "AllUsers", + "is_all_users_group": true + } + """ + + Scenario: Should return can_request_help_to arrays when permissions with specific origins are defined + Given I am @Teacher + And there are the following item permissions: + | item | group | source_group | origin | is_owner | can_request_help_to | comment | + | @Item | @Class | @Class | self | false | @HelperGroupSelf1 | | + | @Item | @ClassParentParentParent | @Class | self | false | @HelperGroupSelf1 | shouldn't contain duplicate of previous one | + | @Item | @ClassParent | @Class | self | false | @AllUsers | | + | @Item | @ClassParentParent | @Class | self | false | | check we don't get empty groups | + | @Item | @ClassParentParent | @OtherSourceGroup | self | false | @HelperOtherSourceGroup1 | other source group | + | @Item | @Class | @Class | group_membership | false | @HelperGroupGroupMembership1 | in granted and computed, but not group_membership | + | @Item | @ClassParent | @Class | group_membership | false | @HelperGroupGroupMembership2 | group_membership but not granted | + | @Item | @ClassParentParent | @Class | group_membership | false | @AllUsers | group_membership but not granted | + | @Item | @ClassParentParent | @OtherSourceGroup | group_membership | false | @HelperOtherSourceGroup2 | other source group | + | @Item | @ClassParent | @Class | item_unlocking | false | @HelperGroupItemUnlocking | | + | @Item | @Class | @Class | item_unlocking | false | @AllUsers | | + | @Item | @ClassParentParent | @Class | item_unlocking | false | @HelperGroupNotVisible | not visible | + | @Item | @ClassParentParent | @OtherSourceGroup | item_unlocking | false | @HelperOtherSourceGroup3 | other source group | + | @Item | @Class | @Class | other | false | @AllUsers | | + | @Item | @ClassParentParent | @Class | other | false | @HelperGroupOther1 | | + | @Item | @ClassParent | @Class | other | false | @HelperGroupOther2 | | + | @Item | @ClassParent | @OtherSourceGroup | other | false | @HelperOtherSourceGroup4 | other source group | + | @Item | @ClassParentParent | @OtherSourceGroup | other | false | @HelperOtherSourceGroupNotVisible | other source group, not visible | + # The following lines are to make the groups visible by @Teacher + And the group @Teacher is a descendant of the group @HelperGroupSelf1 + And the group @Teacher is a descendant of the group @HelperGroupGroupMembership1 + And the group @Teacher is a descendant of the group @HelperGroupGroupMembership2 + And the group @Teacher is a descendant of the group @HelperGroupItemUnlocking + And the group @Teacher is a descendant of the group @HelperGroupOther1 + And the group @Teacher is a descendant of the group @HelperGroupOther2 + And the group @Teacher is a descendant of the group @HelperOtherSourceGroup1 + And the group @Teacher is a descendant of the group @HelperOtherSourceGroup2 + And the group @Teacher is a descendant of the group @HelperOtherSourceGroup3 + And the group @Teacher is a descendant of the group @HelperOtherSourceGroup4 + When I send a GET request to "/groups/@Class/permissions/@Class/@Item" + Then the response code should be 200 + And the response at $.granted_via_self.can_request_help_to[*] should be: + | id | name | is_all_users_group | + | @AllUsers | AllUsers | true | + | @HelperGroupSelf1 | Group HelperGroupSelf1 | false | + | @HelperOtherSourceGroup1 | Group HelperOtherSourceGroup1 | false | + And the response at $.granted.can_request_help_to in JSON should be: + """ + { + "id": "@HelperGroupGroupMembership1", + "name": "Group HelperGroupGroupMembership1", + "is_all_users_group": false + } + """ + And the response at $.granted_via_group_membership.can_request_help_to[*] should be: + | id | name | is_all_users_group | + | @HelperGroupGroupMembership2 | Group HelperGroupGroupMembership2 | false | + | @AllUsers | AllUsers | true | + | @HelperOtherSourceGroup2 | Group HelperOtherSourceGroup2 | false | + And the response at $.granted_via_item_unlocking.can_request_help_to[*] should be: + | id | name | is_all_users_group | + | @HelperGroupItemUnlocking | Group HelperGroupItemUnlocking | false | + | @AllUsers | AllUsers | true | + | @HelperGroupNotVisible | | false | + | @HelperOtherSourceGroup3 | Group HelperOtherSourceGroup3 | false | + And the response at $.granted_via_other.can_request_help_to[*] should be: + | id | name | is_all_users_group | + | @AllUsers | AllUsers | true | + | @HelperGroupOther1 | Group HelperGroupOther1 | false | + | @HelperGroupOther2 | Group HelperGroupOther2 | false | + | @HelperOtherSourceGroup4 | Group HelperOtherSourceGroup4 | false | + | @HelperOtherSourceGroupNotVisible | | false | + And the response at $.computed.can_request_help_to[*] should be: + | id | name | is_all_users_group | + | @AllUsers | AllUsers | true | + | @HelperGroupSelf1 | Group HelperGroupSelf1 | false | + | @HelperGroupGroupMembership1 | Group HelperGroupGroupMembership1 | false | + | @HelperGroupGroupMembership2 | Group HelperGroupGroupMembership2 | false | + | @HelperGroupItemUnlocking | Group HelperGroupItemUnlocking | false | + | @HelperGroupNotVisible | | false | + | @HelperGroupOther1 | Group HelperGroupOther1 | false | + | @HelperGroupOther2 | Group HelperGroupOther2 | false | + | @HelperOtherSourceGroup1 | Group HelperOtherSourceGroup1 | false | + | @HelperOtherSourceGroup2 | Group HelperOtherSourceGroup2 | false | + | @HelperOtherSourceGroup3 | Group HelperOtherSourceGroup3 | false | + | @HelperOtherSourceGroup4 | Group HelperOtherSourceGroup4 | false | + | @HelperOtherSourceGroupNotVisible | | false | + + Scenario: Should return can_request_help_to from parent items only into computed and only when it propagates + Given I am @Teacher + And there are the following item permissions: + | item | group | source_group | origin | is_owner | can_request_help_to | comment | + | @Chapter | @Class | @Class | self | false | @HelperGroupSelf1 | | + | @ChapterParent | @ClassParentParentParent | @Class | self | false | @HelperGroupSelfNotVisible | without name | + | @ChapterParentNoPropagation | @ClassParent | @Class | self | false | @HelperGroupSelfNoPropagation | shouldn't appear | + | @Chapter | @Class | @Class | group_membership | false | @HelperGroupGroupMembership1 | | + | @ChapterParent | @ClassParent | @Class | group_membership | false | @AllUsers | | + | @ChapterParentNoPropagation | @ClassParentParent | @Class | group_membership | false | @HelperGroupGroupMembershipNoPropagation | shouldn't appear | + | @Chapter | @ClassParent | @Class | item_unlocking | false | @HelperGroupItemUnlocking1 | | + | @ChapterParent | @Class | @Class | item_unlocking | false | @HelperGroupItemUnlocking2 | | + | @ChapterParentNoPropagation | @ClassParentParent | @Class | item_unlocking | false | @HelperGroupItemUnlockingNoPropagation | shouldn't appear | + | @Chapter | @Class | @Class | other | false | @HelperGroupOther1 | | + | @ChapterParent | @ClassParentParent | @Class | other | false | @HelperGroupOther2 | | + | @ChapterParentNoPropagation | @ClassParent | @OtherSourceGroup | other | false | @HelperGroupOtherNoPropagation | other source group | + # The following lines are to make the groups visible by @Teacher + And the group @Teacher is a descendant of the group @HelperGroupSelf1 + And the group @Teacher is a descendant of the group @HelperGroupGroupMembership1 + And the group @Teacher is a descendant of the group @HelperGroupGroupMembership2 + And the group @Teacher is a descendant of the group @HelperGroupItemUnlocking1 + And the group @Teacher is a descendant of the group @HelperGroupItemUnlocking2 + And the group @Teacher is a descendant of the group @HelperGroupOther1 + And the group @Teacher is a descendant of the group @HelperGroupOther2 + When I send a GET request to "/groups/@Class/permissions/@Class/@Item" + Then the response code should be 200 + And the response at $.granted_via_self.can_request_help_to should be "[]" + And the response at $.granted.can_request_help_to should be "" + And the response at $.granted_via_group_membership.can_request_help_to should be "[]" + And the response at $.granted_via_item_unlocking.can_request_help_to should be "[]" + And the response at $.granted_via_other.can_request_help_to should be "[]" + And the response at $.computed.can_request_help_to[*] should be: + | id | name | is_all_users_group | + | @HelperGroupSelf1 | Group HelperGroupSelf1 | false | + | @HelperGroupSelfNotVisible | | false | + | @HelperGroupGroupMembership1 | Group HelperGroupGroupMembership1 | false | + | @AllUsers | AllUsers | true | + | @HelperGroupItemUnlocking1 | Group HelperGroupItemUnlocking1 | false | + | @HelperGroupItemUnlocking2 | Group HelperGroupItemUnlocking2 | false | + | @HelperGroupOther1 | Group HelperGroupOther1 | false | + | @HelperGroupOther2 | Group HelperGroupOther2 | false | diff --git a/app/api/threads/list_threads.feature b/app/api/threads/list_threads.feature index 2ef0aa114..d4d478019 100644 --- a/app/api/threads/list_threads.feature +++ b/app/api/threads/list_threads.feature @@ -257,7 +257,7 @@ Feature: List threads | -latest_update_at | 2 | 2 | 0 | @TaskMaxUpdateAt | | -latest_update_at | 2 | 2 | 1 | @TaskSecondMaxUpdateAt | - Scenario Outline: Should support pagination parameters + Scenario: Should support pagination parameters with results Given I am @John And there are the following items: | item | type | @@ -268,14 +268,25 @@ Feature: List threads | @John | @TaskMinUpdateAt | 1 | 2023-01-01 00:00:01 | | @John | @TaskMaxUpdateAt | 1 | 2023-01-01 00:00:02 | And I am @John - When I send a GET request to "/threads?is_mine=1&limit=1&sort=latest_update_at&from.item_id=&from.participant_id=" + When I send a GET request to "/threads?is_mine=1&limit=1&sort=latest_update_at&from.item_id=@TaskMinUpdateAt&from.participant_id=@John" Then the response code should be 200 - And the response should be a JSON array with entries - And the response at $[0].item.id should be "" - Examples: - | from.item_id | from.participant_id | nb_results | result_item | - | @TaskMinUpdateAt | @John | 1 | @TaskMaxUpdateAt | - | @TaskMaxUpdateAt | @John | 0 | | + And the response should be a JSON array with 1 entries + And the response at $[0].item.id should be "@TaskMaxUpdateAt" + + Scenario: Should support pagination parameters with no results + Given I am @John + And there are the following items: + | item | type | + | @TaskMinUpdateAt | Task | + | @TaskMaxUpdateAt | Task | + And there are the following threads: + | participant | item | visible_by_participant | latest_update_at | + | @John | @TaskMinUpdateAt | 1 | 2023-01-01 00:00:01 | + | @John | @TaskMaxUpdateAt | 1 | 2023-01-01 00:00:02 | + And I am @John + When I send a GET request to "/threads?is_mine=1&limit=1&sort=latest_update_at&from.item_id=@TaskMaxUpdateAt&from.participant_id=@John" + Then the response code should be 200 + And the response should be a JSON array with 0 entries Scenario Outline: Should filter by status if parameter status is given Given I am @John @@ -320,4 +331,20 @@ Feature: List threads | latest_update_gt | first_result_item | nb_results | | 2023-01-01T00:00:00Z | @Task1 | 3 | | 2023-01-01T00:00:02Z | @Task3 | 1 | - | 2023-01-01T00:00:03Z | | 0 | + + Scenario: Should return no results when latest_update_gt is given but no entries are greater than latest_update_gt + Given I am @John + And there are the following items: + | item | type | + | @Task1 | Task | + | @Task2 | Task | + | @Task3 | Task | + And there are the following threads: + | participant | item | visible_by_participant | latest_update_at | + | @John | @Task1 | 1 | 2023-01-01 00:00:01 | + | @John | @Task2 | 1 | 2023-01-01 00:00:02 | + | @John | @Task3 | 1 | 2023-01-01 00:00:03 | + And I am @John + When I send a GET request to "/threads?is_mine=1&latest_update_gt=2023-01-01T00:00:03Z&sort=latest_update_at" + Then the response code should be 200 + And the response should be a JSON array with 0 entries diff --git a/app/database/item_store.go b/app/database/item_store.go index 3282903dc..6bbb76565 100644 --- a/app/database/item_store.go +++ b/app/database/item_store.go @@ -322,8 +322,8 @@ func (s *ItemStore) DeleteItem(itemID int64) (err error) { }) } -// getAncestorsRequestHelpPropagationQuery gets all ancestors of an itemID while request_help_propagation = 1. -func (s *ItemStore) getAncestorsRequestHelpPropagationQuery(itemID int64) *DB { +// GetAncestorsRequestHelpPropagatedQuery gets all ancestors of an itemID while request_help_propagation = 1. +func (s *ItemStore) GetAncestorsRequestHelpPropagatedQuery(itemID int64) *DB { return s.Raw(` WITH RECURSIVE items_ancestors_request_help_propagation(item_id) AS ( diff --git a/app/database/user.go b/app/database/user.go index c9700c78b..e20dd81bb 100644 --- a/app/database/user.go +++ b/app/database/user.go @@ -69,7 +69,7 @@ func (u *User) CanRequestHelpTo(s *DataStore, itemID, helperGroupID int64) bool // one of the ancestors (including himself) of User has the can_request_help_to(Group) on Item, // recursively on Item’s ancestors while request_help_propagation=1, for each Group being a descendant of Group. - itemAncestorsRequestHelpPropagationQuery := s.Items().getAncestorsRequestHelpPropagationQuery(itemID) + itemAncestorsRequestHelpPropagationQuery := s.Items().GetAncestorsRequestHelpPropagatedQuery(itemID) canRequestHelpTo, err := s.Users(). Joins("JOIN groups_ancestors_active ON groups_ancestors_active.child_group_id = ?", u.GroupID). diff --git a/testhelpers/feature_context.go b/testhelpers/feature_context.go index 608339bfe..3c59f3942 100644 --- a/testhelpers/feature_context.go +++ b/testhelpers/feature_context.go @@ -52,6 +52,7 @@ func FeatureContext(s *godog.Suite) { s.Step(`^there are the following items:$`, ctx.ThereAreTheFollowingItems) s.Step(`^there are the following tasks:$`, ctx.ThereAreTheFollowingTasks) s.Step(`^there are the following item permissions:$`, ctx.ThereAreTheFollowingItemPermissions) + s.Step(`^there are the following item relations:$`, ctx.ThereAreTheFollowingItemRelations) s.Step(`^I can watch the group (@\w+)$`, ctx.ICanWatchGroup) s.Step(`^I can watch the participant with id "([^"]*)"$`, ctx.ICanWatchGroupWithID) s.Step(`^I can view (none|info|content|content_with_descendants|solution) on item with id "([^"]*)"$`, @@ -83,7 +84,7 @@ func FeatureContext(s *godog.Suite) { s.Step(`^the response should be a JSON array with (\d+) entr(ies|y)$`, ctx.ItShouldBeAJSONArrayWithEntries) s.Step(`^the response at ([^ ]+) should be "([^"]*)"$`, ctx.TheResponseAtShouldBeTheValue) s.Step("^the response at ([^ ]+) should be:$", ctx.TheResponseAtShouldBe) - s.Step("^the response should not be defined at ([^ ]+)$", ctx.TheResponseShouldNotBeDefinedAt) + s.Step("^the response at ([^ ]+) in JSON should be:$", ctx.TheResponseAtInJSONShouldBe) s.Step(`^the table "([^"]*)" should be:$`, ctx.TableShouldBe) s.Step(`^the table "([^"]*)" should be empty$`, ctx.TableShouldBeEmpty) diff --git a/testhelpers/steps_app_language.go b/testhelpers/steps_app_language.go index 8f889c84d..4894e44df 100644 --- a/testhelpers/steps_app_language.go +++ b/testhelpers/steps_app_language.go @@ -20,7 +20,10 @@ const ( strTrue = "true" ) -var itemPermissionKeys = []string{"can_view", "can_grant_view", "can_watch", "can_edit", "is_owner", "can_request_help_to"} +var ( + itemPermissionKeys = []string{"can_view", "can_grant_view", "can_watch", "can_edit", "is_owner", "can_request_help_to"} + itemPropagationKeys = []string{"grant_view_propagation", "watch_propagation", "edit_propagation", "request_help_propagation"} +) // ctx.getParameterMap parses parameters in format key1=val1,key2=val2,... into a map. func (ctx *TestContext) getParameterMap(parameters string) map[string]string { @@ -257,23 +260,34 @@ func (ctx *TestContext) addGroupManager(manager, group, canWatchMembers, canGran } // addPermissionsGranted adds a permission granted in the database. -func (ctx *TestContext) addPermissionGranted(group, item, permission, permissionValue string) { +func (ctx *TestContext) addPermissionGranted(group, item, sourceGroup, origin, permission, permissionValue string) { groupID := ctx.getReference(group) + sourceGroupID := ctx.getReference(sourceGroup) itemID := ctx.getReference(item) permissionsGrantedTable := "permissions_granted" - key := strconv.FormatInt(groupID, 10) + "," + strconv.FormatInt(itemID, 10) + key := strconv.FormatInt(groupID, 10) + "," + + strconv.FormatInt(itemID, 10) + "," + + strconv.FormatInt(sourceGroupID, 10) + "," + + origin if !ctx.isInDatabase(permissionsGrantedTable, key) { ctx.addInDatabase(permissionsGrantedTable, key, map[string]interface{}{ "group_id": groupID, - "source_group_id": groupID, + "source_group_id": sourceGroupID, "item_id": itemID, + "origin": origin, }) } if permission == "can_request_help_to" { - permissionValue = strconv.FormatInt(ctx.getReference(permissionValue), 10) + canRequestHelpToGroupID := strconv.FormatInt(ctx.getReference(permissionValue), 10) + + if !ctx.isInDatabase("groups", canRequestHelpToGroupID) { + ctx.addGroup(permissionValue, "Group "+referenceToName(permissionValue), "Class") + } + + permissionValue = canRequestHelpToGroupID } if permission == "is_owner" { @@ -320,6 +334,10 @@ func (ctx *TestContext) addResult(attemptID, participant, item string, validated ) } +func (ctx *TestContext) getItemItemKey(parentItemID, childItemID int64) string { + return strconv.FormatInt(parentItemID, 10) + "," + strconv.FormatInt(childItemID, 10) +} + // addItemItem adds an item-item in the database. func (ctx *TestContext) addItemItem(parentItem, childItem string) { parentItemID := ctx.getReference(parentItem) @@ -327,7 +345,7 @@ func (ctx *TestContext) addItemItem(parentItem, childItem string) { ctx.addInDatabase( "items_items", - strconv.FormatInt(parentItemID, 10)+","+strconv.FormatInt(childItemID, 10), + ctx.getItemItemKey(parentItemID, childItemID), map[string]interface{}{ "parent_item_id": parentItemID, "child_item_id": childItemID, @@ -336,6 +354,12 @@ func (ctx *TestContext) addItemItem(parentItem, childItem string) { ) } +func (ctx *TestContext) addItemItemPropagation(parent, child, propagation, propagationValue string) { + key := ctx.getItemItemKey(ctx.getReference(parent), ctx.getReference(child)) + + ctx.dbTables["items_items"][key][propagation] = propagationValue +} + // addItem adds an item in the database. func (ctx *TestContext) addItem(fields map[string]string) { dbFields := make(map[string]interface{}) @@ -693,9 +717,26 @@ func (ctx *TestContext) ThereAreTheFollowingItemPermissions(itemPermissions *mes } func (ctx *TestContext) applyUserPermissionsOnItem(itemPermission map[string]string) error { + sourceGroup := itemPermission["group"] + if definedSourceGroup, ok := itemPermission["source_group"]; ok { + sourceGroup = definedSourceGroup + } + + origin := "group_membership" + if definedOrigin, ok := itemPermission["origin"]; ok { + origin = definedOrigin + } + for _, permissionKey := range itemPermissionKeys { if permissionValue, ok := itemPermission[permissionKey]; ok { - err := ctx.UserSetPermissionOnItemWithID(permissionKey, permissionValue, itemPermission["group"], itemPermission["item"]) + err := ctx.UserSetPermissionExtendedOnItemWithID( + permissionKey, + permissionValue, + itemPermission["group"], + itemPermission["item"], + sourceGroup, + origin, + ) if err != nil { return err } @@ -705,6 +746,48 @@ func (ctx *TestContext) applyUserPermissionsOnItem(itemPermission map[string]str return nil } +// ThereAreTheFollowingItemRelations defines item relations, in items_items table. +func (ctx *TestContext) ThereAreTheFollowingItemRelations(itemPermissions *messages.PickleStepArgument_PickleTable) error { + for i := 1; i < len(itemPermissions.Rows); i++ { + itemRelation := ctx.getRowMap(i, itemPermissions) + + err := ctx.applyItemRelation(itemRelation) + if err != nil { + return err + } + } + + return nil +} + +func (ctx *TestContext) applyItemRelation(itemRelation map[string]string) error { + ctx.addItemItem(itemRelation["parent"], itemRelation["item"]) + + for _, propagationKey := range itemPropagationKeys { + if propagationValue, ok := itemRelation[propagationKey]; ok { + boolValue, err := strconv.ParseBool(propagationValue) + if err != nil { + panic(fmt.Sprintf("applyItemRelation: %v cannot be parsed as a boolean", boolValue)) + } + + if boolValue { + propagationValue = "1" + } else { + propagationValue = "0" + } + + ctx.addItemItemPropagation( + itemRelation["parent"], + itemRelation["item"], + propagationKey, + propagationValue, + ) + } + } + + return nil +} + // ICanWatchGroup adds the permission for the user to watch a group. func (ctx *TestContext) ICanWatchGroup(groupName string) error { return ctx.UserIsAManagerOfTheGroupWith(getParameterString(map[string]string{ @@ -816,9 +899,23 @@ func (ctx *TestContext) IAmAMemberOfTheGroup(name string) error { return ctx.IAmAMemberOfTheGroupWithID(name) } +// ItemRelationSetPropagation adds a propagation on an item relation. +func (ctx *TestContext) ItemRelationSetPropagation(propagation, value, parent, item string) error { + ctx.addItemItemPropagation(parent, item, propagation, value) + + return nil +} + +// UserSetPermissionExtendedOnItemWithID gives a user a permission on an item with a specific source_group and origin. +func (ctx *TestContext) UserSetPermissionExtendedOnItemWithID(permission, value, user, item, sourceGroup, origin string) error { + ctx.addPermissionGranted(user, item, sourceGroup, origin, permission, value) + + return nil +} + // UserSetPermissionOnItemWithID gives a user a permission on an item. func (ctx *TestContext) UserSetPermissionOnItemWithID(permission, value, user, item string) error { - ctx.addPermissionGranted(user, item, permission, value) + ctx.addPermissionGranted(user, item, user, "group_membership", permission, value) return nil } diff --git a/testhelpers/steps_response.go b/testhelpers/steps_response.go index e8f9907e7..4638a1770 100644 --- a/testhelpers/steps_response.go +++ b/testhelpers/steps_response.go @@ -43,24 +43,19 @@ func (ctx *TestContext) getJSONPathOnResponse(jsonPath string) (interface{}, err return nil, fmt.Errorf("getJSONPathOnResponse: Unmarshal response: %v", err) } - jsonPathRes, err := jsonpath.Get(jsonPath, JSONResponse) - if err != nil { - return nil, fmt.Errorf("getJSONPathOnResponse: Cannot get JsonPath: %v", err) - } - - return jsonPathRes, nil + return jsonpath.Get(jsonPath, JSONResponse) } // TheResponseAtShouldBeTheValue checks that the response at a JSONPath is a certain value. func (ctx *TestContext) TheResponseAtShouldBeTheValue(jsonPath, value string) error { jsonPathRes, err := ctx.getJSONPathOnResponse(jsonPath) if err != nil { - // When an empty value is provided, not finding the jsonPath because it doesn't exist is a success. - if value == "" { + // The JSONPath is not defined. + if value == undefinedValue { return nil } - return err + return fmt.Errorf("TheResponseAtShouldBeTheValue: JSONPath %v doesn't match value %v: %v", jsonPath, value, err) } value = ctx.replaceReferencesByIDs(value) @@ -87,20 +82,24 @@ func jsonPathResultMatchesValue(jsonPathRes interface{}, value string) bool { case float64: expected, _ = strconv.ParseFloat(value, 64) case []interface{}: - // When the result is an empty array, matches if we're looking for an empty value. - if len(jsonPathResultTyped) == 0 && value == "" { + // When the result is an empty array, matches if we're looking for "[]". + if len(jsonPathResultTyped) == 0 && value == "[]" { return true } case interface{}: } - if jsonPathRes == nil && value == "null" { + if jsonPathRes == nil && jsonPathValueConsideredNil(value) { return true } return jsonPathRes == expected } +func jsonPathValueConsideredNil(value string) bool { + return value == nullValue +} + // TheResponseAtShouldBe checks that the response at a JSONPath matches multiple values. func (ctx *TestContext) TheResponseAtShouldBe(jsonPath string, wants *messages.PickleStepArgument_PickleTable) error { jsonPathRes, err := ctx.getJSONPathOnResponse(jsonPath) @@ -132,12 +131,55 @@ func (ctx *TestContext) TheResponseAtShouldBe(jsonPath string, wants *messages.P } return ctx.wantValuesMatchesJSONPathResultArr(wants, typedJSONRes) - case map[string]interface{}: - // The result is an object (eg. "element": {"a": 0, "b": 0}) - return ctx.wantValuesMatchesJSONPathResultObject(wants, typedJSONRes) + default: + if typedJSONRes == nil { + return fmt.Errorf("TheResponseAtShouldBe: The JsonPath result at the path %v is %v", jsonPath, typedJSONRes) + } + } + + panic(fmt.Sprintf("TheResponseAtShouldBe: Result found at JSON Path %v should be an array but is: %v", jsonPath, jsonPathRes)) +} + +// TheResponseAtInJSONShouldBe checks that the response in JSON at a JSONPath matches. +func (ctx *TestContext) TheResponseAtInJSONShouldBe(jsonPath string, wants *messages.PickleStepArgument_PickleDocString) error { + jsonPathRes, err := ctx.getJSONPathOnResponse(jsonPath) + if err != nil { + return err + } + + actual, err := json.MarshalIndent(&jsonPathRes, "", "\t") + if err != nil { + return err } - panic(fmt.Sprintf("TheResponseAtShouldBe: Unhandled case for %v", jsonPathRes)) + preprocessedWants, err := ctx.preprocessString(wants.Content) + if err != nil { + return err + } + + expected, err := indentJSON(preprocessedWants) + if err != nil { + return err + } + + return compareStrings(string(expected), string(actual)) +} + +// indentJSON indents the JSON string. +// Works by re-encoding the JSON string with indentation. +func indentJSON(preprocessedWants string) ([]byte, error) { + var exp interface{} + err := json.Unmarshal([]byte(preprocessedWants), &exp) + if err != nil { + return nil, err + } + + var expected []byte + if expected, err = json.MarshalIndent(&exp, "", "\t"); err != nil { + return nil, err + } + + return expected, nil } func (ctx *TestContext) wantRowsMatchesJSONPathResultArr( @@ -230,51 +272,19 @@ func (ctx *TestContext) wantValuesMatchesJSONPathResultArr( return nil } -func (ctx *TestContext) wantValuesMatchesJSONPathResultObject( - wants *messages.PickleStepArgument_PickleTable, - jsonPathResObject map[string]interface{}, -) error { - headerCells := wants.Rows[0].Cells - objectCells := wants.Rows[1].Cells - - for i := 0; i < len(headerCells); i++ { - key := headerCells[i].Value - wantedValue := ctx.replaceReferencesByIDs(objectCells[i].Value) - actualValue := jsonPathResObject[key] - - if !jsonPathResultMatchesValue(actualValue, wantedValue) { - return fmt.Errorf("wantValuesMatchesJSONPathResultObject: [%v] should be %v but is %v", key, wantedValue, actualValue) - } - } - - return nil -} - func stringifyJSONPathResultValue(value interface{}) string { switch typedValue := value.(type) { case bool: // Convert boolean results to strings because the values we check are coming from Gherkin as strings. return strconv.FormatBool(typedValue) default: - return typedValue.(string) - } -} - -// TheResponseShouldNotBeDefinedAt checks that the provided jsonPath doesn't exist. -func (ctx *TestContext) TheResponseShouldNotBeDefinedAt(jsonPath string) error { - var JSONResponse interface{} - err := json.Unmarshal([]byte(ctx.lastResponseBody), &JSONResponse) - if err != nil { - return fmt.Errorf("TheResponseShouldNotBeDefinedAt: Unmarshal response: %v", err) - } + // The value is nil when the JSONPath is not defined. + if value == nil { + return undefinedValue + } - jsonPathRes, err := jsonpath.Get(jsonPath, JSONResponse) - if err != nil { - //nolint:nilerr // We want jsonpath.Get to return an error. - return nil + return typedValue.(string) } - - return fmt.Errorf("TheResponseShouldNotBeDefinedAt: JsonPath: %v is defined with value %v", jsonPath, jsonPathRes) } func (ctx *TestContext) TheResponseCodeShouldBe(code int) error { //nolint @@ -299,16 +309,10 @@ func (ctx *TestContext) TheResponseDecodedBodyShouldBeJSON(responseType string, return err } - // re-encode expected response - var exp interface{} - err = json.Unmarshal([]byte(expectedBody), &exp) + expected, err := indentJSON(expectedBody) if err != nil { return err } - var expected, actual []byte - if expected, err = json.MarshalIndent(&exp, "", "\t"); err != nil { - return err - } var act interface{} if responseType == "" { @@ -331,7 +335,8 @@ func (ctx *TestContext) TheResponseDecodedBodyShouldBeJSON(responseType string, if responseType != "" { act = payloads.ConvertIntoMap(act) } - if actual, err = json.MarshalIndent(act, "", "\t"); err != nil { + actual, err := json.MarshalIndent(act, "", "\t") + if err != nil { return } @@ -367,7 +372,11 @@ func compareStrings(expected, actual string) error { return nil } -const nullHeaderValue = "[NULL]" +const ( + nullHeaderValue = "[NULL]" + nullValue = "" + undefinedValue = "" +) // TheResponseHeaderShouldBe checks that the response header matches the provided value. func (ctx *TestContext) TheResponseHeaderShouldBe(headerName, headerValue string) (err error) {