Skip to content

Commit dea2edb

Browse files
franccescoclaude
andcommitted
test: add integration tests for all SDK operations
Add 134 integration tests that validate all operations against the real Bloom Growth API, covering users, meetings, scorecard, todos, goals, issues, and headlines (sync + async). Tests are marked with @pytest.mark.integration and can be deselected for CI with -m "not integration". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 13f11fd commit dea2edb

7 files changed

Lines changed: 2013 additions & 0 deletions

tests/test_integration_goals.py

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
"""Integration tests for goal operations against the real Bloom Growth API."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
import pytest_asyncio
7+
8+
from bloomy import AsyncClient, Client
9+
from bloomy.models import (
10+
ArchivedGoalInfo,
11+
CreatedGoalInfo,
12+
GoalInfo,
13+
GoalListResponse,
14+
GoalStatus,
15+
)
16+
17+
MEETING_ID = 324926
18+
19+
20+
@pytest.fixture(scope="module")
21+
def client() -> Client:
22+
"""Create a real client for integration tests.
23+
24+
Yields:
25+
A Bloomy client instance.
26+
27+
"""
28+
c = Client()
29+
yield c
30+
c.close()
31+
32+
33+
@pytest_asyncio.fixture
34+
async def async_client() -> AsyncClient:
35+
"""Create a real async client for integration tests.
36+
37+
Yields:
38+
An async Bloomy client instance.
39+
40+
"""
41+
c = AsyncClient()
42+
yield c
43+
await c.close()
44+
45+
46+
# ── Sync Tests ──────────────────────────────────────────────────────────
47+
48+
49+
@pytest.mark.integration
50+
class TestGoalListSync:
51+
"""Test listing goals (sync)."""
52+
53+
def test_list_active_goals(self, client: Client) -> None:
54+
"""List active goals for the current user."""
55+
goals = client.goal.list()
56+
assert isinstance(goals, list)
57+
for goal in goals:
58+
assert isinstance(goal, GoalInfo)
59+
60+
def test_list_with_archived(self, client: Client) -> None:
61+
"""List goals including archived ones returns GoalListResponse."""
62+
result = client.goal.list(archived=True)
63+
assert isinstance(result, GoalListResponse)
64+
assert isinstance(result.active, list)
65+
assert isinstance(result.archived, list)
66+
for g in result.active:
67+
assert isinstance(g, GoalInfo)
68+
for g in result.archived:
69+
assert isinstance(g, ArchivedGoalInfo)
70+
71+
72+
@pytest.mark.integration
73+
class TestGoalCRUDSync:
74+
"""Test full CRUD lifecycle for goals (sync)."""
75+
76+
def test_create_and_delete_goal(self, client: Client) -> None:
77+
"""Create a goal, verify it, then delete."""
78+
goal = client.goal.create(title="Integration test goal", meeting_id=MEETING_ID)
79+
try:
80+
assert isinstance(goal, CreatedGoalInfo)
81+
assert goal.title == "Integration test goal"
82+
assert goal.meeting_id == MEETING_ID
83+
assert goal.id > 0
84+
finally:
85+
client.goal.delete(goal.id)
86+
87+
def test_update_goal_title(self, client: Client) -> None:
88+
"""Update a goal title."""
89+
goal = client.goal.create(title="Goal original title", meeting_id=MEETING_ID)
90+
try:
91+
client.goal.update(goal.id, title="Goal updated title")
92+
93+
# Verify via list
94+
goals = client.goal.list()
95+
assert isinstance(goals, list)
96+
updated = next((g for g in goals if g.id == goal.id), None)
97+
assert updated is not None
98+
assert updated.title == "Goal updated title"
99+
finally:
100+
client.goal.delete(goal.id)
101+
102+
def test_update_goal_status_enum(self, client: Client) -> None:
103+
"""Update a goal status using GoalStatus enum."""
104+
goal = client.goal.create(title="Goal status enum test", meeting_id=MEETING_ID)
105+
try:
106+
# Set to on-track
107+
client.goal.update(goal.id, status=GoalStatus.ON_TRACK)
108+
# Set to at-risk
109+
client.goal.update(goal.id, status=GoalStatus.AT_RISK)
110+
# Set to complete
111+
client.goal.update(goal.id, status=GoalStatus.COMPLETE)
112+
finally:
113+
client.goal.delete(goal.id)
114+
115+
def test_update_goal_status_string(self, client: Client) -> None:
116+
"""Update a goal status using string values."""
117+
goal = client.goal.create(
118+
title="Goal status string test", meeting_id=MEETING_ID
119+
)
120+
try:
121+
client.goal.update(goal.id, status="on")
122+
client.goal.update(goal.id, status="off")
123+
client.goal.update(goal.id, status="complete")
124+
finally:
125+
client.goal.delete(goal.id)
126+
127+
def test_update_goal_invalid_status(self, client: Client) -> None:
128+
"""Invalid status string should raise ValueError."""
129+
goal = client.goal.create(
130+
title="Goal invalid status test", meeting_id=MEETING_ID
131+
)
132+
try:
133+
with pytest.raises(ValueError, match="Invalid status value"):
134+
client.goal.update(goal.id, status="invalid")
135+
finally:
136+
client.goal.delete(goal.id)
137+
138+
def test_archive_and_restore_goal(self, client: Client) -> None:
139+
"""Archive a goal, verify it's archived, then restore it."""
140+
goal = client.goal.create(title="Goal archive test", meeting_id=MEETING_ID)
141+
try:
142+
# Archive
143+
client.goal.archive(goal.id)
144+
145+
# Verify it appears in archived list
146+
result = client.goal.list(archived=True)
147+
assert isinstance(result, GoalListResponse)
148+
archived_ids = [g.id for g in result.archived]
149+
assert goal.id in archived_ids
150+
151+
# Restore
152+
client.goal.restore(goal.id)
153+
154+
# Verify it's back in active list
155+
goals = client.goal.list()
156+
assert isinstance(goals, list)
157+
active_ids = [g.id for g in goals]
158+
assert goal.id in active_ids
159+
finally:
160+
client.goal.delete(goal.id)
161+
162+
def test_delete_goal(self, client: Client) -> None:
163+
"""Delete a goal and verify it's gone."""
164+
goal = client.goal.create(title="Goal delete test", meeting_id=MEETING_ID)
165+
client.goal.delete(goal.id)
166+
167+
# Verify gone from active list
168+
goals = client.goal.list()
169+
assert isinstance(goals, list)
170+
assert all(g.id != goal.id for g in goals)
171+
172+
173+
@pytest.mark.integration
174+
class TestGoalUpdateOwnerBug:
175+
"""Test for the known bug: goal.update() always overwrites accountable_user.
176+
177+
When calling goal.update(goal_id, title="new title") without passing
178+
accountable_user, the current implementation defaults accountable_user
179+
to self.user_id. This means the owner is silently overwritten even if
180+
the caller only intended to update the title.
181+
"""
182+
183+
def test_update_title_should_not_change_owner(self, client: Client) -> None:
184+
"""Updating only the title should preserve the original owner.
185+
186+
This test exposes the bug in goals.py line 158-159 where
187+
`accountable_user` defaults to `self.user_id` when None.
188+
"""
189+
# Create a goal (owned by current user)
190+
goal = client.goal.create(
191+
title="Owner preservation test", meeting_id=MEETING_ID
192+
)
193+
try:
194+
# Get the original owner
195+
goals = client.goal.list()
196+
assert isinstance(goals, list)
197+
original = next(g for g in goals if g.id == goal.id)
198+
original_user_id = original.user_id
199+
200+
# Update only the title — should NOT change owner
201+
client.goal.update(goal.id, title="Owner preservation updated")
202+
203+
# Verify the owner is still the same
204+
goals = client.goal.list()
205+
assert isinstance(goals, list)
206+
updated = next(g for g in goals if g.id == goal.id)
207+
assert updated.title == "Owner preservation updated"
208+
assert updated.user_id == original_user_id, (
209+
f"Bug: owner changed from {original_user_id} to {updated.user_id} "
210+
f"when only updating title. goal.update() should not default "
211+
f"accountable_user to self.user_id when it's not provided."
212+
)
213+
finally:
214+
client.goal.delete(goal.id)
215+
216+
217+
# ── Async Tests ─────────────────────────────────────────────────────────
218+
219+
220+
@pytest.mark.integration
221+
class TestGoalListAsync:
222+
"""Test listing goals (async)."""
223+
224+
@pytest.mark.asyncio
225+
async def test_list_active_goals(self, async_client: AsyncClient) -> None:
226+
"""List active goals (async)."""
227+
goals = await async_client.goal.list()
228+
assert isinstance(goals, list)
229+
for goal in goals:
230+
assert isinstance(goal, GoalInfo)
231+
232+
@pytest.mark.asyncio
233+
async def test_list_with_archived(self, async_client: AsyncClient) -> None:
234+
"""List goals including archived (async)."""
235+
result = await async_client.goal.list(archived=True)
236+
assert isinstance(result, GoalListResponse)
237+
238+
239+
@pytest.mark.integration
240+
class TestGoalCRUDAsync:
241+
"""Test full CRUD lifecycle for goals (async)."""
242+
243+
@pytest.mark.asyncio
244+
async def test_full_lifecycle(self, async_client: AsyncClient) -> None:
245+
"""Create, update, archive, restore, delete a goal (async)."""
246+
# Create
247+
goal = await async_client.goal.create(
248+
title="Async integration test goal", meeting_id=MEETING_ID
249+
)
250+
try:
251+
assert isinstance(goal, CreatedGoalInfo)
252+
assert goal.title == "Async integration test goal"
253+
254+
# Update title
255+
await async_client.goal.update(goal.id, title="Async updated goal")
256+
257+
# Update status
258+
await async_client.goal.update(goal.id, status=GoalStatus.ON_TRACK)
259+
260+
# Archive
261+
await async_client.goal.archive(goal.id)
262+
263+
# Restore
264+
await async_client.goal.restore(goal.id)
265+
finally:
266+
# Delete
267+
await async_client.goal.delete(goal.id)
268+
269+
@pytest.mark.asyncio
270+
async def test_update_title_should_not_change_owner(
271+
self, async_client: AsyncClient
272+
) -> None:
273+
"""Async version: updating title should not change owner."""
274+
goal = await async_client.goal.create(
275+
title="Async owner preservation test", meeting_id=MEETING_ID
276+
)
277+
try:
278+
goals = await async_client.goal.list()
279+
assert isinstance(goals, list)
280+
original = next(g for g in goals if g.id == goal.id)
281+
original_user_id = original.user_id
282+
283+
await async_client.goal.update(goal.id, title="Async owner updated")
284+
285+
goals = await async_client.goal.list()
286+
assert isinstance(goals, list)
287+
updated = next(g for g in goals if g.id == goal.id)
288+
assert updated.user_id == original_user_id, (
289+
f"Bug: owner changed from {original_user_id} to {updated.user_id}"
290+
)
291+
finally:
292+
await async_client.goal.delete(goal.id)

0 commit comments

Comments
 (0)