From 71beef84981277780c5c5b6f1ddb4c5c34d3f055 Mon Sep 17 00:00:00 2001 From: Yanne Bensafia Date: Tue, 7 Jan 2025 17:42:55 +0100 Subject: [PATCH 1/3] test(config): created config script to setup test workspace --- scripts/release | 13 ++ scripts/setup_test_workspace.py | 267 ++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 scripts/setup_test_workspace.py diff --git a/scripts/release b/scripts/release index a9b17c3d..1fcac3b6 100755 --- a/scripts/release +++ b/scripts/release @@ -19,6 +19,7 @@ ROOT_DIR = Path(__file__).parent.parent CHANGELOG_PATH = ROOT_DIR / "CHANGELOG.md" INIT_PATH = ROOT_DIR / "pygitguardian" / "__init__.py" CASSETTES_DIR = ROOT_DIR / "tests" / "cassettes" +SETUP_WORKSPACE_SCRIPT = ROOT_DIR / "scripts" / "setup_test_workspace.py" # The branch this script must be run from, except in dev mode. RELEASE_BRANCH = "master" @@ -97,6 +98,16 @@ def main(dev_mode: bool) -> int: return 0 +def setup_test_workspace(): + log_progress("Setting up workspace") + try: + check_run( + ["pdm", "run", SETUP_WORKSPACE_SCRIPT], cwd=ROOT_DIR, stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError as exc: + fail(f"There was an error setting up the test workspace :\n{exc.stderr}") + + @main.command() def run_tests() -> None: """Run all tests. @@ -110,6 +121,8 @@ def run_tests() -> None: shutil.rmtree(CASSETTES_DIR) CASSETTES_DIR.mkdir() + setup_test_workspace() + log_progress("Running tests") check_run(["pytest", "tests"], cwd=ROOT_DIR) diff --git a/scripts/setup_test_workspace.py b/scripts/setup_test_workspace.py new file mode 100644 index 00000000..7955e443 --- /dev/null +++ b/scripts/setup_test_workspace.py @@ -0,0 +1,267 @@ +""" +Notice: This script will attempt to setup a test workspace on GitGuardian. +This will allow the user to run tests without relying on cassettes, note that +there are a few limitations due to actions that cannot be performed through +the API, notably : +- Create the workspace +- Create members +- Add deleted members back to the workspace +- Integrate a source +""" + +import os +from typing import Iterable, List, TypeVar + +from pygitguardian.client import GGClient +from pygitguardian.models import ( + AccessLevel, + CreateInvitation, + CreateTeam, + CreateTeamInvitation, + CreateTeamMember, + Detail, + IncidentPermission, + InvitationParameters, + Member, + MembersParameters, + Source, + Team, + TeamMember, + TeamsParameters, + UpdateMember, + UpdateTeamSource, +) +from pygitguardian.models_utils import FromDictWithBase +from tests.utils import CursorPaginatedResponse + + +client = GGClient( + api_key=os.environ["GITGUARDIAN_API_KEY"], + base_uri=os.environ.get("GITGUARDIAN_API_URL"), +) + +T = TypeVar("T") +PaginatedDataType = TypeVar("PaginatedDataType", bound=FromDictWithBase) + +MIN_NB_TEAM = 2 +MIN_NB_MEMBER = 3 # 1 owner, 1 manager and at least one member +MIN_NB_TEAM_MEMBER = 2 +# This is the team that is created in the tests, it should be deleted before we run the tests +PYGITGUARDIAN_TEST_TEAM = "PyGitGuardian team" + + +def ensure_not_detail(var: T | Detail) -> T: + if not isinstance(var, Detail): + return var + else: + raise TypeError(var.detail) + + +def unwrap_paginated_response( + var: CursorPaginatedResponse[PaginatedDataType] | Detail, +) -> List[PaginatedDataType]: + data = ensure_not_detail(var) + + return data.data + + +def ensure_member_coherence(): + deactivated_members = unwrap_paginated_response( + client.list_members(MembersParameters(active=False)) + ) + for member in deactivated_members: + client.update_member(UpdateMember(member.id, AccessLevel.MEMBER, active=True)) + + admin_members = unwrap_paginated_response( + client.list_members(MembersParameters(access_level=AccessLevel.MANAGER)) + ) + + if len(admin_members) > 1: + for member in admin_members[1:]: + ensure_not_detail( + client.update_member(UpdateMember(member.id, AccessLevel.MEMBER)) + ) + else: + members = unwrap_paginated_response( + client.list_members(MembersParameters(access_level=AccessLevel.MEMBER)) + ) + assert ( + len(members) > 0 + ), "There must be at least one member with access level member in the workspace" + + ensure_not_detail( + client.update_member(UpdateMember(members[0].id, AccessLevel.MANAGER)) + ) + + members = ensure_not_detail(client.list_members(MembersParameters(per_page=5))) + + assert len(members.data) > 3, "There must be at least 3 members in the workspace" + + +def add_source_to_team(team: Team, available_sources: Iterable[Source] | None = None): + if available_sources is None: + available_sources = ensure_not_detail(client.list_sources()).data + + ensure_not_detail( + client.update_team_source( + UpdateTeamSource(team.id, [source.id for source in available_sources], []) + ) + ) + + +def add_team_members( + team: Team, + team_members: Iterable[TeamMember], + nb_members: int, + available_members: Iterable[Member] | None = None, +): + assert nb_members > 0, "We should add at least one member" + if available_members is None: + available_members = unwrap_paginated_response(client.list_members()) + + # Every manager is by default a team leader + has_admin = any(team_member.is_team_leader for team_member in team_members) + + if not has_admin: + admin_member = next( + ( + member + for member in available_members + if member.access_level == AccessLevel.MANAGER + ), + None, + ) + assert admin_member is not None, "There should be at least one admin member" + + ensure_not_detail( + client.create_team_member( + team.id, + CreateTeamMember( + admin_member.id, + is_team_leader=True, + incident_permission=IncidentPermission.FULL_ACCESS, + ), + ) + ) + nb_members -= 1 + + team_member_ids = {team_member.member_id for team_member in team_members} + for _ in range(nb_members): + to_add_member = next( + ( + member + for member in available_members + if member.id not in team_member_ids + and member.access_level not in {AccessLevel.OWNER, AccessLevel.MANAGER} + ), + None, + ) + assert to_add_member is not None, "There is not enough members in the workspace" + is_team_leader = False + permissions = IncidentPermission.FULL_ACCESS + + if to_add_member.access_level == AccessLevel.MANAGER: + is_team_leader = True + + ensure_not_detail( + client.create_team_member( + team.id, + CreateTeamMember( + to_add_member.id, + is_team_leader=is_team_leader, + incident_permission=permissions, + ), + ) + ) + + +def ensure_team_coherence(): + pygitguardian_teams = [] + try: + pygitguardian_teams = unwrap_paginated_response( + client.list_teams(TeamsParameters(search=PYGITGUARDIAN_TEST_TEAM)) + ) + except TypeError as exc: + if str(exc) != "Team not found.": + raise + finally: + for team in pygitguardian_teams: + ensure_not_detail(client.delete_team(team.id)) + + teams = unwrap_paginated_response( + # exclude global team since we can't add sources / members to it + client.list_teams(TeamsParameters(is_global=False)) + ) + + nb_teams = len(teams) + if nb_teams < MIN_NB_TEAM: + for i in range(MIN_NB_TEAM - nb_teams): + new_team = ensure_not_detail( + client.create_team(CreateTeam(name=f"PyGitGuardian Team {i}")) + ) + teams.append(new_team) + + # Ensure every team has: + # - At least one source + # - At least two members, one with admin access and one with member access + for team in teams: + team_members = unwrap_paginated_response(client.list_team_members(team.id)) + nb_team_members = len(team_members) + if nb_team_members < MIN_NB_TEAM_MEMBER: + add_team_members(team, team_members, MIN_NB_TEAM_MEMBER - nb_team_members) + + team_sources = unwrap_paginated_response(client.list_team_sources(team.id)) + nb_team_sources = len(team_sources) + if nb_team_sources == 0: + add_source_to_team(team) + + +def ensure_invitation_coherence(): + test_invitation = unwrap_paginated_response( + client.list_invitations(InvitationParameters(search="pygitguardian")) + ) + + for invitation in test_invitation: + ensure_not_detail(client.delete_invitation(invitation.id)) + invitations = unwrap_paginated_response(client.list_invitations()) + + if len(invitations) < 1: + invitation = ensure_not_detail( + client.create_invitation( + CreateInvitation( + email="pygitguardian@invitation.com", + access_level=AccessLevel.MEMBER, + ) + ) + ) + invitations.append(invitation) + + teams = unwrap_paginated_response(client.list_teams()) + invitation = invitations[0] + for team in teams: + team_invitations = unwrap_paginated_response( + client.list_team_invitations(team.id) + ) + if not team_invitations: + ensure_not_detail( + client.create_team_invitation( + team.id, + CreateTeamInvitation( + invitation_id=invitation.id, + is_team_leader=False, + incident_permission=IncidentPermission.FULL_ACCESS, + ), + ) + ) + + +def main(): + ensure_member_coherence() + ensure_team_coherence() + ensure_invitation_coherence() + + print("Test workspace has been set up properly") + + +if __name__ == "__main__": + main() From c7b5714edda2d0afebfa0c72105195ef9e897ae0 Mon Sep 17 00:00:00 2001 From: Yanne Bensafia Date: Thu, 9 Jan 2025 11:53:02 +0100 Subject: [PATCH 2/3] test(client): loosen the restriction to have one admin member per team by setting permissions to full access --- .../test_create_team_invitation.yaml | 212 +++++++------- tests/cassettes/test_create_team_member.yaml | 268 ++++++++++-------- tests/test_client.py | 17 +- 3 files changed, 273 insertions(+), 224 deletions(-) diff --git a/tests/cassettes/test_create_team_invitation.yaml b/tests/cassettes/test_create_team_invitation.yaml index 8270d3c1..89235fbd 100644 --- a/tests/cassettes/test_create_team_invitation.yaml +++ b/tests/cassettes/test_create_team_invitation.yaml @@ -9,57 +9,64 @@ interactions: Connection: - keep-alive User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) + - pygitguardian/1.19.0 (Darwin;py3.11.8) method: GET uri: https://api.gitguardian.com/v1/teams?cursor=&per_page=20&is_global=False response: body: string: - '[{"id":19,"is_global":false,"name":"This is a test","description":"","gitguardian_url":"http://localhost:3000/workspace/6/settings/user/teams/19"},{"id":20,"is_global":false,"name":"Team - test","description":"","gitguardian_url":"http://localhost:3000/workspace/6/settings/user/teams/20"},{"id":21,"is_global":false,"name":"PyGitGuardian - team","description":"","gitguardian_url":"http://localhost:3000/workspace/6/settings/user/teams/21"}]' + '[{"id":799237,"is_global":false,"name":"PyGitGuardian Team 1","description":"","gitguardian_url":"https://dashboard.gitguardian.com/workspace/628984/settings/user/teams/799237"},{"id":799238,"is_global":false,"name":"PyGitGuardian + team","description":"","gitguardian_url":"https://dashboard.gitguardian.com/workspace/628984/settings/user/teams/799238"}]' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, POST, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '438' - Content-Type: + content-length: + - '353' + content-type: - application/json - Cross-Origin-Opener-Policy: + cross-origin-opener-policy: - same-origin - Date: - - Thu, 12 Dec 2024 16:59:32 GMT - Link: + date: + - Thu, 09 Jan 2025 12:32:16 GMT + link: - '' - Referrer-Policy: - - same-origin - Server: - - nginx/1.24.0 - Vary: + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '30' + x-frame-options: - DENY - X-Per-Page: + - SAMEORIGIN + x-per-page: - '20' - X-Request-ID: - - c2b0a05b57e590cfaf8f9fe2221eda91 - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: code: 200 message: OK - request: - body: null + body: + '{"email": "pygitguardian+create_team_invitation@example.com", "access_level": + "member"}' headers: Accept: - '*/*' @@ -67,55 +74,62 @@ interactions: - gzip, deflate Connection: - keep-alive + Content-Length: + - '87' + Content-Type: + - application/json User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) - method: GET + - pygitguardian/1.19.0 (Darwin;py3.11.8) + method: POST uri: https://api.gitguardian.com/v1/invitations response: body: - string: '[{"id":13,"date":"2024-12-12T16:53:59.247129Z","email":"pygitguardian@example.com","role":"member","access_level":"member"},{"id":14,"date":"2024-12-12T16:54:44.192249Z","email":"example@test.com","role":"member","access_level":"member"}]' + string: '{"id":25184,"date":"2025-01-09T12:32:17.009583Z","email":"pygitguardian+create_team_invitation@example.com","role":"member","access_level":"member"}' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, POST, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '238' - Content-Type: + content-length: + - '148' + content-type: - application/json - Cross-Origin-Opener-Policy: - - same-origin - Date: - - Thu, 12 Dec 2024 16:59:32 GMT - Link: - - '' - Referrer-Policy: + cross-origin-opener-policy: - same-origin - Server: - - nginx/1.24.0 - Vary: + date: + - Thu, 09 Jan 2025 12:32:17 GMT + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '63' + x-frame-options: - DENY - X-Per-Page: - - '20' - X-Request-ID: - - df09bccf6aea67a2215c5dd905f6033e - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + - SAMEORIGIN + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: - code: 200 - message: OK + code: 201 + message: Created - request: - body: '{"invitation_id": 13, "is_team_leader": true, "incident_permission": "can_view"}' + body: '{"invitation_id": 25184, "is_team_leader": true, "incident_permission": + "can_view"}' headers: Accept: - '*/*' @@ -124,49 +138,55 @@ interactions: Connection: - keep-alive Content-Length: - - '80' + - '83' Content-Type: - application/json User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) + - pygitguardian/1.19.0 (Darwin;py3.11.8) method: POST - uri: https://api.gitguardian.com/v1/teams/19/team_invitations + uri: https://api.gitguardian.com/v1/teams/799237/team_invitations response: body: - string: '{"id":7,"team_id":19,"invitation_id":13,"is_team_leader":true,"team_permission":"can_manage","incident_permission":"can_view"}' + string: '{"id":52456,"team_id":799237,"invitation_id":25184,"is_team_leader":true,"team_permission":"can_manage","incident_permission":"can_view"}' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, POST, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '126' - Content-Type: + content-length: + - '137' + content-type: - application/json - Cross-Origin-Opener-Policy: - - same-origin - Date: - - Thu, 12 Dec 2024 16:59:32 GMT - Referrer-Policy: + cross-origin-opener-policy: - same-origin - Server: - - nginx/1.24.0 - Vary: + date: + - Thu, 09 Jan 2025 12:32:17 GMT + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '45' + x-frame-options: - DENY - X-Request-ID: - - 9ce9f089220243eca87b41bc81b72b73 - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + - SAMEORIGIN + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: code: 201 message: Created diff --git a/tests/cassettes/test_create_team_member.yaml b/tests/cassettes/test_create_team_member.yaml index 7b7d16a3..15000c0a 100644 --- a/tests/cassettes/test_create_team_member.yaml +++ b/tests/cassettes/test_create_team_member.yaml @@ -9,52 +9,59 @@ interactions: Connection: - keep-alive User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) + - pygitguardian/1.19.0 (Darwin;py3.11.8) method: GET uri: https://api.gitguardian.com/v1/members response: body: string: - '[{"id":6,"role":"owner","access_level":"owner","email":"toto@gg.com","name":"toto - tata","created_at":"2019-07-15T12:14:14.245000Z","last_login":"2024-12-03T09:29:43.181169Z","active":true},{"id":17,"role":"member","access_level":"member","email":"henri.delateamsecretetducorealerting@gg.com","name":"Henri - De la team secret et du core alerting","created_at":"2024-12-12T17:07:32.636754Z","last_login":null,"active":true}]' + '[{"id":919565,"role":"owner","access_level":"owner","email":"pierre.lalanne@gitguardian.com","name":"Pierre + Lalanne","created_at":"2025-01-06T14:10:44.775142Z","last_login":"2025-01-07T10:59:01.555170Z","active":true},{"id":921540,"role":"member","access_level":"member","email":"yanne.bensafia+member2@gitguardian.com","name":"Yanne + Member 2 Bensafia","created_at":"2025-01-09T10:55:49.787320Z","last_login":"2025-01-09T10:55:49.836530Z","active":true},{"id":921547,"role":"member","access_level":"member","email":"yanne.bensafia@gitguardian.com","name":"Yanne + Bensafia","created_at":"2025-01-09T11:04:35.239421Z","last_login":"2025-01-09T10:55:59.096742Z","active":true}]' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '421' - Content-Type: + content-length: + - '673' + content-type: - application/json - Cross-Origin-Opener-Policy: + cross-origin-opener-policy: - same-origin - Date: - - Thu, 12 Dec 2024 17:08:38 GMT - Link: + date: + - Thu, 09 Jan 2025 12:30:48 GMT + link: - '' - Referrer-Policy: - - same-origin - Server: - - nginx/1.24.0 - Vary: + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '44' + x-frame-options: - DENY - X-Per-Page: + - SAMEORIGIN + x-per-page: - '20' - X-Request-ID: - - cfa0a8bd5e7f0c1617fca33915d7861d - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: code: 200 message: OK @@ -68,52 +75,57 @@ interactions: Connection: - keep-alive User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) + - pygitguardian/1.19.0 (Darwin;py3.11.8) method: GET uri: https://api.gitguardian.com/v1/teams?cursor=&per_page=20&is_global=False response: body: string: - '[{"id":19,"is_global":false,"name":"This is a test","description":"","gitguardian_url":"http://localhost:3000/workspace/6/settings/user/teams/19"},{"id":20,"is_global":false,"name":"Team - test","description":"","gitguardian_url":"http://localhost:3000/workspace/6/settings/user/teams/20"},{"id":21,"is_global":false,"name":"PyGitGuardian - team","description":"","gitguardian_url":"http://localhost:3000/workspace/6/settings/user/teams/21"}]' + '[{"id":799237,"is_global":false,"name":"PyGitGuardian Team 1","description":"","gitguardian_url":"https://dashboard.gitguardian.com/workspace/628984/settings/user/teams/799237"},{"id":799238,"is_global":false,"name":"PyGitGuardian + team","description":"","gitguardian_url":"https://dashboard.gitguardian.com/workspace/628984/settings/user/teams/799238"}]' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, POST, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '438' - Content-Type: + content-length: + - '353' + content-type: - application/json - Cross-Origin-Opener-Policy: + cross-origin-opener-policy: - same-origin - Date: - - Thu, 12 Dec 2024 17:08:38 GMT - Link: + date: + - Thu, 09 Jan 2025 12:30:49 GMT + link: - '' - Referrer-Policy: - - same-origin - Server: - - nginx/1.24.0 - Vary: + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '87' + x-frame-options: - DENY - X-Per-Page: + - SAMEORIGIN + x-per-page: - '20' - X-Request-ID: - - 637b279a05f908b2b5fcfbf2c724113e - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: code: 200 message: OK @@ -127,54 +139,60 @@ interactions: Connection: - keep-alive User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) + - pygitguardian/1.19.0 (Darwin;py3.11.8) method: GET - uri: https://api.gitguardian.com/v1/teams/19/team_memberships + uri: https://api.gitguardian.com/v1/teams/799237/team_memberships response: body: - string: '[{"id":23,"team_id":19,"member_id":6,"is_team_leader":true,"team_permission":"can_manage","incident_permission":"full_access"}]' + string: '[{"id":830609,"team_id":799237,"member_id":919565,"is_team_leader":true,"team_permission":"can_manage","incident_permission":"full_access"},{"id":830610,"team_id":799237,"member_id":921547,"is_team_leader":false,"team_permission":"cannot_manage","incident_permission":"can_view"}]' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, POST, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '127' - Content-Type: + content-length: + - '280' + content-type: - application/json - Cross-Origin-Opener-Policy: + cross-origin-opener-policy: - same-origin - Date: - - Thu, 12 Dec 2024 17:08:38 GMT - Link: + date: + - Thu, 09 Jan 2025 12:30:49 GMT + link: - '' - Referrer-Policy: - - same-origin - Server: - - nginx/1.24.0 - Vary: + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '105' + x-frame-options: - DENY - X-Per-Page: + - SAMEORIGIN + x-per-page: - '20' - X-Request-ID: - - 128a5c3219e7182c88b624aa99dab334 - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: code: 200 message: OK - request: - body: '{"member_id": 17, "is_team_leader": false, "incident_permission": "can_view"}' + body: '{"member_id": 921540, "is_team_leader": false, "incident_permission": "full_access"}' headers: Accept: - '*/*' @@ -183,49 +201,55 @@ interactions: Connection: - keep-alive Content-Length: - - '77' + - '84' Content-Type: - application/json User-Agent: - - pygitguardian/1.18.0 (Darwin;py3.11.8) + - pygitguardian/1.19.0 (Darwin;py3.11.8) method: POST - uri: https://api.gitguardian.com/v1/teams/19/team_memberships + uri: https://api.gitguardian.com/v1/teams/799237/team_memberships response: body: - string: '{"id":28,"team_id":19,"member_id":17,"is_team_leader":false,"team_permission":"cannot_manage","incident_permission":"can_view"}' + string: '{"id":830621,"team_id":799237,"member_id":921540,"is_team_leader":false,"team_permission":"cannot_manage","incident_permission":"full_access"}' headers: - Access-Control-Expose-Headers: + access-control-expose-headers: - X-App-Version - Allow: + allow: - GET, POST, HEAD, OPTIONS - Connection: - - keep-alive - Content-Length: - - '127' - Content-Type: + content-length: + - '142' + content-type: - application/json - Cross-Origin-Opener-Policy: - - same-origin - Date: - - Thu, 12 Dec 2024 17:08:39 GMT - Referrer-Policy: + cross-origin-opener-policy: - same-origin - Server: - - nginx/1.24.0 - Vary: + date: + - Thu, 09 Jan 2025 12:30:50 GMT + referrer-policy: + - strict-origin-when-cross-origin + server: + - istio-envoy + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: - Cookie - X-App-Version: - - dev - X-Content-Type-Options: + x-app-version: + - v2.133.1 + x-content-type-options: + - nosniff - nosniff - X-Frame-Options: + x-envoy-upstream-service-time: + - '193' + x-frame-options: - DENY - X-Request-ID: - - a55f190008b2b60e632a4e5917daec4f - X-SCA-Engine-Version: - - 2.2.0 - X-Secrets-Engine-Version: - - 2.127.0 + - SAMEORIGIN + x-sca-engine-version: + - 2.3.0 + x-sca-last-vuln-fetch: + - '2025-01-09T11:07:18.494922+00:00' + x-secrets-engine-version: + - 2.129.1 + x-xss-protection: + - 1; mode=block status: code: 201 message: Created diff --git a/tests/test_client.py b/tests/test_client.py index 0931be3d..5ef578cc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -67,7 +67,7 @@ ) from .conftest import create_client, my_vcr -from .utils import get_invitation, get_source, get_team +from .utils import get_source, get_team FILENAME = ".env" @@ -1615,7 +1615,13 @@ def test_create_team_invitation(client: GGClient): """ team = get_team() - invitation = get_invitation() + invitation = client.create_invitation( + CreateInvitation( + "pygitguardian+create_team_invitation@example.com", AccessLevel.MEMBER + ) + ) + + assert isinstance(invitation, Invitation), invitation.detail result = client.create_team_invitation( team.id, @@ -1751,13 +1757,12 @@ def test_create_team_member(client: GGClient): result = client.create_team_member( team.id, - CreateTeamMember(member_to_add.id, False, IncidentPermission.VIEW), + CreateTeamMember(member_to_add.id, False, IncidentPermission.FULL_ACCESS), ) assert isinstance(result, TeamMember), result - assert result.incident_permission == IncidentPermission.VIEW - assert not result.is_team_leader + assert result.incident_permission == IncidentPermission.FULL_ACCESS @my_vcr.use_cassette("test_create_team_member_parameters.yaml", ignore_localhost=False) @@ -1788,7 +1793,7 @@ def test_create_team_member_without_mail(client: GGClient): result = client.create_team_member( team.id, - CreateTeamMember(member_to_add.id, False, IncidentPermission.VIEW), + CreateTeamMember(member_to_add.id, False, IncidentPermission.FULL_ACCESS), CreateTeamMemberParameters(send_email=False), ) From 90d92c373ceee603286cbeb560f9a8b0e710b54d Mon Sep 17 00:00:00 2001 From: Yanne Bensafia Date: Mon, 20 Jan 2025 11:33:00 +0100 Subject: [PATCH 3/3] docs(scripts): add docstring on each `ensure_*` to list the requirements being checked --- scripts/setup_test_workspace.py | 68 ++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/scripts/setup_test_workspace.py b/scripts/setup_test_workspace.py index 7955e443..86a3c84d 100644 --- a/scripts/setup_test_workspace.py +++ b/scripts/setup_test_workspace.py @@ -4,9 +4,10 @@ there are a few limitations due to actions that cannot be performed through the API, notably : - Create the workspace -- Create members -- Add deleted members back to the workspace -- Integrate a source +- We cannot create members, so there must exist a minimum amount of members in the workspace + - This also means deleted members cannot be brought back from the script +- We cannot integrate a source entirely from the public API + - There must exist a source in the workspace """ import os @@ -50,7 +51,7 @@ PYGITGUARDIAN_TEST_TEAM = "PyGitGuardian team" -def ensure_not_detail(var: T | Detail) -> T: +def ensure_success(var: T | Detail) -> T: if not isinstance(var, Detail): return var else: @@ -60,12 +61,21 @@ def ensure_not_detail(var: T | Detail) -> T: def unwrap_paginated_response( var: CursorPaginatedResponse[PaginatedDataType] | Detail, ) -> List[PaginatedDataType]: - data = ensure_not_detail(var) + data = ensure_success(var) return data.data def ensure_member_coherence(): + """ + This function ensures that the workspace : + - Has no deactivated members + - If there are, they will be activated + - Has at most 1 admin / manager (excluding owner) + - It may demote some manager to member + - There is at least `MIN_NB_MEMBER` + """ + deactivated_members = unwrap_paginated_response( client.list_members(MembersParameters(active=False)) ) @@ -78,7 +88,7 @@ def ensure_member_coherence(): if len(admin_members) > 1: for member in admin_members[1:]: - ensure_not_detail( + ensure_success( client.update_member(UpdateMember(member.id, AccessLevel.MEMBER)) ) else: @@ -89,20 +99,22 @@ def ensure_member_coherence(): len(members) > 0 ), "There must be at least one member with access level member in the workspace" - ensure_not_detail( + ensure_success( client.update_member(UpdateMember(members[0].id, AccessLevel.MANAGER)) ) - members = ensure_not_detail(client.list_members(MembersParameters(per_page=5))) + members = ensure_success(client.list_members(MembersParameters(per_page=5))) - assert len(members.data) > 3, "There must be at least 3 members in the workspace" + assert ( + len(members.data) > MIN_NB_MEMBER + ), "There must be at least 3 members in the workspace" def add_source_to_team(team: Team, available_sources: Iterable[Source] | None = None): if available_sources is None: - available_sources = ensure_not_detail(client.list_sources()).data + available_sources = ensure_success(client.list_sources()).data - ensure_not_detail( + ensure_success( client.update_team_source( UpdateTeamSource(team.id, [source.id for source in available_sources], []) ) @@ -133,7 +145,7 @@ def add_team_members( ) assert admin_member is not None, "There should be at least one admin member" - ensure_not_detail( + ensure_success( client.create_team_member( team.id, CreateTeamMember( @@ -163,7 +175,7 @@ def add_team_members( if to_add_member.access_level == AccessLevel.MANAGER: is_team_leader = True - ensure_not_detail( + ensure_success( client.create_team_member( team.id, CreateTeamMember( @@ -176,6 +188,17 @@ def add_team_members( def ensure_team_coherence(): + """ + This function ensures that the workspace : + - Has no team with name prefixed by `PYGITGUARDIAN_TEST_TEAM` + - At least `MIN_NB_TEAM` exist + - If not they will be created + - Every team has at least one source + - If possible, it will try to add at least one source + - Every team has at least 2 members, an admin and a member + - If possible, it will try to add those members + """ + pygitguardian_teams = [] try: pygitguardian_teams = unwrap_paginated_response( @@ -186,7 +209,7 @@ def ensure_team_coherence(): raise finally: for team in pygitguardian_teams: - ensure_not_detail(client.delete_team(team.id)) + ensure_success(client.delete_team(team.id)) teams = unwrap_paginated_response( # exclude global team since we can't add sources / members to it @@ -196,7 +219,7 @@ def ensure_team_coherence(): nb_teams = len(teams) if nb_teams < MIN_NB_TEAM: for i in range(MIN_NB_TEAM - nb_teams): - new_team = ensure_not_detail( + new_team = ensure_success( client.create_team(CreateTeam(name=f"PyGitGuardian Team {i}")) ) teams.append(new_team) @@ -217,16 +240,25 @@ def ensure_team_coherence(): def ensure_invitation_coherence(): + """ + This function ensures that the workspace : + - Has no invitation for emails starting with `pygitguardian` + - There is at least one pending invitation + - If not, an invitation will be sent to `pygitguardian@example.com` + - All team have attached team invitations + - If not, they will be created + """ + test_invitation = unwrap_paginated_response( client.list_invitations(InvitationParameters(search="pygitguardian")) ) for invitation in test_invitation: - ensure_not_detail(client.delete_invitation(invitation.id)) + ensure_success(client.delete_invitation(invitation.id)) invitations = unwrap_paginated_response(client.list_invitations()) if len(invitations) < 1: - invitation = ensure_not_detail( + invitation = ensure_success( client.create_invitation( CreateInvitation( email="pygitguardian@invitation.com", @@ -243,7 +275,7 @@ def ensure_invitation_coherence(): client.list_team_invitations(team.id) ) if not team_invitations: - ensure_not_detail( + ensure_success( client.create_team_invitation( team.id, CreateTeamInvitation(