Skip to content

Commit fa5b5d5

Browse files
committed
now a test module per method
1 parent 6ef4f04 commit fa5b5d5

File tree

183 files changed

+5325
-4636
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

183 files changed

+5325
-4636
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@ _data
5454
_logs
5555
_sample_responses
5656
_scripts
57+
_cov_html
5758
secrets.json
5859
tokens.json

TODO.md

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22

33
## TODO
44

5-
- [ ] break bigger test files down into files by method: activity and nutrition,
6-
at least!
7-
8-
- [ ] confirm that validation of methods where before_date or after_date is
9-
handled consistently across classes
5+
\_ [ ] Complete coverage
106

117
- [ ] need constants for sort "desc"/"asc"
128

13-
- [ ] break tests into separate files for each method? It's a lot, but the test
14-
files are huge
9+
- [ ] Confront MyPy. See https://stackoverflow.com/a/51294709 for json help
10+
11+
- [ ] add a note about typing, method naming semantics, aliases, and interns.
12+
13+
- [ ] When typing resource, wrap the actual response type around the JSONType,,
14+
e.g. List[JSONType], Dict[str, JSONType], so that the user can actually know
15+
what to expect (or None, of course)
16+
17+
- [ ] Test that all methods have an alias in `Client` and that the signatures
18+
match
1519

1620
- [ ] validate:
1721
`fitbit_client.exceptions.ValidationException: Calories From Fat must be a valid non-negative number. Currently it is "146.79".`
1822
It needs to be a positive int!
1923

20-
- [ ] Make sure that mocks have type annotations
24+
- [ ] Should mocks have type annotations?
2125

2226
- [ ] consolidate fixures where possible
2327

@@ -66,17 +70,9 @@
6670
- [ ] Form to chang scopes are part of OAuth flow? Maybe get rid of the cut and
6771
paste method altogether? It's less to test...
6872

69-
- [ ] Validation to NutritionResource - Calories must be ints, not floats like
70-
everything else
71-
7273
- [ ] Response validation? Accidentally doing a GET instead of a POST on, e.g.
7374
`food.json` will yield a response, but not the one you want!
7475

75-
- [ ] Confront MyPy. See https://stackoverflow.com/a/51294709 for json help
76-
77-
- [ ] Test that all methods have an alias in `Client` and that the signatures
78-
match
79-
8076
- [ ] Make the food download_food_logs (rename to `get_food_logs`) and food log
8177
to CSV part of one helper package. It should expand the foods to have their
8278
complete nutrition (a separate call for each unique food)

