pgw-api 1.2.0__tar.gz → 1.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pgw_api-1.2.0 → pgw_api-1.2.1}/PKG-INFO +1 -1
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api.egg-info/PKG-INFO +1 -1
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api.egg-info/SOURCES.txt +1 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pyproject.toml +1 -1
- pgw_api-1.2.1/tests/test_billing.py +138 -0
- pgw_api-1.2.1/tests/test_client.py +388 -0
- pgw_api-1.2.0/tests/test_client.py +0 -199
- {pgw_api-1.2.0 → pgw_api-1.2.1}/LICENSE +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/README.md +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api/__init__.py +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api/client.py +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api/exceptions.py +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api/models.py +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api.egg-info/dependency_links.txt +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api.egg-info/requires.txt +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/pgw_api.egg-info/top_level.txt +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/setup.cfg +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/tests/test_models.py +0 -0
- {pgw_api-1.2.0 → pgw_api-1.2.1}/tests/test_parsing.py +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Tests for billing HTML parsing and the BillingSummary model."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from pgw_api.client import PGWApiClient
|
|
9
|
+
from pgw_api.models import BillingSummary
|
|
10
|
+
from tests.conftest import make_response, make_login_success, make_webmethod_response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
BILLING_HTML = """
|
|
14
|
+
<input type="hidden" id="hdnTotalBillOFCurrentMonth" value="$120.50" />
|
|
15
|
+
<input type="hidden" id="hdnGasUsageOFCurrentMonth" value="85.0" />
|
|
16
|
+
<input type="hidden" id="hdnnumOfDaysCurrentMonth" value="32" />
|
|
17
|
+
<input type="hidden" id="hdnTotalBillOFPreviousMonth" value="$95.00" />
|
|
18
|
+
<input type="hidden" id="hdnGasUsageOFPreviousMonth" value="60.0" />
|
|
19
|
+
<input type="hidden" id="hdnnumOfDaysPreviousMonth" value="30" />
|
|
20
|
+
<input type="hidden" id="hdnTotalBillOFPreviousYearPreviousMonth" value="$110.00" />
|
|
21
|
+
<input type="hidden" id="hdnGasUsageOFPreviousYearPreviousMonth" value="80.0" />
|
|
22
|
+
<input type="hidden" id="hdnPrevAmount" value="$120.50" />
|
|
23
|
+
<input type="hidden" id="hdnbillComparisionOFCurrentMonth" value="[{"Periodfrom":"2024-01-15T00:00:00","PeriodTo":"2024-02-14T00:00:00"}]" />
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _setup_session(responses):
|
|
28
|
+
session = MagicMock()
|
|
29
|
+
idx = {"i": 0}
|
|
30
|
+
|
|
31
|
+
def _ctx(resp):
|
|
32
|
+
cm = MagicMock()
|
|
33
|
+
cm.__aenter__ = AsyncMock(return_value=resp)
|
|
34
|
+
cm.__aexit__ = AsyncMock(return_value=False)
|
|
35
|
+
return cm
|
|
36
|
+
|
|
37
|
+
def _next(*args, **kwargs):
|
|
38
|
+
r = responses[idx["i"]]
|
|
39
|
+
idx["i"] += 1
|
|
40
|
+
return _ctx(r)
|
|
41
|
+
|
|
42
|
+
session.get = MagicMock(side_effect=_next)
|
|
43
|
+
session.post = MagicMock(side_effect=_next)
|
|
44
|
+
return session
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestBillingParsing:
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_parses_all_fields(self):
|
|
50
|
+
session = _setup_session([
|
|
51
|
+
make_response(), # GET login page
|
|
52
|
+
make_response(text=make_login_success()), # POST validateLogin
|
|
53
|
+
make_response(), # GET Dashboard
|
|
54
|
+
make_response(text=BILLING_HTML), # GET BillDashboard
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
client = PGWApiClient("user", "pass")
|
|
58
|
+
billing = await client.async_get_billing(session)
|
|
59
|
+
|
|
60
|
+
assert billing.current_bill == 120.50
|
|
61
|
+
assert billing.current_usage_ccf == 85.0
|
|
62
|
+
assert billing.current_period_days == 32
|
|
63
|
+
assert billing.previous_bill == 95.00
|
|
64
|
+
assert billing.previous_usage_ccf == 60.0
|
|
65
|
+
assert billing.previous_period_days == 30
|
|
66
|
+
assert billing.previous_year_bill == 110.00
|
|
67
|
+
assert billing.previous_year_usage_ccf == 80.0
|
|
68
|
+
assert billing.balance_due == 120.50
|
|
69
|
+
assert billing.period_start == date(2024, 1, 15)
|
|
70
|
+
assert billing.period_end == date(2024, 2, 14)
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_missing_fields_default_to_zero(self):
|
|
74
|
+
html = "<html><body>no hidden fields here</body></html>"
|
|
75
|
+
session = _setup_session([
|
|
76
|
+
make_response(),
|
|
77
|
+
make_response(text=make_login_success()),
|
|
78
|
+
make_response(),
|
|
79
|
+
make_response(text=html),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
client = PGWApiClient("user", "pass")
|
|
83
|
+
billing = await client.async_get_billing(session)
|
|
84
|
+
|
|
85
|
+
assert billing.current_bill == 0.0
|
|
86
|
+
assert billing.current_usage_ccf == 0.0
|
|
87
|
+
assert billing.current_period_days == 0
|
|
88
|
+
assert billing.period_start is None
|
|
89
|
+
assert billing.period_end is None
|
|
90
|
+
|
|
91
|
+
@pytest.mark.asyncio
|
|
92
|
+
async def test_malformed_period_json(self):
|
|
93
|
+
html = '<input type="hidden" id="hdnbillComparisionOFCurrentMonth" value="not json" />'
|
|
94
|
+
session = _setup_session([
|
|
95
|
+
make_response(),
|
|
96
|
+
make_response(text=make_login_success()),
|
|
97
|
+
make_response(),
|
|
98
|
+
make_response(text=html),
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
client = PGWApiClient("user", "pass")
|
|
102
|
+
billing = await client.async_get_billing(session)
|
|
103
|
+
assert billing.period_start is None
|
|
104
|
+
assert billing.period_end is None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestBillingSummaryModel:
|
|
108
|
+
def test_current_usage_cf(self):
|
|
109
|
+
b = BillingSummary(
|
|
110
|
+
current_bill=100, current_usage_ccf=50, current_period_days=30,
|
|
111
|
+
previous_bill=80, previous_usage_ccf=40, previous_period_days=30,
|
|
112
|
+
previous_year_bill=90, previous_year_usage_ccf=45, balance_due=100,
|
|
113
|
+
)
|
|
114
|
+
assert b.current_usage_cf == 5000.0
|
|
115
|
+
|
|
116
|
+
def test_previous_usage_cf(self):
|
|
117
|
+
b = BillingSummary(
|
|
118
|
+
current_bill=100, current_usage_ccf=50, current_period_days=30,
|
|
119
|
+
previous_bill=80, previous_usage_ccf=40, previous_period_days=30,
|
|
120
|
+
previous_year_bill=90, previous_year_usage_ccf=45, balance_due=100,
|
|
121
|
+
)
|
|
122
|
+
assert b.previous_usage_cf == 4000.0
|
|
123
|
+
|
|
124
|
+
def test_current_rate(self):
|
|
125
|
+
b = BillingSummary(
|
|
126
|
+
current_bill=200, current_usage_ccf=100, current_period_days=30,
|
|
127
|
+
previous_bill=80, previous_usage_ccf=40, previous_period_days=30,
|
|
128
|
+
previous_year_bill=90, previous_year_usage_ccf=45, balance_due=200,
|
|
129
|
+
)
|
|
130
|
+
assert b.current_rate == 2.0
|
|
131
|
+
|
|
132
|
+
def test_current_rate_zero_usage(self):
|
|
133
|
+
b = BillingSummary(
|
|
134
|
+
current_bill=0, current_usage_ccf=0, current_period_days=0,
|
|
135
|
+
previous_bill=0, previous_usage_ccf=0, previous_period_days=0,
|
|
136
|
+
previous_year_bill=0, previous_year_usage_ccf=0, balance_due=0,
|
|
137
|
+
)
|
|
138
|
+
assert b.current_rate is None
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Tests for PGWApiClient using mocked HTTP responses."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from pgw_api.client import PGWApiClient
|
|
10
|
+
from pgw_api.exceptions import PGWAuthError, PGWConnectionError
|
|
11
|
+
from tests.conftest import CSRF_HTML, make_login_success, make_response, make_webmethod_response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _setup_session(responses):
|
|
15
|
+
"""Wire up a mock session with a sequence of responses."""
|
|
16
|
+
session = MagicMock()
|
|
17
|
+
idx = {"i": 0}
|
|
18
|
+
|
|
19
|
+
def _ctx(resp):
|
|
20
|
+
cm = MagicMock()
|
|
21
|
+
cm.__aenter__ = AsyncMock(return_value=resp)
|
|
22
|
+
cm.__aexit__ = AsyncMock(return_value=False)
|
|
23
|
+
return cm
|
|
24
|
+
|
|
25
|
+
def _next(*args, **kwargs):
|
|
26
|
+
r = responses[idx["i"]]
|
|
27
|
+
idx["i"] += 1
|
|
28
|
+
return _ctx(r)
|
|
29
|
+
|
|
30
|
+
session.get = MagicMock(side_effect=_next)
|
|
31
|
+
session.post = MagicMock(side_effect=_next)
|
|
32
|
+
return session
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestMonthlyUsage:
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_parses_monthly_entries(self):
|
|
38
|
+
usage_payload = make_webmethod_response({
|
|
39
|
+
"objUsageGenerationResultSetTwo": [
|
|
40
|
+
{"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
|
|
41
|
+
{"Month": 12, "Year": 2023, "UsageValue": 72.0, "FromDate": "11/14/23", "ToDate": "12/15/23"},
|
|
42
|
+
]
|
|
43
|
+
})
|
|
44
|
+
session = _setup_session([
|
|
45
|
+
make_response(), # GET login page
|
|
46
|
+
make_response(text=make_login_success()), # POST validateLogin
|
|
47
|
+
make_response(), # GET Dashboard
|
|
48
|
+
make_response(text=CSRF_HTML), # GET usage page
|
|
49
|
+
make_response(text=usage_payload), # POST LoadGasUsage
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
client = PGWApiClient("user", "pass")
|
|
53
|
+
result = await client.async_get_usage(session)
|
|
54
|
+
|
|
55
|
+
assert len(result) == 2
|
|
56
|
+
assert result[0].month == date(2024, 1, 1)
|
|
57
|
+
assert result[0].ccf == 85.0
|
|
58
|
+
assert result[1].month == date(2023, 12, 1)
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_empty_usage(self):
|
|
62
|
+
usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
|
|
63
|
+
session = _setup_session([
|
|
64
|
+
make_response(),
|
|
65
|
+
make_response(text=make_login_success()),
|
|
66
|
+
make_response(),
|
|
67
|
+
make_response(text=CSRF_HTML),
|
|
68
|
+
make_response(text=usage_payload),
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
client = PGWApiClient("user", "pass")
|
|
72
|
+
result = await client.async_get_usage(session)
|
|
73
|
+
assert result == []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestDailyUsage:
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_parses_daily_entries(self):
|
|
79
|
+
usage_payload = make_webmethod_response({
|
|
80
|
+
"objUsageGenerationResultSetTwo": [
|
|
81
|
+
{"FromDate": "01/15/24", "UsageValue": 3.2},
|
|
82
|
+
{"FromDate": "01/14/24", "UsageValue": 4.1},
|
|
83
|
+
{"FromDate": "01/16/24", "UsageValue": 2.8},
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
session = _setup_session([
|
|
87
|
+
make_response(),
|
|
88
|
+
make_response(text=make_login_success()),
|
|
89
|
+
make_response(),
|
|
90
|
+
make_response(text=CSRF_HTML),
|
|
91
|
+
make_response(text=usage_payload),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
client = PGWApiClient("user", "pass")
|
|
95
|
+
result = await client.async_get_daily_usage(
|
|
96
|
+
session, date(2024, 1, 14), date(2024, 1, 16)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
assert len(result) == 3
|
|
100
|
+
assert result[0].date == date(2024, 1, 16)
|
|
101
|
+
assert result[0].ccf == 2.8
|
|
102
|
+
assert result[-1].date == date(2024, 1, 14)
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_skips_entries_missing_date(self):
|
|
106
|
+
usage_payload = make_webmethod_response({
|
|
107
|
+
"objUsageGenerationResultSetTwo": [
|
|
108
|
+
{"FromDate": "01/15/24", "UsageValue": 3.2},
|
|
109
|
+
{"FromDate": None, "UsageValue": 4.1},
|
|
110
|
+
]
|
|
111
|
+
})
|
|
112
|
+
session = _setup_session([
|
|
113
|
+
make_response(),
|
|
114
|
+
make_response(text=make_login_success()),
|
|
115
|
+
make_response(),
|
|
116
|
+
make_response(text=CSRF_HTML),
|
|
117
|
+
make_response(text=usage_payload),
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
client = PGWApiClient("user", "pass")
|
|
121
|
+
result = await client.async_get_daily_usage(
|
|
122
|
+
session, date(2024, 1, 1), date(2024, 1, 31)
|
|
123
|
+
)
|
|
124
|
+
assert len(result) == 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestHourlyUsage:
|
|
128
|
+
@pytest.mark.asyncio
|
|
129
|
+
async def test_parses_hourly_entries(self):
|
|
130
|
+
usage_payload = make_webmethod_response({
|
|
131
|
+
"objUsageGenerationResultSetTwo": [
|
|
132
|
+
{"FromDate": "01/15/2024 01:00:00 AM", "UsageValue": 0.3},
|
|
133
|
+
{"FromDate": "01/15/2024 02:00:00 AM", "UsageValue": 0.4},
|
|
134
|
+
{"FromDate": "01/15/2024 12:00:00 PM", "UsageValue": 0.1},
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
session = _setup_session([
|
|
138
|
+
make_response(),
|
|
139
|
+
make_response(text=make_login_success()),
|
|
140
|
+
make_response(),
|
|
141
|
+
make_response(text=CSRF_HTML),
|
|
142
|
+
make_response(text=usage_payload),
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
client = PGWApiClient("user", "pass")
|
|
146
|
+
result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
|
|
147
|
+
|
|
148
|
+
assert len(result) == 3
|
|
149
|
+
assert result[0].timestamp == datetime(2024, 1, 15, 12, 0, 0)
|
|
150
|
+
assert result[0].ccf == 0.1
|
|
151
|
+
assert result[-1].timestamp == datetime(2024, 1, 15, 1, 0, 0)
|
|
152
|
+
|
|
153
|
+
@pytest.mark.asyncio
|
|
154
|
+
async def test_empty_hourly(self):
|
|
155
|
+
usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
|
|
156
|
+
session = _setup_session([
|
|
157
|
+
make_response(),
|
|
158
|
+
make_response(text=make_login_success()),
|
|
159
|
+
make_response(),
|
|
160
|
+
make_response(text=CSRF_HTML),
|
|
161
|
+
make_response(text=usage_payload),
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
client = PGWApiClient("user", "pass")
|
|
165
|
+
result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
|
|
166
|
+
assert result == []
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TestGetAll:
|
|
170
|
+
@pytest.mark.asyncio
|
|
171
|
+
async def test_returns_usage_and_billing(self):
|
|
172
|
+
billing_html = """
|
|
173
|
+
<input type="hidden" id="hdnTotalBillOFCurrentMonth" value="$120.50" />
|
|
174
|
+
<input type="hidden" id="hdnGasUsageOFCurrentMonth" value="85.0" />
|
|
175
|
+
<input type="hidden" id="hdnnumOfDaysCurrentMonth" value="32" />
|
|
176
|
+
<input type="hidden" id="hdnTotalBillOFPreviousMonth" value="$95.00" />
|
|
177
|
+
<input type="hidden" id="hdnGasUsageOFPreviousMonth" value="60.0" />
|
|
178
|
+
<input type="hidden" id="hdnnumOfDaysPreviousMonth" value="30" />
|
|
179
|
+
<input type="hidden" id="hdnTotalBillOFPreviousYearPreviousMonth" value="$110.00" />
|
|
180
|
+
<input type="hidden" id="hdnGasUsageOFPreviousYearPreviousMonth" value="80.0" />
|
|
181
|
+
<input type="hidden" id="hdnPrevAmount" value="$120.50" />
|
|
182
|
+
"""
|
|
183
|
+
usage_payload = make_webmethod_response({
|
|
184
|
+
"objUsageGenerationResultSetTwo": [
|
|
185
|
+
{"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
|
|
186
|
+
]
|
|
187
|
+
})
|
|
188
|
+
session = _setup_session([
|
|
189
|
+
make_response(), # GET login page
|
|
190
|
+
make_response(text=make_login_success()), # POST validateLogin
|
|
191
|
+
make_response(), # GET Dashboard (billing)
|
|
192
|
+
make_response(text=billing_html), # GET BillDashboard
|
|
193
|
+
make_response(), # GET Dashboard (usage)
|
|
194
|
+
make_response(text=CSRF_HTML), # GET usage page
|
|
195
|
+
make_response(text=usage_payload), # POST LoadGasUsage
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
client = PGWApiClient("user", "pass")
|
|
199
|
+
usage, billing = await client.async_get_all(session)
|
|
200
|
+
|
|
201
|
+
assert len(usage) == 1
|
|
202
|
+
assert usage[0].ccf == 85.0
|
|
203
|
+
assert billing.current_bill == 120.50
|
|
204
|
+
assert billing.balance_due == 120.50
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestValidateCredentials:
|
|
208
|
+
@pytest.mark.asyncio
|
|
209
|
+
async def test_returns_true_on_success(self):
|
|
210
|
+
session = _setup_session([
|
|
211
|
+
make_response(),
|
|
212
|
+
make_response(text=make_login_success()),
|
|
213
|
+
])
|
|
214
|
+
|
|
215
|
+
client = PGWApiClient("user", "pass")
|
|
216
|
+
assert await client.async_validate_credentials(session) is True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TestErrorHandling:
|
|
220
|
+
@pytest.mark.asyncio
|
|
221
|
+
async def test_csrf_error_raises_auth_error(self):
|
|
222
|
+
error_payload = make_webmethod_response({
|
|
223
|
+
"dtException": [{"MessageInformation": "Invalid CSRF Token"}]
|
|
224
|
+
})
|
|
225
|
+
session = _setup_session([
|
|
226
|
+
make_response(),
|
|
227
|
+
make_response(text=make_login_success()),
|
|
228
|
+
make_response(),
|
|
229
|
+
make_response(text=CSRF_HTML),
|
|
230
|
+
make_response(text=error_payload),
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
client = PGWApiClient("user", "pass")
|
|
234
|
+
with pytest.raises(PGWAuthError, match="CSRF"):
|
|
235
|
+
await client.async_get_usage(session)
|
|
236
|
+
|
|
237
|
+
@pytest.mark.asyncio
|
|
238
|
+
async def test_bad_credentials(self):
|
|
239
|
+
login_fail = make_webmethod_response(
|
|
240
|
+
[{"STATUS": 0, "Message": "Invalid username or password"}]
|
|
241
|
+
)
|
|
242
|
+
session = _setup_session([
|
|
243
|
+
make_response(),
|
|
244
|
+
make_response(text=login_fail),
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
client = PGWApiClient("user", "wrong")
|
|
248
|
+
with pytest.raises(PGWAuthError, match="Invalid username"):
|
|
249
|
+
await client.async_get_usage(session)
|
|
250
|
+
|
|
251
|
+
@pytest.mark.asyncio
|
|
252
|
+
async def test_login_exception_response(self):
|
|
253
|
+
login_err = make_webmethod_response(
|
|
254
|
+
{"dtException": [{"MessageInformation": "Account locked"}]}
|
|
255
|
+
)
|
|
256
|
+
session = _setup_session([
|
|
257
|
+
make_response(),
|
|
258
|
+
make_response(text=login_err),
|
|
259
|
+
])
|
|
260
|
+
|
|
261
|
+
client = PGWApiClient("user", "pass")
|
|
262
|
+
with pytest.raises(PGWAuthError, match="Account locked"):
|
|
263
|
+
await client.async_get_usage(session)
|
|
264
|
+
|
|
265
|
+
@pytest.mark.asyncio
|
|
266
|
+
async def test_login_empty_response(self):
|
|
267
|
+
login_empty = make_webmethod_response([])
|
|
268
|
+
session = _setup_session([
|
|
269
|
+
make_response(),
|
|
270
|
+
make_response(text=login_empty),
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
client = PGWApiClient("user", "pass")
|
|
274
|
+
with pytest.raises(PGWAuthError, match="unexpected response"):
|
|
275
|
+
await client.async_get_usage(session)
|
|
276
|
+
|
|
277
|
+
@pytest.mark.asyncio
|
|
278
|
+
async def test_login_non_200(self):
|
|
279
|
+
session = _setup_session([
|
|
280
|
+
make_response(),
|
|
281
|
+
make_response(status=500),
|
|
282
|
+
])
|
|
283
|
+
|
|
284
|
+
client = PGWApiClient("user", "pass")
|
|
285
|
+
with pytest.raises(PGWAuthError, match="status 500"):
|
|
286
|
+
await client.async_get_usage(session)
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_login_page_non_200(self):
|
|
290
|
+
session = _setup_session([
|
|
291
|
+
make_response(status=503),
|
|
292
|
+
])
|
|
293
|
+
|
|
294
|
+
client = PGWApiClient("user", "pass")
|
|
295
|
+
with pytest.raises(PGWConnectionError, match="status 503"):
|
|
296
|
+
await client.async_get_usage(session)
|
|
297
|
+
|
|
298
|
+
@pytest.mark.asyncio
|
|
299
|
+
async def test_missing_csrf_token(self):
|
|
300
|
+
session = _setup_session([
|
|
301
|
+
make_response(),
|
|
302
|
+
make_response(text=make_login_success()),
|
|
303
|
+
make_response(),
|
|
304
|
+
make_response(text="<html>no token here</html>"),
|
|
305
|
+
])
|
|
306
|
+
|
|
307
|
+
client = PGWApiClient("user", "pass")
|
|
308
|
+
with pytest.raises(PGWConnectionError, match="CSRF token"):
|
|
309
|
+
await client.async_get_usage(session)
|
|
310
|
+
|
|
311
|
+
@pytest.mark.asyncio
|
|
312
|
+
async def test_usage_page_non_200(self):
|
|
313
|
+
session = _setup_session([
|
|
314
|
+
make_response(),
|
|
315
|
+
make_response(text=make_login_success()),
|
|
316
|
+
make_response(),
|
|
317
|
+
make_response(status=500),
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
client = PGWApiClient("user", "pass")
|
|
321
|
+
with pytest.raises(PGWConnectionError, match="status 500"):
|
|
322
|
+
await client.async_get_usage(session)
|
|
323
|
+
|
|
324
|
+
@pytest.mark.asyncio
|
|
325
|
+
async def test_load_usage_non_200(self):
|
|
326
|
+
session = _setup_session([
|
|
327
|
+
make_response(),
|
|
328
|
+
make_response(text=make_login_success()),
|
|
329
|
+
make_response(),
|
|
330
|
+
make_response(text=CSRF_HTML),
|
|
331
|
+
make_response(status=500),
|
|
332
|
+
])
|
|
333
|
+
|
|
334
|
+
client = PGWApiClient("user", "pass")
|
|
335
|
+
with pytest.raises(PGWConnectionError, match="status 500"):
|
|
336
|
+
await client.async_get_usage(session)
|
|
337
|
+
|
|
338
|
+
@pytest.mark.asyncio
|
|
339
|
+
async def test_load_usage_malformed_json(self):
|
|
340
|
+
session = _setup_session([
|
|
341
|
+
make_response(),
|
|
342
|
+
make_response(text=make_login_success()),
|
|
343
|
+
make_response(),
|
|
344
|
+
make_response(text=CSRF_HTML),
|
|
345
|
+
make_response(text="not json"),
|
|
346
|
+
])
|
|
347
|
+
|
|
348
|
+
client = PGWApiClient("user", "pass")
|
|
349
|
+
with pytest.raises(PGWConnectionError, match="Unexpected response"):
|
|
350
|
+
await client.async_get_usage(session)
|
|
351
|
+
|
|
352
|
+
@pytest.mark.asyncio
|
|
353
|
+
async def test_general_exception_in_usage(self):
|
|
354
|
+
error_payload = make_webmethod_response({
|
|
355
|
+
"dtException": [{"MessageInformation": "Something went wrong"}]
|
|
356
|
+
})
|
|
357
|
+
session = _setup_session([
|
|
358
|
+
make_response(),
|
|
359
|
+
make_response(text=make_login_success()),
|
|
360
|
+
make_response(),
|
|
361
|
+
make_response(text=CSRF_HTML),
|
|
362
|
+
make_response(text=error_payload),
|
|
363
|
+
])
|
|
364
|
+
|
|
365
|
+
client = PGWApiClient("user", "pass")
|
|
366
|
+
with pytest.raises(PGWConnectionError, match="Something went wrong"):
|
|
367
|
+
await client.async_get_usage(session)
|
|
368
|
+
|
|
369
|
+
@pytest.mark.asyncio
|
|
370
|
+
async def test_skips_monthly_entries_missing_fields(self):
|
|
371
|
+
usage_payload = make_webmethod_response({
|
|
372
|
+
"objUsageGenerationResultSetTwo": [
|
|
373
|
+
{"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
|
|
374
|
+
{"Month": None, "Year": 2024, "UsageValue": 50.0},
|
|
375
|
+
{"Month": 2, "Year": None, "UsageValue": 50.0},
|
|
376
|
+
]
|
|
377
|
+
})
|
|
378
|
+
session = _setup_session([
|
|
379
|
+
make_response(),
|
|
380
|
+
make_response(text=make_login_success()),
|
|
381
|
+
make_response(),
|
|
382
|
+
make_response(text=CSRF_HTML),
|
|
383
|
+
make_response(text=usage_payload),
|
|
384
|
+
])
|
|
385
|
+
|
|
386
|
+
client = PGWApiClient("user", "pass")
|
|
387
|
+
result = await client.async_get_usage(session)
|
|
388
|
+
assert len(result) == 1
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"""Tests for PGWApiClient using mocked HTTP responses."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from datetime import date, datetime
|
|
5
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from pgw_api.client import PGWApiClient
|
|
10
|
-
from pgw_api.exceptions import PGWAuthError, PGWConnectionError
|
|
11
|
-
from tests.conftest import CSRF_HTML, make_login_success, make_response, make_webmethod_response
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _setup_session(responses):
|
|
15
|
-
"""Wire up a mock session with a sequence of responses."""
|
|
16
|
-
session = MagicMock()
|
|
17
|
-
idx = {"i": 0}
|
|
18
|
-
|
|
19
|
-
def _ctx(resp):
|
|
20
|
-
cm = MagicMock()
|
|
21
|
-
cm.__aenter__ = AsyncMock(return_value=resp)
|
|
22
|
-
cm.__aexit__ = AsyncMock(return_value=False)
|
|
23
|
-
return cm
|
|
24
|
-
|
|
25
|
-
def _next(*args, **kwargs):
|
|
26
|
-
r = responses[idx["i"]]
|
|
27
|
-
idx["i"] += 1
|
|
28
|
-
return _ctx(r)
|
|
29
|
-
|
|
30
|
-
session.get = MagicMock(side_effect=_next)
|
|
31
|
-
session.post = MagicMock(side_effect=_next)
|
|
32
|
-
return session
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class TestMonthlyUsage:
|
|
36
|
-
@pytest.mark.asyncio
|
|
37
|
-
async def test_parses_monthly_entries(self):
|
|
38
|
-
usage_payload = make_webmethod_response({
|
|
39
|
-
"objUsageGenerationResultSetTwo": [
|
|
40
|
-
{"Month": 1, "Year": 2024, "UsageValue": 85.0, "FromDate": "12/15/23", "ToDate": "01/16/24"},
|
|
41
|
-
{"Month": 12, "Year": 2023, "UsageValue": 72.0, "FromDate": "11/14/23", "ToDate": "12/15/23"},
|
|
42
|
-
]
|
|
43
|
-
})
|
|
44
|
-
session = _setup_session([
|
|
45
|
-
make_response(), # GET login page
|
|
46
|
-
make_response(text=make_login_success()), # POST validateLogin
|
|
47
|
-
make_response(), # GET Dashboard
|
|
48
|
-
make_response(text=CSRF_HTML), # GET usage page
|
|
49
|
-
make_response(text=usage_payload), # POST LoadGasUsage
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
client = PGWApiClient("user", "pass")
|
|
53
|
-
result = await client.async_get_usage(session)
|
|
54
|
-
|
|
55
|
-
assert len(result) == 2
|
|
56
|
-
assert result[0].month == date(2024, 1, 1)
|
|
57
|
-
assert result[0].ccf == 85.0
|
|
58
|
-
assert result[1].month == date(2023, 12, 1)
|
|
59
|
-
|
|
60
|
-
@pytest.mark.asyncio
|
|
61
|
-
async def test_empty_usage(self):
|
|
62
|
-
usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
|
|
63
|
-
session = _setup_session([
|
|
64
|
-
make_response(),
|
|
65
|
-
make_response(text=make_login_success()),
|
|
66
|
-
make_response(),
|
|
67
|
-
make_response(text=CSRF_HTML),
|
|
68
|
-
make_response(text=usage_payload),
|
|
69
|
-
])
|
|
70
|
-
|
|
71
|
-
client = PGWApiClient("user", "pass")
|
|
72
|
-
result = await client.async_get_usage(session)
|
|
73
|
-
assert result == []
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class TestDailyUsage:
|
|
77
|
-
@pytest.mark.asyncio
|
|
78
|
-
async def test_parses_daily_entries(self):
|
|
79
|
-
usage_payload = make_webmethod_response({
|
|
80
|
-
"objUsageGenerationResultSetTwo": [
|
|
81
|
-
{"FromDate": "01/15/24", "UsageValue": 3.2},
|
|
82
|
-
{"FromDate": "01/14/24", "UsageValue": 4.1},
|
|
83
|
-
{"FromDate": "01/16/24", "UsageValue": 2.8},
|
|
84
|
-
]
|
|
85
|
-
})
|
|
86
|
-
session = _setup_session([
|
|
87
|
-
make_response(),
|
|
88
|
-
make_response(text=make_login_success()),
|
|
89
|
-
make_response(),
|
|
90
|
-
make_response(text=CSRF_HTML),
|
|
91
|
-
make_response(text=usage_payload),
|
|
92
|
-
])
|
|
93
|
-
|
|
94
|
-
client = PGWApiClient("user", "pass")
|
|
95
|
-
result = await client.async_get_daily_usage(
|
|
96
|
-
session, date(2024, 1, 14), date(2024, 1, 16)
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
assert len(result) == 3
|
|
100
|
-
assert result[0].date == date(2024, 1, 16)
|
|
101
|
-
assert result[0].ccf == 2.8
|
|
102
|
-
assert result[-1].date == date(2024, 1, 14)
|
|
103
|
-
|
|
104
|
-
@pytest.mark.asyncio
|
|
105
|
-
async def test_skips_entries_missing_date(self):
|
|
106
|
-
usage_payload = make_webmethod_response({
|
|
107
|
-
"objUsageGenerationResultSetTwo": [
|
|
108
|
-
{"FromDate": "01/15/24", "UsageValue": 3.2},
|
|
109
|
-
{"FromDate": None, "UsageValue": 4.1},
|
|
110
|
-
]
|
|
111
|
-
})
|
|
112
|
-
session = _setup_session([
|
|
113
|
-
make_response(),
|
|
114
|
-
make_response(text=make_login_success()),
|
|
115
|
-
make_response(),
|
|
116
|
-
make_response(text=CSRF_HTML),
|
|
117
|
-
make_response(text=usage_payload),
|
|
118
|
-
])
|
|
119
|
-
|
|
120
|
-
client = PGWApiClient("user", "pass")
|
|
121
|
-
result = await client.async_get_daily_usage(
|
|
122
|
-
session, date(2024, 1, 1), date(2024, 1, 31)
|
|
123
|
-
)
|
|
124
|
-
assert len(result) == 1
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class TestHourlyUsage:
|
|
128
|
-
@pytest.mark.asyncio
|
|
129
|
-
async def test_parses_hourly_entries(self):
|
|
130
|
-
usage_payload = make_webmethod_response({
|
|
131
|
-
"objUsageGenerationResultSetTwo": [
|
|
132
|
-
{"FromDate": "01/15/2024 01:00:00 AM", "UsageValue": 0.3},
|
|
133
|
-
{"FromDate": "01/15/2024 02:00:00 AM", "UsageValue": 0.4},
|
|
134
|
-
{"FromDate": "01/15/2024 12:00:00 PM", "UsageValue": 0.1},
|
|
135
|
-
]
|
|
136
|
-
})
|
|
137
|
-
session = _setup_session([
|
|
138
|
-
make_response(),
|
|
139
|
-
make_response(text=make_login_success()),
|
|
140
|
-
make_response(),
|
|
141
|
-
make_response(text=CSRF_HTML),
|
|
142
|
-
make_response(text=usage_payload),
|
|
143
|
-
])
|
|
144
|
-
|
|
145
|
-
client = PGWApiClient("user", "pass")
|
|
146
|
-
result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
|
|
147
|
-
|
|
148
|
-
assert len(result) == 3
|
|
149
|
-
assert result[0].timestamp == datetime(2024, 1, 15, 12, 0, 0)
|
|
150
|
-
assert result[0].ccf == 0.1
|
|
151
|
-
assert result[-1].timestamp == datetime(2024, 1, 15, 1, 0, 0)
|
|
152
|
-
|
|
153
|
-
@pytest.mark.asyncio
|
|
154
|
-
async def test_empty_hourly(self):
|
|
155
|
-
usage_payload = make_webmethod_response({"objUsageGenerationResultSetTwo": []})
|
|
156
|
-
session = _setup_session([
|
|
157
|
-
make_response(),
|
|
158
|
-
make_response(text=make_login_success()),
|
|
159
|
-
make_response(),
|
|
160
|
-
make_response(text=CSRF_HTML),
|
|
161
|
-
make_response(text=usage_payload),
|
|
162
|
-
])
|
|
163
|
-
|
|
164
|
-
client = PGWApiClient("user", "pass")
|
|
165
|
-
result = await client.async_get_hourly_usage(session, date(2024, 1, 15))
|
|
166
|
-
assert result == []
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class TestErrorHandling:
|
|
170
|
-
@pytest.mark.asyncio
|
|
171
|
-
async def test_csrf_error_raises_auth_error(self):
|
|
172
|
-
error_payload = make_webmethod_response({
|
|
173
|
-
"dtException": [{"MessageInformation": "Invalid CSRF Token"}]
|
|
174
|
-
})
|
|
175
|
-
session = _setup_session([
|
|
176
|
-
make_response(),
|
|
177
|
-
make_response(text=make_login_success()),
|
|
178
|
-
make_response(),
|
|
179
|
-
make_response(text=CSRF_HTML),
|
|
180
|
-
make_response(text=error_payload),
|
|
181
|
-
])
|
|
182
|
-
|
|
183
|
-
client = PGWApiClient("user", "pass")
|
|
184
|
-
with pytest.raises(PGWAuthError, match="CSRF"):
|
|
185
|
-
await client.async_get_usage(session)
|
|
186
|
-
|
|
187
|
-
@pytest.mark.asyncio
|
|
188
|
-
async def test_bad_credentials(self):
|
|
189
|
-
login_fail = make_webmethod_response(
|
|
190
|
-
[{"STATUS": 0, "Message": "Invalid username or password"}]
|
|
191
|
-
)
|
|
192
|
-
session = _setup_session([
|
|
193
|
-
make_response(),
|
|
194
|
-
make_response(text=login_fail),
|
|
195
|
-
])
|
|
196
|
-
|
|
197
|
-
client = PGWApiClient("user", "wrong")
|
|
198
|
-
with pytest.raises(PGWAuthError, match="Invalid username"):
|
|
199
|
-
await client.async_get_usage(session)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|