Skip to content

Commit d1636c2

Browse files
authored
[TOOLSLIBS-1751] OAuth Support (#205)
* 7.0 requirements * new clients * prevent ciricular imports * line length * adds oauth baseurl support * final oauth client * inherits from BaseClient class * typing changes for new clients * adds client docstrings, type fixes * adds types-mock * adds new client tests * conditional to prevent circular imports * exception specificity * moves urls to own file * corrects name * adds basic oauth client tests * adds client info to docs * adds types-oauthlib * ignores mypy type checking here * readds nose * moves mock * rm 3.6 test runner * uses super _request * add pyjwt and cryptography * use only jwt assertion with oauth * update docs index * updates black * add subject to self * updates tests * changes from review requests * test fix * private method override
1 parent 350034b commit d1636c2

39 files changed

+835
-200
lines changed

.github/workflows/test_runner.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-20.04
88
strategy:
99
matrix:
10-
python-version: ["3.6", "3.7", "3.8", "3.9"]
10+
python-version: ["3.7", "3.8", "3.9"]
1111

1212
steps:
1313
- uses: actions/checkout@v2

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v2.3.0
3+
rev: v4.6.0
44
hooks:
55
- id: check-yaml
66
- id: end-of-file-fixer
77
- id: trailing-whitespace
88
- id: no-commit-to-branch
99
args: ["--branch", "master", "--branch", "main"]
1010
- repo: https://github.com/psf/black
11-
rev: 21.12b0
11+
rev: 24.4.0
1212
hooks:
1313
- id: black
1414
- repo: https://github.com/codespell-project/codespell
15-
rev: v2.1.0
15+
rev: v2.2.6
1616
hooks:
1717
- id: codespell
1818
- repo: https://github.com/pre-commit/mirrors-mypy
19-
rev: v0.931
19+
rev: v1.9.0
2020
hooks:
2121
- id: mypy
2222
additional_dependencies: [types-requests, types-six, types-urllib3, types-mock]

docs/index.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import. To get started, import the package, and create an
2222
.. code-block:: python
2323
2424
import urbanairship as ua
25-
airship = ua.Airship('<app key>', '<master secret>')
25+
airship = ua.client.BasicAuthClient('<app key>', '<master secret>')
2626
2727
push = airship.Push(airship=airship)
2828
push.audience = ua.ios_channel('074e84a2-9ed9-4eee-9ca4-cc597bfdbef3')
@@ -35,6 +35,17 @@ providing connection pooling and strict SSL checking. The ``Airship``
3535
object is threadsafe, and can be instantiated once and reused in
3636
multiple threads.
3737

38+
Authentication Clients
39+
----------------------
40+
41+
The library supports authentication via 1 of 3 client classes:
42+
43+
* BasicAuthClient - This is the same as the deprecated `Airship` client class for using Key/Secret authentication.
44+
* BearerTokenClient - This client takes a `token` argument with an Airship-generated bearer token in addition to the key and other configuration options.
45+
* OAuthClient - This client requests an OAuth bearer token using the `client_id` and JWT assertion and automatically refreshes tokens as needed from the Airship OAuth2 provider. Please see the OAuth2 section of the Airship API documentation for more on this authentication method. If you prefer to handle token refresh yourself, the `access_token` returned from the Airship OAuth2 proivder can be used with the `BearerTokenClient`.
46+
47+
More about these methods, including examples of instantiation can be found on the Airship docs site.
48+
3849
EU Base URL
3950
-----------
4051

requirements-dev.txt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
requests>=1.2
2-
six
3-
backoff>=1.11
4-
nose
5-
mock
1+
-r requirements.txt
62
tox
73
black
84
pre-commit
9-
sphinx-rtd-theme

requirements.txt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
requests>=1.2
1+
requests==2.31.0
2+
backoff==2.2.1
23
types-requests
34
six
45
types-six
5-
nose
6-
mock
76
sphinx-rtd-theme
87
mypy
98
mypy-extensions
10-
backoff>=1.11
9+
nose
10+
mock
11+
types-mock
12+
pyjwt==2.8.0
13+
cryptography==42.0.5

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[nosetests]
22
with-doctest=1
3+
4+
[flake8]
5+
max-line-length = 99

tests/client/__init__.py

Whitespace-only changes.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import unittest
2+
3+
from tests import TEST_KEY, TEST_SECRET
4+
from urbanairship.client import BasicAuthClient
5+
6+
7+
class TestBasicClient(unittest.TestCase):
8+
def test_basic_client_timeout(self):
9+
timeout_int = 50
10+
11+
airship_timeout = BasicAuthClient(
12+
key=TEST_KEY, secret=TEST_SECRET, timeout=timeout_int
13+
)
14+
15+
self.assertEqual(airship_timeout.timeout, timeout_int)
16+
17+
def test_basic_client_timeout_exception(self):
18+
timeout_str = "50"
19+
20+
with self.assertRaises(ValueError):
21+
BasicAuthClient(key=TEST_KEY, secret=TEST_SECRET, timeout=timeout_str)
22+
23+
def test_basic_client_retry(self):
24+
retry_int = 5
25+
26+
airship_w_retry = BasicAuthClient(TEST_KEY, TEST_SECRET, retries=retry_int)
27+
28+
self.assertEqual(retry_int, airship_w_retry.retries)
29+
30+
def test_basic_client_location(self):
31+
location = "eu"
32+
33+
airship_eu = BasicAuthClient(
34+
key=TEST_KEY, secret=TEST_SECRET, location=location
35+
)
36+
37+
self.assertEqual(airship_eu.location, location)
38+
39+
def test_basic_client_location_exception(self):
40+
invalid_location = "xx"
41+
42+
with self.assertRaises(ValueError):
43+
BasicAuthClient(key=TEST_KEY, secret=TEST_SECRET, location=invalid_location)

tests/client/test_oauth_client.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import unittest
2+
3+
from tests import TEST_KEY, TEST_TOKEN
4+
from urbanairship.client import OAuthClient
5+
6+
7+
class TestOAuthClient(unittest.TestCase):
8+
def setUp(self) -> None:
9+
self.scope = ["nu"]
10+
self.ip_addr = ["24.20.40.0/24"]
11+
self.timeout = 50
12+
self.retries = 3
13+
self.private_key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
14+
15+
self.test_oauth_client = OAuthClient(
16+
client_id=TEST_KEY,
17+
private_key=self.private_key,
18+
key=TEST_KEY,
19+
scope=self.scope,
20+
ip_addr=self.ip_addr,
21+
timeout=self.timeout,
22+
retries=self.retries,
23+
)
24+
25+
def test_oauth_client_timeout(self):
26+
self.assertEqual(self.test_oauth_client.timeout, self.timeout)
27+
28+
def test_oauth_client_id(self):
29+
self.assertEqual(self.test_oauth_client.client_id, TEST_KEY)
30+
31+
def test_oauth_client_scope(self):
32+
self.assertEqual(self.test_oauth_client.scope, self.scope)
33+
34+
def test_oauth_client_ip_addr(self):
35+
self.assertEqual(self.test_oauth_client.ip_addr, self.ip_addr)
36+
37+
def test_oauth_client_retry(self):
38+
self.assertEqual(self.test_oauth_client.retries, self.retries)
39+
40+
def test_oauth_token_url(self):
41+
self.assertEqual(
42+
self.test_oauth_client.token_url, "https://oauth2.asnapius.com/token"
43+
)
44+
45+
def test_oauth_private_key(self):
46+
self.assertIn("-----BEGIN PRIVATE KEY-----", self.test_oauth_client.private_key)
47+
self.assertIn("-----END PRIVATE KEY-----", self.test_oauth_client.private_key)

tests/client/test_response.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import json
2+
import unittest
3+
import uuid
4+
5+
import mock
6+
import requests
7+
8+
import urbanairship as ua
9+
from tests import TEST_KEY, TEST_SECRET
10+
from urbanairship.client import BasicAuthClient
11+
12+
13+
class TestAirshipResponse(unittest.TestCase):
14+
test_channel = str(uuid.uuid4())
15+
airship = BasicAuthClient(TEST_KEY, TEST_SECRET, location="us")
16+
common_push = ua.Push(airship=airship)
17+
common_push.device_types = ua.device_types("ios")
18+
common_push.audience = ua.channel(test_channel)
19+
common_push.notification = ua.notification(alert="testing")
20+
21+
def test_unauthorized(self):
22+
with mock.patch.object(BasicAuthClient, "_request") as mock_request:
23+
response = requests.Response()
24+
response._content = json.dumps({"ok": False}).encode("utf-8")
25+
response.status_code = 401
26+
mock_request.return_value = response
27+
28+
try:
29+
self.common_push.send()
30+
except Exception as e:
31+
self.assertIsInstance(ua.Unauthorized, e)
32+
33+
def test_client_error(self):
34+
with mock.patch.object(BasicAuthClient, "_request") as mock_request:
35+
response = requests.Response()
36+
response._content = json.dumps({"ok": False}).encode("utf-8")
37+
response.status_code = 400
38+
mock_request.return_value = response
39+
40+
try:
41+
r = self.common_push.send()
42+
except Exception as e:
43+
self.assertIsInstance(ua.AirshipFailure, e)
44+
self.assertEqual(r.status_code, 400)
45+
46+
def test_server_error(self):
47+
with mock.patch.object(BasicAuthClient, "_request") as mock_request:
48+
response = requests.Response()
49+
response._content = json.dumps({"ok": False}).encode("utf-8")
50+
response.status_code = 500
51+
mock_request.return_value = response
52+
53+
try:
54+
r = self.common_push.send()
55+
except Exception as e:
56+
self.assertIsInstance(ua.AirshipFailure, e)
57+
self.assertEqual(r.status_code, 500)

0 commit comments

Comments
 (0)