8000 error codes · james1ewis/firebase-admin-python@bad576c · GitHub
[go: up one dir, main page]

Skip to content

Commit bad576c

Browse files
committed
error codes
1 parent 467fcf5 commit bad576c

File tree

5 files changed

+102
-72
lines changed

5 files changed

+102
-72
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pytest integration/ --cert path/to/service_acct.json --apikey path/to/apikey.txt
179179

180180
### End-to-End Testing for Dynamic Links.
181181

182-
To run end-to-end tests for Firebase dynamic links follow these steps:
182+
To run end-to-end tests for Firebase Dynamic Links follow these steps:
183183

184184
1. From the [`Firebase Console`](https://firebase.corp.google.com/), create a short link in your project under "`Grow` > `Dynamic Links`".
185185
2. From your browser or phone, go to that short link and see that it redirects as desired.

firebase_admin/dynamic_links.py

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from firebase_admin import _utils
2626

2727

28-
2928
PLATFORM_DESKTOP = 'desktop'
3029
PLATFORM_IOS = 'ios'
3130
PLATFORM_ANDROID = 'android'
@@ -53,7 +52,19 @@
5352
_LINKS_ATTRIBUTE = '_dynamic_links'
5453
_LINKS_BASE_URL = 'https://firebasedynamiclinks.googleapis.com/v1/'
5554

55+
_INTERNAL_ERROR = 'internal-error'
5656
_UNKNOWN_ERROR = 'unknown-error'
57+
_AUTHENTICATION_ERROR = 'authentication-error'
58+
_BAD_REQUEST = 'invalid-argument'
59+
60+
_error_codes = {
61+
400: _BAD_REQUEST,
62+
401: _AUTHENTICATION_ERROR,
63+
403: _AUTHENTICATION_ERROR,
64+
500: _INTERNAL_ERROR,
65+
}
66+
67+
5768
def get_link_stats(short_link, stat_options, app=None):
5869
""" Returns a ``LinkStats`` object with the event statistics for the given short link
5970
@@ -70,13 +81,14 @@ def get_link_stats(short_link, stat_options, app=None):
7081
7182
Raises:
7283
ValueError: If any of the arguments are invalid.
73-
short_link must start with the "https" protocol.
74-
stat_options should have duration_days > 0.
84+
``short_link`` must start with the "https" protocol.
85+
``stat_options`` must have duration_days > 0.
7586
"""
7687
return _get_link_service(app).get_stats(short_link, stat_options)
7788

89+
7890
def _get_link_service(app):
79-
"""Returns an _DynamicLinksService instance for an App.
91+
"""Returns a _DynamicLinksService instance for an App.
8092
8193
If the App already has a _DynamicLinksService associated with it, simply returns
8294
it. Otherwise creates a new _DynamicLinksService, and adds it to the App before
@@ -97,13 +109,14 @@ def _get_link_service(app):
97109
class LinkStats(object):
98110
"""The ``LinkStats`` object is returned by get_link_stats, it contains a list of
99111
``EventStats``"""
112+
100113
def __init__(self, event_stats):
101114
if not isinstance(event_stats, (list, tuple)):
102115
raise ValueError('Invalid data argument: {0}. Must be a list or tuple'
103116
.format(event_stats))
104117
if not all(isinstance(es, EventStats) for es in event_stats):
105118
raise ValueError('Invalid data argument: elements of event stats must be' +
106-
' "EventStats", found{}'.format(type(event_stats[0])))
119+
' "EventStats", found "{}"'.format(type(event_stats[0])))
107120
self._stats = event_stats
108121

