Skip to content

Commit 0dbb878

Browse files
authored
Merge pull request #133 from GitGuardian/ybensafia/setup-test-workplace-script
Implement script to setup workspace for tests
2 parents 633ea99 + 90d92c3 commit 0dbb878

File tree

5 files changed

+585
-224
lines changed

5 files changed

+585
-224
lines changed

scripts/release

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ROOT_DIR = Path(__file__).parent.parent
1919
CHANGELOG_PATH = ROOT_DIR / "CHANGELOG.md"
2020
INIT_PATH = ROOT_DIR / "pygitguardian" / "__init__.py"
2121
CASSETTES_DIR = ROOT_DIR / "tests" / "cassettes"
22+
SETUP_WORKSPACE_SCRIPT = ROOT_DIR / "scripts" / "setup_test_workspace.py"
2223

2324
# The branch this script must be run from, except in dev mode.
2425
RELEASE_BRANCH = "master"
@@ -97,6 +98,16 @@ def main(dev_mode: bool) -> int:
9798
return 0
9899

99100

101+
def setup_test_workspace():
102+
log_progress("Setting up workspace")
103+
try:
104+
check_run(
105+
["pdm", "run", SETUP_WORKSPACE_SCRIPT], cwd=ROOT_DIR, stderr=subprocess.PIPE
106+
)
107+
except subprocess.CalledProcessError as exc:
108+
fail(f"There was an error setting up the test workspace :\n{exc.stderr}")
109+
110+
100111
@main.command()
101112
def run_tests() -> None:
102113
"""Run all tests.
@@ -110,6 +121,8 @@ def run_tests() -> None:
110121
shutil.rmtree(CASSETTES_DIR)
111122
CASSETTES_DIR.mkdir()
112123

124+
setup_test_workspace()
125+
113126
log_progress("Running tests")
114127
check_run(["pytest", "tests"], cwd=ROOT_DIR)
115128

scripts/setup_test_workspace.py

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"""
2+
Notice: This script will attempt to setup a test workspace on GitGuardian.
3+
This will allow the user to run tests without relying on cassettes, note that
4+
there are a few limitations due to actions that cannot be performed through
5+
the API, notably :
6+
- Create the workspace
7+
- We cannot create members, so there must exist a minimum amount of members in the workspace
8+
- This also means deleted members cannot be brought back from the script
9+
- We cannot integrate a source entirely from the public API
10+
- There must exist a source in the workspace
11+
"""
12+
13+
import os
14+
from typing import Iterable, List, TypeVar
15+
16+
from pygitguardian.client import GGClient
17+
from pygitguardian.models import (
18+
AccessLevel,
19+
CreateInvitation,
20+
CreateTeam,
21+
CreateTeamInvitation,
22+
CreateTeamMember,
23+
Detail,
24+
IncidentPermission,
25+
InvitationParameters,
26+
Member,
27+
MembersParameters,
28+
Source,
29+
Team,
30+
TeamMember,
31+
TeamsParameters,
32+
UpdateMember,
33+
UpdateTeamSource,
34+
)
35+
from pygitguardian.models_utils import FromDictWithBase
36+
from tests.utils import CursorPaginatedResponse
37+
38+
39+
client = GGClient(
40+
api_key=os.environ["GITGUARDIAN_API_KEY"],
41+
base_uri=os.environ.get("GITGUARDIAN_API_URL"),
42+
)
43+
44+
T = TypeVar("T")
45+
PaginatedDataType = TypeVar("PaginatedDataType", bound=FromDictWithBase)
46+
47+
MIN_NB_TEAM = 2
48+
MIN_NB_MEMBER = 3 # 1 owner, 1 manager and at least one member
49+
MIN_NB_TEAM_MEMBER = 2
50+
# This is the team that is created in the tests, it should be deleted before we run the tests
51+
PYGITGUARDIAN_TEST_TEAM = "PyGitGuardian team"
52+
53+
54+
def ensure_success(var: T | Detail) -> T:
55+
if not isinstance(var, Detail):
56+
return var
57+
else:
58+
raise TypeError(var.detail)
59+
60+
61+
def unwrap_paginated_response(
62+
var: CursorPaginatedResponse[PaginatedDataType] | Detail,
63+
) -> List[PaginatedDataType]:
64+
data = ensure_success(var)
65+
66+
return data.data
67+
68+
69+
def ensure_member_coherence():
70+
"""
71+
This function ensures that the workspace :
72+
- Has no deactivated members
73+
- If there are, they will be activated
74+
- Has at most 1 admin / manager (excluding owner)
75+
- It may demote some manager to member
76+
- There is at least `MIN_NB_MEMBER`
77+
"""
78+
79+
deactivated_members = unwrap_paginated_response(
80+
client.list_members(MembersParameters(active=False))
81+
)
82+
for member in deactivated_members:
83+
client.update_member(UpdateMember(member.id, AccessLevel.MEMBER, active=True))
84+
85+
admin_members = unwrap_paginated_response(
86+
client.list_members(MembersParameters(access_level=AccessLevel.MANAGER))
87+
)
88+
89+
if len(admin_members) > 1:
90+
for member in admin_members[1:]:
91+
ensure_success(
92+
client.update_member(UpdateMember(member.id, AccessLevel.MEMBER))
93+
)
94+
else:
95+
members = unwrap_paginated_response(
96+
client.list_members(MembersParameters(access_level=AccessLevel.MEMBER))
97+
)
98+
assert (
99+
len(members) > 0
100+
), "There must be at least one member with access level member in the workspace"
101+
102+
ensure_success(
103+
client.update_member(UpdateMember(members[0].id, AccessLevel.MANAGER))
104+
)
105+
106+
members = ensure_success(client.list_members(MembersParameters(per_page=5)))
107+
108+
assert (
109+
len(members.data) > MIN_NB_MEMBER
110+
), "There must be at least 3 members in the workspace"
111+
112+
113+
def add_source_to_team(team: Team, available_sources: Iterable[Source] | None = None):
114+
if available_sources is None:
115+
available_sources = ensure_success(client.list_sources()).data
116+
117+
ensure_success(
118+
client.update_team_source(
119+
UpdateTeamSource(team.id, [source.id for source in available_sources], [])
120+
)
121+
)
122+
123+
124+
def add_team_members(
125+
team: Team,
126+
team_members: Iterable[TeamMember],
127+
nb_members: int,
128+
available_members: Iterable[Member] | None = None,
129+
):
130+
assert nb_members > 0, "We should add at least one member"
131+
if available_members is None:
132+
available_members = unwrap_paginated_response(client.list_members())
133+
134+
# Every manager is by default a team leader
135+
has_admin = any(team_member.is_team_leader for team_member in team_members)
136+
137+
if not has_admin:
138+
admin_member = next(
139+
(
140+
member
141+
for member in available_members
142+
if member.access_level == AccessLevel.MANAGER
143+
),
144+
None,
145+
)
146+
assert admin_member is not None, "There should be at least one admin member"
147+
148+
ensure_success(
149+
client.create_team_member(
150+
team.id,
151+
CreateTeamMember(
152+
admin_member.id,
153+
is_team_leader=True,
154+
incident_permission=IncidentPermission.FULL_ACCESS,
155+
),
156+
)
157+
)
158+
nb_members -= 1
159+
160+
team_member_ids = {team_member.member_id for team_member in team_members}
161+
for _ in range(nb_members):
162+
to_add_member = next(
163+
(
164+
member
165+
for member in available_members
166+
if member.id not in team_member_ids
167+
and member.access_level not in {AccessLevel.OWNER, AccessLevel.MANAGER}
168+
),
169+
None,
170+
)
171+
assert to_add_member is not None, "There is not enough members in the workspace"
172+
is_team_leader = False
173+
permissions = IncidentPermission.FULL_ACCESS
174+
175+
if to_add_member.access_level == AccessLevel.MANAGER:
176+
is_team_leader = True
177+
178+
ensure_success(
179+
client.create_team_member(
180+
team.id,
181+
CreateTeamMember(
182+
to_add_member.id,
183+
is_team_leader=is_team_leader,
184+
incident_permission=permissions,
185+
),
186+
)
187+
)
188+
189+
190+
def ensure_team_coherence():
191+
"""
192+
This function ensures that the workspace :
193+
- Has no team with name prefixed by `PYGITGUARDIAN_TEST_TEAM`
194+
- At least `MIN_NB_TEAM` exist
195+
- If not they will be created
196+
- Every team has at least one source
197+
- If possible, it will try to add at least one source
198+
- Every team has at least 2 members, an admin and a member
199+
- If possible, it will try to add those members
200+
"""
201+
202+
pygitguardian_teams = []
203+
try:
204+
pygitguardian_teams = unwrap_paginated_response(
205+
client.list_teams(TeamsParameters(search=PYGITGUARDIAN_TEST_TEAM))
206+
)
207+
except TypeError as exc:
208+
if str(exc) != "Team not found.":
209+
raise
210+
finally:
211+
for team in pygitguardian_teams:
212+
ensure_success(client.delete_team(team.id))
213+
214+
teams = unwrap_paginated_response(
215+
# exclude global team since we can't add sources / members to it
216+
client.list_teams(TeamsParameters(is_global=False))
217+
)
218+
219+
nb_teams = len(teams)
220+
if nb_teams < MIN_NB_TEAM:
221+
for i in range(MIN_NB_TEAM - nb_teams):
222+
new_team = ensure_success(
223+
client.create_team(CreateTeam(name=f"PyGitGuardian Team {i}"))
224+
)
225+
teams.append(new_team)
226+
227+
# Ensure every team has:
228+
# - At least one source
229+
# - At least two members, one with admin access and one with member access
230+
for team in teams:
231+
team_members = unwrap_paginated_response(client.list_team_members(team.id))
232+
nb_team_members = len(team_members)
233+
if nb_team_members < MIN_NB_TEAM_MEMBER:
234+
add_team_members(team, team_members, MIN_NB_TEAM_MEMBER - nb_team_members)
235+
236+
team_sources = unwrap_paginated_response(client.list_team_sources(team.id))
237+
nb_team_sources = len(team_sources)
238+
if nb_team_sources == 0:
239+
add_source_to_team(team)
240+
241+
242+
def ensure_invitation_coherence():
243+
"""
244+
This function ensures that the workspace :
245+
- Has no invitation for emails starting with `pygitguardian`
246+
- There is at least one pending invitation
247+
- If not, an invitation will be sent to `pygitguardian@example.com`
248+
- All team have attached team invitations
249+
- If not, they will be created
250+
"""
251+
252+
test_invitation = unwrap_paginated_response(
253+
client.list_invitations(InvitationParameters(search="pygitguardian"))
254+
)
255+
256+
for invitation in test_invitation:
257+
ensure_success(client.delete_invitation(invitation.id))
258+
invitations = unwrap_paginated_response(client.list_invitations())
259+
260+
if len(invitations) < 1:
261+
invitation = ensure_success(
262+
client.create_invitation(
263+
CreateInvitation(
264+
email="pygitguardian@invitation.com",
265+
access_level=AccessLevel.MEMBER,
266+
)
267+
)
268+
)
269+
invitations.append(invitation)
270+
271+
teams = unwrap_paginated_response(client.list_teams())
272+
invitation = invitations[0]
273+
for team in teams:
274+
team_invitations = unwrap_paginated_response(
275+
client.list_team_invitations(team.id)
276+
)
277+
if not team_invitations:
278+
ensure_success(
279+
client.create_team_invitation(
280+
team.id,
281+
CreateTeamInvitation(
282+
invitation_id=invitation.id,
283+
is_team_leader=False,
284+
incident_permission=IncidentPermission.FULL_ACCESS,
285+
),
286+
)
287+
)
288+
289+
290+
def main():
291+
ensure_member_coherence()
292+
ensure_team_coherence()
293+
ensure_invitation_coherence()
294+
295+
print("Test workspace has been set up properly")
296+
297+
298+
if __name__ == "__main__":
299+
main()

0 commit comments

Comments
 (0)