fitbit_client/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from fitbit_client.resources.activity_timeseries import ActivityTimeSeriesResource
1313
from fitbit_client.resources.body import BodyResource
1414
from fitbit_client.resources.body_timeseries import BodyTimeSeriesResource
15-
from fitbit_client.resources.breathingrate import BreathingRateResource
15+
from fitbit_client.resources.breathing_rate import BreathingRateResource
1616
from fitbit_client.resources.cardio_fitness_score import CardioFitnessScoreResource
1717
from fitbit_client.resources.device import DeviceResource
1818
from fitbit_client.resources.electrocardiogram import ElectrocardiogramResource
@@ -83,7 +83,7 @@ def __init__(
8383
self.activity: ActivityResource = ActivityResource(self.auth.session, language=language, locale=locale)
8484
self.body_timeseries: BodyTimeSeriesResource = BodyTimeSeriesResource(self.auth.session, language=language, locale=locale)
8585
self.body: BodyResource = BodyResource(self.auth.session, language=language, locale=locale)
86-
self.breathingrate: BreathingRateResource = BreathingRateResource(self.auth.session, language=language, locale=locale)
86+
self.breathing_rate: BreathingRateResource = BreathingRateResource(self.auth.session, language=language, locale=locale)
8787
self.cardio_fitness_score: CardioFitnessScoreResource = CardioFitnessScoreResource(self.auth.session, language=language, locale=locale)
8888
self.device: DeviceResource = DeviceResource(self.auth.session, language=language, locale=locale)
8989
self.electrocardiogram: ElectrocardiogramResource = ElectrocardiogramResource(self.auth.session, language=language, locale=locale)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# fitbit_client/resources/breathingrate.py
1+
# fitbit_client/resources/breathing_rate.py
22

33
# Standard library imports
44
from typing import Any

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ extensions = [
7575
testpaths = ["tests"]
7676
minversion = 6.0
7777
python_files = "test_*.py"
78-
addopts = "-ra -q --cov=fitbit_client --cache-clear --cov-report=term-missing -v --tb=native"
78+
addopts = "-ra -q --cov=fitbit_client --cache-clear --cov-report=term-missing --cov-report=html:cov_html -v --tb=native"
7979
pythonpath = ["."]
8080

8181
# https://pytest-cov.readthedocs.io/en/latest/config.html
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# tests/resources/active_zone_minutes/__init__.py
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# tests/resources/active_zone_minutes/conftest.py
2+
3+
"""Fixtures for active_zone_minutes tests."""
4+
5+
# Standard library imports
6+
7+
# Standard library imports
8+
from unittest.mock import patch
9+
10+
# Third party imports
11+
from pytest import fixture
12+
13+
# Local imports
14+
from fitbit_client.resources.active_zone_minutes import ActiveZoneMinutesResource
15+
16+
17+
@fixture
18+
def azm_resource(mock_oauth_session, mock_logger):
19+
"""Fixture to provide an ActiveZoneMinutesResource instance"""
20+
with patch("fitbit_client.resources.base.getLogger", return_value=mock_logger):
21+
return ActiveZoneMinutesResource(
22+
oauth_session=mock_oauth_session, locale="en_US", language="en_US"
23+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# tests/resources/active_zone_minutes/test_get_azm_timeseries.py
2+
3+
"""Tests for the get_azm_timeseries endpoint."""
4+
5+
6+
def test_get_azm_timeseries_with_today_date(azm_resource, mock_response):
7+
"""Test using 'today' as the date parameter"""
8+
mock_response.json.return_value = {"activities-active-zone-minutes": []}
9+
azm_resource.oauth.request.return_value = mock_response
10+
result = azm_resource.get_azm_timeseries_by_date(date="today")
11+
assert result == mock_response.json.return_value
12+
azm_resource.oauth.request.assert_called_once_with(
13+
"GET",
14+
"https://api.fitbit.com/1/user/-/activities/active-zone-minutes/date/today/1d.json",
15+
data=None,
16+
json=None,
17+
params=None,
18+
headers={"Accept-Locale": "en_US", "Accept-Language": "en_US"},
19+
)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# tests/resources/active_zone_minutes/test_get_azm_timeseries_by_date.py
2+
3+
"""Tests for the get_azm_timeseries_by_date endpoint."""
4+
5+
# Third party imports
6+
7+
# Third party imports
8+
from pytest import raises
9+
10+
# Local imports
11+
from fitbit_client.exceptions import InvalidDateException
12+
from fitbit_client.resources.constants import Period
13+
14+
15+
def test_get_azm_timeseries_by_date_success(azm_resource, mock_response):
16+
"""Test successful retrieval of AZM time series by date with default period"""
17+
mock_response.json.return_value = {
18+
"activities-active-zone-minutes": [
19+
{
20+
"dateTime": "2025-02-01",
21+
"value": {
22+
"activeZoneMinutes": 102,
23+
"fatBurnActiveZoneMinutes": 90,
24+
"cardioActiveZoneMinutes": 8,
25+
"peakActiveZoneMinutes": 4,
26+
},
27+
}
28+
]
29+
}
30+
azm_resource.oauth.request.return_value = mock_response
31+
result = azm_resource.get_azm_timeseries_by_date(date="2025-02-01")
32+
assert result == mock_response.json.return_value
33+
azm_resource.oauth.request.assert_called_once_with(
34+
"GET",
35+
"https://api.fitbit.com/1/user/-/activities/active-zone-minutes/date/2025-02-01/1d.json",
36+
data=None,
37+
json=None,
38+
params=None,
39+
headers={"Accept-Locale": "en_US", "Accept-Language": "en_US"},
40+
)
41+
42+
43+
def test_get_azm_timeseries_by_date_explicit_period(azm_resource, mock_response):
44+
"""Test successful retrieval of AZM time series by date with explicit ONE_DAY period"""
45+
mock_response.json.return_value = {"activities-active-zone-minutes": []}
46+
azm_resource.oauth.request.return_value = mock_response
47+
result = azm_resource.get_azm_timeseries_by_date(date="2025-02-01", period=Period.ONE_DAY)
48+
assert result == mock_response.json.return_value
49+
azm_resource.oauth.request.assert_called_once_with(
50+
"GET",
51+
"https://api.fitbit.com/1/user/-/activities/active-zone-minutes/date/2025-02-01/1d.json",
52+
data=None,
53+
json=None,
54+
params=None,
55+
headers={"Accept-Locale": "en_US", "Accept-Language": "en_US"},
56+
)
57+
58+
59+
def test_get_azm_timeseries_by_date_with_user_id(azm_resource, mock_response):
60+
"""Test getting AZM time series for a specific user"""
61+
mock_response.json.return_value = {"activities-active-zone-minutes": []}
62+
azm_resource.oauth.request.return_value = mock_response
63+
result = azm_resource.get_azm_timeseries_by_date(date="2025-02-01", user_id="123ABC")
64+
assert result == mock_response.json.return_value
65+
azm_resource.oauth.request.assert_called_once_with(
66+
"GET",
67+
"https://api.fitbit.com/1/user/123ABC/activities/active-zone-minutes/date/2025-02-01/1d.json",
68+
data=None,
69+
json=None,
70+
params=None,
71+
headers={"Accept-Locale": "en_US", "Accept-Language": "en_US"},
72+
)
73+
74+
75+
def test_get_azm_timeseries_by_date_invalid_period(azm_resource):
76+
"""Test that using any period other than ONE_DAY raises ValueError"""
77+
invalid_periods = [
78+
Period.SEVEN_DAYS,
79+
Period.THIRTY_DAYS,
80+
Period.ONE_WEEK,
81+
Period.ONE_MONTH,
82+
Period.THREE_MONTHS,
83+
Period.SIX_MONTHS,
84+
Period.ONE_YEAR,
85+
Period.MAX,
86+
]
87+
for period in invalid_periods:
88+
with raises(ValueError) as exc_info:
89+
azm_resource.get_azm_timeseries_by_date(date="2025-02-01", period=period)
90+
assert "Only 1d period is supported for AZM time series" in str(exc_info.value)
91+
92+
93+
def test_get_azm_timeseries_by_date_invalid_date(azm_resource):
94+
"""Test that invalid date format raises InvalidDateException"""
95+
with raises(InvalidDateException) as exc_info:
96+
azm_resource.get_azm_timeseries_by_date(date="invalid-date")
97+
assert "invalid-date" in str(exc_info.value)
98+
assert exc_info.value.field_name == "date"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# tests/resources/active_zone_minutes/test_get_azm_timeseries_by_interval.py
2+
3+
"""Tests for the get_azm_timeseries_by_interval endpoint."""
4+
5+
# Third party imports
6+
7+
# Standard library imports
8+
from datetime import datetime
9+
from datetime import timedelta
10+
11+
# Third party imports
12+
from pytest import raises
13+
14+
# Local imports
15+
from fitbit_client.exceptions import InvalidDateException
16+
from fitbit_client.exceptions import InvalidDateRangeException
17+
18+
19+
def test_get_azm_timeseries_by_interval_success(azm_resource, mock_response):
20+
"""Test successful retrieval of AZM time series by date range"""
21+
mock_response.json.return_value = {
22+
"activities-active-zone-minutes": [
23+
{
24+
"dateTime": "2025-02-01",
25+
"value": {
26+
"activeZoneMinutes": 102,
27+
"fatBurnActiveZoneMinutes": 90,
28+
"cardioActiveZoneMinutes": 8,
29+
"peakActiveZoneMinutes": 4,
30+
},
31+
},
32+
{
33+
"dateTime": "2025-02-02",
34+
"value": {
35+
"activeZoneMinutes": 47,
36+
"fatBurnActiveZoneMinutes": 43,
37+
"cardioActiveZoneMinutes": 4,
38+
},
39+
},
40+
]
41+
}
42+
azm_resource.oauth.request.return_value = mock_response
43+
result = azm_resource.get_azm_timeseries_by_interval(
44+
start_date="2025-02-01", end_date="2025-02-02"
45+
)
46+
assert result == mock_response.json.return_value
47+
azm_resource.oauth.request.assert_called_once_with(
48+
"GET",
49+
"https://api.fitbit.com/1/user/-/activities/active-zone-minutes/date/2025-02-01/2025-02-02.json",
50+
data=None,
51+
json=None,
52+
params=None,
53+
headers={"Accept-Locale": "en_US", "Accept-Language": "en_US"},
54+
)
55+
56+
57+
def test_get_azm_timeseries_by_interval_with_user_id(azm_resource, mock_response):
58+
"""Test getting AZM time series by date range for a specific user"""
59+
mock_response.json.return_value = {"activities-active-zone-minutes": []}
60+
azm_resource.oauth.request.return_value = mock_response
61+
result = azm_resource.get_azm_timeseries_by_interval(
62+
start_date="2025-02-01", end_date="2025-02-02", user_id="123ABC"
63+
)
64+
assert result == mock_response.json.return_value
65+
azm_resource.oauth.request.assert_called_once_with(
66+
"GET",
67+
"https://api.fitbit.com/1/user/123ABC/activities/active-zone-minutes/date/2025-02-01/2025-02-02.json",
68+
data=None,
69+
json=None,
70+
params=None,
71+
headers={"Accept-Locale": "en_US", "Accept-Language": "en_US"},
72+
)
73+
74+
75+
def test_get_azm_timeseries_by_interval_invalid_dates(azm_resource):
76+
"""Test that invalid date formats raise InvalidDateException"""
77+
with raises(InvalidDateException) as exc_info:
78+
azm_resource.get_azm_timeseries_by_interval(start_date="invalid", end_date="2024-02-01")
79+
assert "invalid" in str(exc_info.value)
80+
assert exc_info.value.field_name == "start_date"
81+
with raises(InvalidDateException) as exc_info:
82+
azm_resource.get_azm_timeseries_by_interval(start_date="2024-02-01", end_date="invalid")
83+
assert "invalid" in str(exc_info.value)
84+
assert exc_info.value.field_name == "end_date"
85+
86+
87+
def test_get_azm_timeseries_by_interval_invalid_date_order(azm_resource):
88+
"""Test that start_date after end_date raises InvalidDateRangeException"""
89+
with raises(InvalidDateRangeException) as exc_info:
90+
azm_resource.get_azm_timeseries_by_interval(start_date="2025-02-02", end_date="2025-02-01")
91+
assert "Start date 2025-02-02 is after end date 2025-02-01" in str(exc_info.value)
92+
93+
94+
def test_get_azm_timeseries_by_interval_exceeds_max_range(azm_resource):
95+
"""Test that exceeding the 1095 day range limit raises InvalidDateRangeException"""
96+
start_date = (datetime.now() - timedelta(days=1096)).strftime("%Y-%m-%d")
97+
end_date = datetime.now().strftime("%Y-%m-%d")
98+
with raises(InvalidDateRangeException) as exc_info:
99+
azm_resource.get_azm_timeseries_by_interval(start_date=start_date, end_date=end_date)
100+
assert "1095 days" in str(exc_info.value)
101+
assert "AZM time series" in str(exc_info.value)

0 commit comments

Comments
 (0)