Skip to content

Commit f1dcb30

Browse files
authored
Fix/google next standard response (#962)
* refactor: standardize error handling and response modeling in Google API integration * refactor: enhance module documentation for Google Workspace API integration * refactor: improve test coverage and error handling in Google Workspace API integration * feat: add FakeGoogleService for testing Google API integrations * fix: double wrapping IntegrationResponse * refactor: update Google Directory API functions to return IntegrationResponse * refactor: update tests to utilize IntegrationResponse for Google API calls * refactor: enhance Google Workspace schemas with additional fields and validation * feat: add factory helper functions for creating Google groups, users, and members * test: add validation tests for Google groups, users, and members helpers * refactor: delegate Google groups, users, and members fixture logic to shared helper functions * refactor: replace fixture logic with factory helpers for Google groups, users, and members in tests * test: update assertions in API error handling test for IntegrationResponse * refactor: revert legacy fixture logic with direct implementations for Google groups, users, and members * refactor: propagate standardized IntegrationResponse directly
1 parent 2875b4f commit f1dcb30

File tree

9 files changed

+1593
-914
lines changed

9 files changed

+1593
-914
lines changed

app/integrations/google_workspace/google_directory_next.py

Lines changed: 183 additions & 121 deletions
Large diffs are not rendered by default.

app/integrations/google_workspace/google_service_next.py

Lines changed: 189 additions & 164 deletions
Large diffs are not rendered by default.
Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,143 @@
1-
from typing import List, Optional
2-
from pydantic import BaseModel
1+
from typing import Any, Dict, List, Optional, Union
2+
from pydantic import BaseModel, Field, field_validator
3+
4+
5+
class Name(BaseModel):
6+
givenName: Optional[str] = None
7+
familyName: Optional[str] = None
8+
fullName: Optional[str] = None
9+
displayName: Optional[str] = None
10+
11+
model_config = {"extra": "ignore"}
12+
13+
14+
class User(BaseModel):
15+
id: Optional[str] = None
16+
primaryEmail: Optional[str] = None
17+
name: Optional[Name] = None
18+
suspended: Optional[bool] = None
19+
emails: Optional[List[Dict[str, Any]]] = None
20+
aliases: Optional[List[str]] = None
21+
nonEditableAliases: Optional[List[str]] = None
22+
customerId: Optional[str] = None
23+
orgUnitPath: Optional[str] = None
24+
thumbnailPhotoUrl: Optional[str] = None
25+
thumbnailPhotoEtag: Optional[str] = None
26+
recoveryEmail: Optional[str] = None
27+
recoveryPhone: Optional[str] = None
28+
isAdmin: Optional[bool] = None
29+
isDelegatedAdmin: Optional[bool] = None
30+
lastLoginTime: Optional[str] = None
31+
creationTime: Optional[str] = None
32+
agreedToTerms: Optional[bool] = None
33+
archived: Optional[bool] = None
34+
changePasswordAtNextLogin: Optional[bool] = None
35+
ipWhitelisted: Optional[bool] = None
36+
isMailboxSetup: Optional[bool] = None
37+
isEnrolledIn2Sv: Optional[bool] = None
38+
isEnforcedIn2Sv: Optional[bool] = None
39+
includeInGlobalAddressList: Optional[bool] = None
40+
# languages can be a list of dicts in sample JSON; accept any for flexibility
41+
languages: Optional[List[Any]] = None
42+
43+
model_config = {"extra": "ignore"}
44+
45+
46+
class Member(BaseModel):
47+
kind: Optional[str] = None
48+
etag: Optional[str] = None
49+
id: Optional[str] = None
50+
email: Optional[str] = None
51+
role: Optional[str] = None
52+
type: Optional[str] = None
53+
status: Optional[str] = None
54+
primaryEmail: Optional[str] = None
55+
name: Optional[Dict[str, Any]] = None
56+
isAdmin: Optional[bool] = None
57+
isDelegatedAdmin: Optional[bool] = None
58+
59+
model_config = {"extra": "ignore"}
360

461

562
class Group(BaseModel):
6-
id: Optional[str]
7-
email: Optional[str]
8-
name: Optional[str]
9-
description: Optional[str]
10-
directMembersCount: Optional[int]
63+
kind: Optional[str] = None
64+
id: Optional[str] = None
65+
etag: Optional[str] = None
66+
email: Optional[str] = None
67+
name: Optional[str] = None
68+
description: Optional[str] = None
69+
# directMembersCount may be returned as string by API; accept both and coerce to int when possible
70+
directMembersCount: Optional[Union[int, str]] = None
71+
adminCreated: Optional[bool] = None
72+
nonEditableAliases: Optional[List[str]] = None
73+
74+
model_config = {"extra": "ignore"}
75+
76+
@field_validator("directMembersCount", mode="before")
77+
def _coerce_direct_members_count(cls, v):
78+
if v is None:
79+
return None
80+
# Accept int already
81+
if isinstance(v, int):
82+
return v
83+
# Accept numeric strings
84+
if isinstance(v, str):
85+
try:
86+
return int(v)
87+
except ValueError:
88+
return None
89+
# Fallback: try to coerce to int
90+
try:
91+
return int(v)
92+
except Exception:
93+
return None
94+
1195

96+
# Enriched member used for assembled responses (contains nested user details)
97+
class MemberWithUser(Member):
98+
user: Optional[User] = None
1299

100+
model_config = {"extra": "ignore"}
101+
102+
103+
# Assembled group model that includes members (enriched)
104+
class GroupWithMembers(Group):
105+
members: List[MemberWithUser] = Field(default_factory=list)
106+
107+
model_config = {"extra": "ignore"}
108+
109+
110+
class GroupsResult(BaseModel):
111+
result: List[Group] = Field(default_factory=list)
112+
time: Optional[float] = None
113+
summary: Optional[str] = None
114+
115+
model_config = {"extra": "ignore"}
116+
117+
118+
# Backwards-compatible list response types (keep for existing tests/code)
13119
class GroupsListResponse(BaseModel):
14120
kind: Optional[str]
15121
etag: Optional[str]
16-
groups: List[Group] = []
122+
groups: List[Group] = Field(default_factory=list)
17123
nextPageToken: Optional[str]
18124

19-
20-
class Member(BaseModel):
21-
kind: Optional[str]
22-
email: Optional[str]
23-
role: Optional[str]
24-
type: Optional[str]
25-
status: Optional[str]
26-
id: Optional[str]
125+
model_config = {"extra": "ignore"}
27126

28127

29128
class MembersListResponse(BaseModel):
30129
kind: Optional[str]
31130
etag: Optional[str]
32-
members: List[Member] = []
131+
members: List[Member] = Field(default_factory=list)
33132
nextPageToken: Optional[str]
34133

35-
36-
class User(BaseModel):
37-
id: Optional[str]
38-
primaryEmail: Optional[str]
39-
name: Optional[dict]
40-
suspended: Optional[bool]
41-
emails: Optional[list]
134+
model_config = {"extra": "ignore"}
42135

43136

44137
class UsersListResponse(BaseModel):
45138
kind: Optional[str]
46139
etag: Optional[str]
47-
users: List[User] = []
140+
users: List[User] = Field(default_factory=list)
48141
nextPageToken: Optional[str]
142+
143+
model_config = {"extra": "ignore"}

app/tests/conftest.py

Lines changed: 60 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import pytest
2-
from integrations.google_workspace.schemas import (
3-
Group,
4-
User,
5-
Member,
2+
from tests.factory_helpers import (
3+
make_google_groups,
4+
make_google_users,
5+
make_google_members,
66
)
77

88
# Google API Python Client
99

1010

1111
# Google Discovery Directory Resource
12-
# Base fixtures
12+
# Legacy Fixtures
1313
@pytest.fixture
1414
def google_groups():
1515
def _google_groups(n=3, prefix="", domain="test.com"):
@@ -75,127 +75,6 @@ def _google_group_members(n=3, prefix="", domain="test.com"):
7575
return _google_group_members
7676

7777

78-
# --- Google Directory API Pydantic factories ---
79-
80-
81-
@pytest.fixture
82-
def google_group_factory():
83-
"""
84-
Factory fixture to generate a list of valid Google Group dicts for tests.
85-
Usage:
86-
groups = google_group_factory(n=2, prefix="dev-")
87-
# returns a list of dicts (model_dump)
88-
"""
89-
90-
def _factory(n=3, prefix="", domain="test.com", as_model=False):
91-
groups = [
92-
Group(
93-
id=f"{prefix}google_group_id{i+1}",
94-
name=f"{prefix}group-name{i+1}",
95-
email=f"{prefix}group-name{i+1}@{domain}",
96-
description=f"{prefix}description{i+1}",
97-
directMembersCount=i + 1,
98-
)
99-
for i in range(n)
100-
]
101-
if as_model:
102-
return groups
103-
return [g.model_dump() for g in groups]
104-
105-
return _factory
106-
107-
108-
@pytest.fixture
109-
def google_user_factory():
110-
"""
111-
Factory fixture to generate a list of valid Google User dicts or models for tests.
112-
Usage:
113-
users = google_user_factory(n=2, prefix="dev-")
114-
# returns a list of dicts (model_dump)
115-
users = google_user_factory(n=2, as_model=True)
116-
# returns a list of User models
117-
"""
118-
119-
def _factory(n=3, prefix="", domain="test.com", as_model=False):
120-
users = [
121-
User(
122-
id=f"{prefix}user_id{i+1}",
123-
primaryEmail=f"{prefix}user-email{i+1}@{domain}",
124-
emails=[
125-
{
126-
"address": f"{prefix}user-email{i+1}@{domain}",
127-
"primary": True,
128-
"type": "work",
129-
}
130-
],
131-
suspended=False,
132-
name={
133-
"fullName": f"Given_name_{i+1} Family_name_{i+1}",
134-
"familyName": f"Family_name_{i+1}",
135-
"givenName": f"Given_name_{i+1}",
136-
"displayName": f"Given_name_{i+1} Family_name_{i+1}",
137-
},
138-
)
139-
for i in range(n)
140-
]
141-
if as_model:
142-
return users
143-
return [u.model_dump() for u in users]
144-
145-
return _factory
146-
147-
148-
@pytest.fixture
149-
def google_member_factory():
150-
"""
151-
Factory fixture to generate a list of valid Google Member dicts or models for tests.
152-
Usage:
153-
members = google_member_factory(n=2, prefix="dev-")
154-
# returns a list of dicts (model_dump)
155-
members = google_member_factory(n=2, as_model=True)
156-
# returns a list of Member models
157-
"""
158-
159-
def _factory(n=3, prefix="", domain="test.com", as_model=False):
160-
users = [
161-
User(
162-
id=f"{prefix}user_id{i+1}",
163-
primaryEmail=f"{prefix}user-email{i+1}@{domain}",
164-
emails=[
165-
{
166-
"address": f"{prefix}user-email{i+1}@{domain}",
167-
"primary": True,
168-
"type": "work",
169-
}
170-
],
171-
suspended=False,
172-
name={
173-
"fullName": f"Given_name_{i+1} Family_name_{i+1}",
174-
"familyName": f"Family_name_{i+1}",
175-
"givenName": f"Given_name_{i+1}",
176-
"displayName": f"Given_name_{i+1} Family_name_{i+1}",
177-
},
178-
)
179-
for i in range(n)
180-
]
181-
members = [
182-
Member(
183-
kind="admin#directory#member",
184-
email=user.primaryEmail,
185-
role="MEMBER",
186-
type="USER",
187-
status="ACTIVE",
188-
id=user.id,
189-
)
190-
for user in users
191-
]
192-
if as_model:
193-
return members
194-
return [m.model_dump() for m in members]
195-
196-
return _factory
197-
198-
19978
# Fixture with users
20079
@pytest.fixture
20180
def google_groups_w_users(google_groups, google_group_members, google_users):
@@ -264,6 +143,61 @@ def _factory(n_groups=1, n_members_per_group=2, domain="test.com"):
264143
return _factory
265144

266145

146+
# --- Google Directory API Pydantic factories ---
147+
148+
149+
@pytest.fixture
150+
def google_group_factory():
151+
"""
152+
Factory fixture to generate a list of valid Google Group dicts for tests.
153+
Usage:
154+
groups = google_group_factory(n=2, prefix="dev-")
155+
# returns a list of dicts (model_dump)
156+
"""
157+
158+
def _factory(n=3, prefix="", domain="test.com", as_model=False):
159+
# Delegate to shared helper to avoid duplicated logic
160+
return make_google_groups(n=n, prefix=prefix, domain=domain, as_model=as_model)
161+
162+
return _factory
163+
164+
165+
@pytest.fixture
166+
def google_user_factory():
167+
"""
168+
Factory fixture to generate a list of valid Google User dicts or models for tests.
169+
Usage:
170+
users = google_user_factory(n=2, prefix="dev-")
171+
# returns a list of dicts (model_dump)
172+
users = google_user_factory(n=2, as_model=True)
173+
# returns a list of User models
174+
"""
175+
176+
def _factory(n=3, prefix="", domain="test.com", as_model=False):
177+
# Delegate to shared helper to avoid duplicated logic
178+
return make_google_users(n=n, prefix=prefix, domain=domain, as_model=as_model)
179+
180+
return _factory
181+
182+
183+
@pytest.fixture
184+
def google_member_factory():
185+
"""
186+
Factory fixture to generate a list of valid Google Member dicts or models for tests.
187+
Usage:
188+
members = google_member_factory(n=2, prefix="dev-")
189+
# returns a list of dicts (model_dump)
190+
members = google_member_factory(n=2, as_model=True)
191+
# returns a list of Member models
192+
"""
193+
194+
def _factory(n=3, prefix="", domain="test.com", as_model=False):
195+
# Delegate to shared helper to avoid duplicated logic
196+
return make_google_members(n=n, prefix=prefix, domain=domain, as_model=as_model)
197+
198+
return _factory
199+
200+
267201
@pytest.fixture
268202
def google_batch_response_factory():
269203
"""Factory to create Google API batch response structures."""

0 commit comments

Comments
 (0)