109122
@property
@@ -115,40 +128,42 @@ def event_stats(self):
115128
"""
116129
return self._stats
117130

131+
118132
class EventStats(object):
119-
"""``EventStat`` is a single stat item containing (platform, event, count)"""
133+
"""``EventStat`` is a single stat item containing (platform, event, count)
120134
121-
def __init__(self, **kwargs):
122-
"""Create new instance of EventStats(platform, event, count)
123-
The input values are the strings returned by the REST call.
124-
The internal values stored in the ``EventStats`` object are
125-
the package constants named at the start of this package."""
126-
required = {'platform', 'event', 'count'}
127-
params = set(kwargs.keys())
128-
missing = required - params
129-
unexpected = params - required
130-
if missing:
131-
raise ValueError('Missing arguments for EventStats: {}'.format(missing))
132-
if unexpected:
133-
raise ValueError('Unexpected arguments for EventStats: {}'.format(unexpected))
134-
135-
platform = kwargs['platform']
136-
if not isinstance(platform, six.string_types) or platform not in _platforms.keys():
137-
raise ValueError('Invalid Platform value "{}".'.format(platform))
138-
self._platform = _platforms[platform]
135+
The constructor input values are the strings returned by the REST call.
136+
e.g. "ANDROID", or "APP_RE_OPEN". See the Dynamic Links `API docs`_ .
137+
The internal values stored in the ``EventStats`` object are the package
138+
constants named at the start of this package.
139139
140-
event = kwargs['event']
141-
if not isinstance(event, six.string_types) or event not in _event_types.keys():
142-
raise ValueError('Invalid Event Type value "{}".'.format(event))
143-
self._event = _event_types[event]
140+
.. _API docs https://firebase.google.com/docs/reference/dynamic-links/analytics
141+
"""
144142

145-
count = kwargs['count']
143+
def __init__(self, **kwargs):
144+
platform = kwargs.pop('platform', None)
145+
event = kwargs.pop('event', None)
146+
count = kwargs.pop('count', None)
147+
148+
if not isinstance(platform, six.string_types) or platform not in _platforms:
149+
raise ValueError(
150+
'Invalid Platform argument value "{}".'.format(platform))
151+
if not isinstance(event, six.string_types) or event not in _event_types:
152+
raise ValueError(
153+
'Invalid Event Type argument value "{}".'.format(event))
146154
if(not ((isinstance(count, six.string_types) # a string
147155
and count.isdigit()) # ... that is made of digits(non negative)
148156
or (not isinstance(count, bool) # bool is confused as an instance of int
149-
and isinstance(count, (int, float)) # number
157+
and isinstance(count, (int, float)) # number
150158
and count >= 0))): # non negative
151-
raise ValueError('Invalid Count, must be a non negative int, "{}".'.format(count))
159+
raise ValueError('Invalid Count argument value, must be a non negative int, "{}".'
160+
.format(count))
161+
if kwargs:
162+
raise ValueError(
163+
'Unexpected arguments for EventStats: {}'.format(kwargs))
164+
165+
self._platform = _platforms[platform]
166+
self._event = _event_types[event]
152167
self._count = int(count)
153168

154169
@property
@@ -181,8 +196,6 @@ def duration_days(self):
181196
class _DynamicLinksService(object):
182197
"""Provides methods for the Firebase dynamic links interaction"""
183198

184-
INTERNAL_ERROR = 'internal-error'
185-
186199
def __init__(self, app):
187200
self._client = _http_client.JsonHttpClient(
188201
credential=app.credential.get_credential(),
@@ -193,20 +206,22 @@ def __init__(self, app):
193206
def _format_request_string(self, short_link, options):
194207
days = options.duration_days
195208
# Complaints about the named second argument needed to replace "/"
196-
url_quoted = urllib.parse.quote(short_link, safe='') #pylint: disable=redundant-keyword-arg
209+
url_quoted = urllib.parse.quote(short_link, safe='') # pylint: disable=redundant-keyword-arg
197210
return self._request_string.format(url_quoted, days)
198211

199212
def get_stats(self, short_link, stat_options):
200213
"""Returns the LinkStats of the requested short_link for the duration set in options"""
201214
if(not isinstance(short_link, six.string_types)
202215
or not short_link.startswith('https://')):
203-
raise ValueError('short_link must be a string and begin with "https://".')
216+
raise ValueError(
217+
'short_link must be a string and begin with "https://".')
204218
if not isinstance(stat_options, StatOptions):
205219
raise ValueError('stat_options must be of type StatOptions.')
206220

207221
request_string = self._format_request_string(short_link, stat_options)
208222
try:
209-
resp = self._client.body('get', request_string, timeout=self._timeout)
223+
resp = self._client.body(
224+
'get', request_string, timeout=self._timeout)
210225
except requests.exceptions.RequestException as error:
211226
self._handle_error(error)
212227
else:
@@ -218,7 +233,7 @@ def _handle_error(self, error):
218233
"""Error handler for dynamic links request errors"""
219234
if error.response is None:
220235
msg = 'Failed to call dynamic links API: {0}'.format(error)
221-
raise ApiCallError(self.INTERNAL_ERROR, msg, error)
236+
raise ApiCallError(_INTERNAL_ERROR, msg, error)
222237
data = {}
223238
try:
224239
parsed_body = error.response.json()
@@ -227,12 +242,13 @@ def _handle_error(self, error):
227242
except ValueError:
228243
pass
229244
error_details = data.get('error', {})
230-
code = error_details.get('code', _UNKNOWN_ERROR)
245+
code = error_details.get('code')
246+
code_str = _error_codes.get(code, _UNKNOWN_ERROR)
231247
msg = error_details.get('message')
232248
if not msg:
233249
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(
234250
error.response.status_code, error.response.content.decode())
235-
raise ApiCallError(code, msg, error)
251+
raise ApiCallError(code_str, msg, error)
236252

237253

238254
class ApiCallError(Exception):

integration/test_dynamic_links.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,16 @@
1818
import pytest
1919

2020
from firebase_admin import dynamic_links
21-
2221
from tests import testutils
2322

2423
dynamic_links_e2e_url = ''
2524
try:
2625
dynamic_links_e2e_url = testutils.resource('dynamic_links_e2e_url.txt').strip()
2726
except IOError:
28-
sys.stderr.write("\nEnd-to-end tests not set up, see CONTRIBUTING.md file.\n")
27+
sys.stderr.write('\nEnd-to-end tests not set up, see CONTRIBUTING.md file.\n')
2928

3029
@pytest.mark.skipif(not dynamic_links_e2e_url,
31-
reason="End-to-end tests not set up, see CONTRIBTING.md file.")
30+
reason='End-to-end tests not set up, see CONTRIBTING.md file.')
3231
class TestEndToEnd(object):
3332
"""Runs an end-to-end test, see comment string for setup."""
3433

@@ -52,14 +51,26 @@ def test_unauthorized(self):
5251
dynamic_links.get_link_stats(
5352
'https://fake1.app.goo.gl/uQWc',
5453
dynamic_links.StatOptions(duration_days=4000))
55-
assert excinfo.value.code == 403
54+
assert excinfo.value.code == 'authentication-error'
55+
print(excinfo.value, dir(excinfo.value))
56+
57+
print(excinfo.value, dir(excinfo.value), "\nARGS: ", excinfo.value.args,
58+
"\nCODE : ", excinfo.value.code,
59+
"\nDETAIL",
60+
excinfo.value.detail, "\n\nh:",dir(excinfo.value.detail))
5661

5762
@pytest.mark.skipif(not dynamic_links_e2e_url,
58-
reason="End-to-end tests not set up, see CONTRIBTING.md file.")
63+
reason='End-to-end tests not set up, see CONTRIBTING.md file.')
5964
def test_bad_request(self):
6065
with pytest.raises(dynamic_links.ApiCallError) as excinfo:
6166
dynamic_links.get_link_stats(
62-
dynamic_links_e2e_url+"/too/many/slashes/in/shortlink",
67+
dynamic_links_e2e_url + '/too/many/slashes/in/shortlink',
6368
dynamic_links.StatOptions(duration_days=4000))
64-
assert excinfo.value.code == 400
69+
assert excinfo.value.code == 'invalid-argument'
6570
assert 'Request contains an invalid argument' in str(excinfo.value)
71+
72+
print(excinfo.value, dir(excinfo.value), "\nARGS: ", excinfo.value.args,
73+
"\nCODE : ", excinfo.value.code,
74+
"\nDETAIL",
75+
excinfo.value.detail, "\n\nh:",dir(excinfo.value.detail))
76+

scripts/cert2.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "service_account",
3+
"project_id": "avishalom-testremote",
4+
"private_key_id": "0aef32b946eac1d2fd4c4151c06b8c25b3d088b4",
5+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaSwZnhitWQSie\nO1e263H9KaG2EWeXBODdQ6UPT8PEdjnVMZznbb5KNIgW0y85wNS5EA7J6CWuM86u\n4p4cG6Gy0NopaItkqgaGWNhDD7FI1Hwois0ZbElwK9G5TEvddm0iUWuNJCv6nqYi\nYKrubsNUzhzuBzFhvAztqMT+mipIyN0sfWj3XdBOuf5pSANrhTgMdxPeeOwhJvQn\n9NXUq4bW+y7/VyflwRjHiztTZhGDkCC5VeXtV0Kx0br6UYanuEgMAdtJV5u3Eu3h\nmYQ/4K0MJmHFwcThSCmBuuNINTJw/YPJRKRQ6yJiW9vVCdDmmTk9R3EgG+sE/BeJ\nCk5kZewRAgMBAAECggEAPw0otVZ9UDa5YAwVGLGXd+Ky7EGpiOvb7l+tzJeGgzrj\nAE3RdjuBFzbnZBhyBJJZ+7RlrrnY6BrQu+Psw8TA699wP5qy1/SpTO5yldkMVBxN\nDo5GKTz4t8eYYTYeDIbQK0WFg3yEIlsBviutvljyJq5B4T18PxnHWLluozKh0/Lq\nbW1MdPuUmRsgNT/RHhehcNaBDP3qwuiXyOqDv4PgMUYgG93IvhgU/b/cPR/1d5Uk\nqhnd2c2UsiBSa7RuAB3ZYBwtS0N9e9YpxDIzxFKXQ4hPV0zzU3dQq1y5PGjyzlw5\nUYC0W/cNJO4DlTygDwjVOrBPsyoBOj3LEOrtpzTpvwKBgQDY4BVMYbZW64/jT9cw\ngzP4+6x9DjUqf4oU4RhQ/XtbDtNu5et7tBDGlKKTqftqOIiMDHsoAsL7odDfNdUG\nAYehAnjT1eI8MmZnm4qUhJNZdk1gzP/EgqJgnc8Qfs2aVHBWxw28S2EqEpJ8RcTv\nh3spwC4eeI744OzLl8tHQO8qAwKBgQC2ILdCi3lC3eNkL/wo1mgRWJG3EKmxtTvT\nAM+2mUxc4lkllK2UbCSJX8PbuWDDlQJZhw77OthtZXCofmDjzq2NmMdaiD8Jz4Nw\njDrl3KZAZRF22uI34iqR7OD7k+qbBO0wFtZjJbvlDQCVwrjoUIRwGrqBStPwvPZ0\nf52Wh4P/WwKBgQDJFXQJ/ytOilk+XT/b1jrxJ4D17p9zCRAhbdfa+DxQ4H0//OSL\nLRjE1vmqyle3VDWfDM9/+JeLMqz02Pfr+q0jp6QaXdzHDcAPXpNuQ0JQF7WFBG49\naRZfWNKtq7S83H/Qpf1hUc8EcHXrzdDcepTC0FKyo/uEXSCRYOv05AscuwKBgQCs\nxg5zn1JSn6ImmerRZsxkoCvjiXghsDbnbV6e74BkoQlGwGuGYKyscV+g4pQsFgCb\no6cPp3xHEzMzdGg/1AIKUIPVm2iJywT60NzS0GYasoZFxVFTCEewFRI9Ns4Zbyv/\nMbsBZXuAx7vlVksJf9CTdJe3LaAvQWNfkuyRL0+F/wKBgE8HHLHc8QpvkWSeRqZM\nawfndJUZVK3M6RhnH+jgg9cftGU/SMerEEwCsaeSbTGQGrBkbL1D7/mxyuzlsKuu\nbkL73R6kvAXHF3Pcdm9pCIRSfrIdsS4Vlz5tF9zp+Uz+R4JKaGPhRjOdbUgi8BWB\nhww6bkvkVU/CIlPYhCgfFlWS\n-----END PRIVATE KEY-----\n",
6+
"client_email": "firebase-adminsdk-33hi6@avishalom-testremote.iam.gserviceaccount.com",
7+
"client_id": "109983970186096734463",
8+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9+
"token_uri": "https://accounts.google.com/o/oauth2/token",
10+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-33hi6%40avishalom-testremote.iam.gserviceaccount.com"
12+
}

tests/test_dynamic_links.py

Lines changed: 16 additions & 25 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
"""Test cases for the firebase_admin.dynamic_links module."""
1717

1818
import json
19-
from itertools import product
20-
import pytest
19+
import itertools
2120

22-
from tests import testutils
21+
import pytest
2322

2423
import firebase_admin
2524
from firebase_admin import dynamic_links
2625
from firebase_admin import credentials
26+
from tests import testutils
2727

2828

2929
MOCK_SHORT_URL = 'https://fake1.app.goo.gl/uQWc'
@@ -81,9 +81,9 @@ def test_get_stats(self, dynamic_links_test):
8181
options = dynamic_links.StatOptions(duration_days=9)
8282
link_stats = dynamic_links.get_link_stats(
8383
MOCK_SHORT_URL, options, app=dynamic_links_test.app)
84-
assert recorder[0].url.startswith("https://firebasedynamiclinks.googleapis.com")
84+
assert recorder[0].url.startswith('https://firebasedynamiclinks.googleapis.com')
8585
assert (recorder[0].path_url ==
86-
"/v1/https%3A%2F%2Ffake1.app.goo.gl%2FuQWc/linkStats?durationDays=9")
86+
'/v1/https%3A%2F%2Ffake1.app.goo.gl%2FuQWc/linkStats?durationDays=9')
8787
assert isinstance(link_stats, dynamic_links.LinkStats)
8888
for event_stat in link_stats.event_stats:
8989
assert isinstance(event_stat, dynamic_links.EventStats)
@@ -94,18 +94,6 @@ def test_get_stats(self, dynamic_links_test):
9494
assert returned.event == direct['event']
9595
assert returned.count == direct['count']
9696

97-
def test_get_stats_error(self, dynamic_links_test):
98-
err_body = json.dumps({'error': {
99-
'status': 'INTERNAL_ERROR',
100-
'message': 'test_error',
101-
'code': 500}})
102-
dynamic_links_test._instrument_dynamic_links(payload=err_body,
103-
status=500)
104-
options = dynamic_links.StatOptions(duration_days=9)
105-
with pytest.raises(dynamic_links.ApiCallError) as excinfo:
106-
dynamic_links.get_link_stats(MOCK_SHORT_URL, options, app=dynamic_links_test.app)
107-
assert excinfo.value.code == 500
108-
10997
@pytest.mark.parametrize('error_code', [400, 401, 500])
11098
def test_server_error(self, dynamic_links_test, error_code):
11199
options = dynamic_links.StatOptions(duration_days=9)
@@ -119,6 +107,7 @@ def test_server_error(self, dynamic_links_test, error_code):
119107
dynamic_links.get_link_stats(
120108
MOCK_SHORT_URL, options, app=dynamic_links_test.app)
121109
assert 'json_test_error' in str(excinfo.value)
110+
assert excinfo.value.code == dynamic_links._error_codes[error_code]
122111

123112
@pytest.mark.parametrize('error_code', [400, 401, 500])
124113
def test_server_unformatted_error(self, dynamic_links_test, error_code):
@@ -130,8 +119,9 @@ def test_server_unformatted_error(self, dynamic_links_test, error_code):
130119
dynamic_links.get_link_stats(
131120
MOCK_SHORT_URL, options, app=dynamic_links_test.app)
132121
assert 'custom error message' in str(excinfo.value)
122+
assert excinfo.value.code == dynamic_links._UNKNOWN_ERROR
133123

134-
def test_server_none_error(self, dynamic_links_test):
124+
def test_server_non_payload_error(self, dynamic_links_test):
135125
options = dynamic_links.StatOptions(duration_days=9)
136126
dynamic_links_test._instrument_dynamic_links(
137127
payload='',
@@ -140,6 +130,7 @@ def test_server_none_error(self, dynamic_links_test):
140130
dynamic_links.get_link_stats(
141131
MOCK_SHORT_URL, options, app=dynamic_links_test.app)
142132
assert 'Unexpected HTTP response' in str(excinfo.value)
133+
assert excinfo.value.code == dynamic_links._UNKNOWN_ERROR
143134

144135

145136
@pytest.mark.parametrize('invalid_url', ['google.com'] + INVALID_STRINGS)
@@ -148,14 +139,14 @@ def test_get_stats_invalid_url(self, dynamic_links_test, invalid_url):
148139
with pytest.raises(ValueError) as excinfo:
149140
dynamic_links.get_link_stats(invalid_url, options, app=dynamic_links_test.app)
150141
assert 'short_link must be a string and begin with "https://".' in str(excinfo.value)
151-
142+
152143
@pytest.mark.parametrize('invalid_options', INVALID_STRINGS)
153144
def test_get_stats_invalid_options(self, dynamic_links_test, invalid_options):
154145
with pytest.raises(ValueError) as excinfo:
155146
dynamic_links.get_link_stats(
156147
MOCK_SHORT_URL, invalid_options, app=dynamic_links_test.app)
157148
assert 'stat_options must be of type StatOptions.' in str(excinfo.value)
158-
149+
159150
@pytest.mark.parametrize('invalid_duration', [0] + INVALID_NON_NEGATIVE_NUMS)
160151
def test_get_stats_invalid_duration_days(self, invalid_duration):
161152
with pytest.raises(ValueError) as excinfo:
@@ -166,8 +157,8 @@ def test_get_stats_invalid_duration_days(self, invalid_duration):
166157

167158
class TestEventStats(object):
168159
@pytest.mark.parametrize('platform, event',
169-
product(dynamic_links._platforms.keys(),
170-
dynamic_links._event_types.keys()))
160+
itertools.product(dynamic_links._platforms.keys(),
161+
dynamic_links._event_types.keys()))
171162
def test_valid_platform_values(self, platform, event):
172163
event_stats = dynamic_links.EventStats(
173164
platform=platform,
@@ -184,7 +175,7 @@ def test_invalid_platform_values(self, arg):
184175
platform=arg,
185176
event='CLICK',
186177
count='1')
187-
assert 'Invalid Platform value' in str(excinfo.value)
178+
assert 'Invalid Platform argument value' in str(excinfo.value)
188179

189180
@pytest.mark.parametrize('event', dynamic_links._event_types.keys())
190181
def test_valid_event_values(self, event):
@@ -201,7 +192,7 @@ def test_invalid_event_values(self, arg):
201192
platform='ANDROID',
202193
event=arg,
203194
count='1')
204-
assert 'Invalid Event Type value' in str(excinfo.value)
195+
assert 'Invalid Event Type argument value' in str(excinfo.value)
205196

206197
@pytest.mark.parametrize('count', [1, 123, 1234])
207198
def test_valid_count_values(self, count):
@@ -226,7 +217,7 @@ class TestLinkStats(object):
226217
def test_invalid_event_stats_list(self, arg):
227218
with pytest.raises(ValueError) as excinfo:
228219
dynamic_links.LinkStats(arg)
229-
assert'Must be a list or tuple' in str(excinfo.value)
220+
assert 'Must be a list or tuple' in str(excinfo.value)
230221

231222
@pytest.mark.parametrize('arg', [list([1, 2]), list('asdf'), tuple([1, 2])])
232223
def test_empty_event_stats_list(self, arg):

0 commit comments

Comments
 (0)
0