Repository: mapbox/mapbox-sdk-py
Pull request #86 (2015-12-04 15:47:36)

Cover 100 quest
This test, once passing, should give us 100% coverage.

However, we're back to square one with finding a mock solution for s3 and boto.


Patch:
diff --git a/mapbox/services/base.py b/mapbox/services/base.py
index 4a25402..efd0eeb 100644
--- a/mapbox/services/base.py
+++ b/mapbox/services/base.py
@@ -1,6 +1,8 @@
 """Base Service class"""
 
 import os
+import base64
+import json
 
 import requests
 
@@ -27,6 +29,20 @@ class Service:
         """A product token for use in User-Agent headers."""
         return 'mapbox-sdk-py/{0}'.format(__version__)
 
+    @property
+    def username(self):
+        """Get username from access token
+        Token contains base64 encoded json object with username"""
+        token = self.session.params['access_token']
+        if not token:
+            raise ValueError("session does not have a valid access_token param")
+        data = token.split('.')[1]
+        data = data.replace('-', '+').replace('_', '/')
+        try:
+            return json.loads(base64.b64decode(data).decode('utf-8'))['u']
+        except (ValueError, KeyError):
+            raise ValueError("access_token does not contain username")
+
     def handle_http_error(self, response, custom_messages=None,
                           raise_for_status=False):
         if not custom_messages:
diff --git a/mapbox/services/uploads.py b/mapbox/services/uploads.py
index 47e2451..b103d8a 100644
--- a/mapbox/services/uploads.py
+++ b/mapbox/services/uploads.py
@@ -1,7 +1,7 @@
 # mapbox
+import boto3
 from uritemplate import URITemplate
 from .base import Service
-from boto3.session import Session
 
 
 class Uploader(Service):
@@ -11,7 +11,7 @@ class Uploader(Service):
 
         from mapbox import Uploader
 
-        u = Uploader('username')
+        u = Uploader()
         url = u.stage('test.tif')
         job = u.create(url, 'test1').json()
 
@@ -24,8 +24,7 @@ class Uploader(Service):
         assert job not in u.list().json()
     """
 
-    def __init__(self, username, access_token=None):
-        self.username = username
+    def __init__(self, access_token=None):
         self.baseuri = 'https://api.mapbox.com/uploads/v1'
         self.session = self.get_session(access_token)
 
@@ -40,7 +39,7 @@ class Uploader(Service):
             custom_messages={401: "Token does not have upload scope"})
         return resp
 
-    def stage(self, filepath, creds=None):
+    def stage(self, filepath, creds=None, session_class=boto3.session.Session):
         """Stages the user's file on S3
         If creds are not provided, temporary credientials will be generated
         Returns the URL to the staged resource.
@@ -49,10 +48,11 @@ class Uploader(Service):
             res = self._get_credentials()
             creds = res.json()
 
-        session = Session(aws_access_key_id=creds['accessKeyId'],
-                          aws_secret_access_key=creds['secretAccessKey'],
-                          aws_session_token=creds['sessionToken'],
-                          region_name="us-east-1")
+        session = session_class(
+            aws_access_key_id=creds['accessKeyId'],
+            aws_secret_access_key=creds['secretAccessKey'],
+            aws_session_token=creds['sessionToken'],
+            region_name="us-east-1")
 
         s3 = session.resource('s3')
         with open(filepath, 'rb') as data:


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index ff773d7..39a3311 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -1,3 +1,6 @@
+import base64
+import os
+
 import pytest
 import requests
 import responses
@@ -60,3 +63,36 @@ def test_custom_messages():
     with pytest.raises(requests.exceptions.HTTPError) as exc:
         assert service.handle_http_error(response, raise_for_status=True)
         assert "401" in exc.value.message
+
+
+class MockService(mapbox.Service):
+    def __init__(self, access_token=None):
+        # In order to get a username, a session must be created on init
+        self.session = self.get_session(access_token)
+
+def test_username(monkeypatch):
+    token = 'pk.{0}.test'.format(base64.b64encode(b'{"u":"testuser"}').decode('utf-8'))
+    service = MockService(access_token=token)
+    assert service.username == 'testuser'
+
+def test_username_failures(monkeypatch):
+    # If your child class doesn't create a session
+    service = mapbox.Service()
+    with pytest.raises(AttributeError) as exc:
+        service.username
+        assert 'session' in exc.value.message
+
+    if 'MAPBOX_ACCESS_TOKEN' in os.environ:
+        monkeypatch.delenv('MAPBOX_ACCESS_TOKEN')
+    service = MockService()
+    with pytest.raises(ValueError) as exc:
+        service.username
+        assert 'access_token' in exc.value.message
+        assert 'param' in exc.value.message
+
+    token = "not.good"
+    service = MockService(access_token=token)
+    with pytest.raises(ValueError) as exc:
+        service.username
+        assert 'access_token' in exc.value.message
+        assert 'username' in exc.value.message
diff --git a/tests/test_staticmaps.py b/tests/test_staticmaps.py
index ce672d1..046368b 100644
--- a/tests/test_staticmaps.py
+++ b/tests/test_staticmaps.py
@@ -97,3 +97,9 @@ def test_staticmap_auto_features(points):
 def test_staticmap_auto_nofeatures(points):
     with pytest.raises(ValueError):
         mapbox.Static(access_token='pk.test').image('mapbox.satellite')
+
+
+def test_staticmap_featurestoolarge(points):
+    with pytest.raises(ValueError):
+        mapbox.Static(access_token='pk.test').image(
+            'mapbox.satellite', -61.7, 12.1, 12, points * 100)
diff --git a/tests/test_upload.py b/tests/test_upload.py
index 252c540..3dd4815 100644
--- a/tests/test_upload.py
+++ b/tests/test_upload.py
@@ -1,4 +1,5 @@
 import json
+import base64
 
 import responses
 
@@ -6,6 +7,9 @@ import mapbox
 
 
 username = 'testuser'
+access_token = 'pk.{0}.test'.format(
+    base64.b64encode(b'{"u":"testuser"}').decode('utf-8'))
+
 upload_response_body = """
     {{"progress": 0,
     "modified": "date.test",
@@ -30,12 +34,12 @@ def test_get_credentials():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/uploads/v1/{0}/credentials?access_token=pk.test'.format(username),
+        'https://api.mapbox.com/uploads/v1/{0}/credentials?access_token={1}'.format(username, access_token),
         match_querystring=True,
         body=query_body, status=200,
         content_type='application/json')
 
-    res = mapbox.Uploader(username, access_token='pk.test')._get_credentials()
+    res = mapbox.Uploader(access_token=access_token)._get_credentials()
     assert res.status_code == 200
     creds = res.json()
     assert username in creds['url']
@@ -48,18 +52,18 @@ def test_get_credentials():
 def test_create():
     responses.add(
         responses.POST,
-        'https://api.mapbox.com/uploads/v1/{0}?access_token=pk.test'.format(username),
+        'https://api.mapbox.com/uploads/v1/{0}?access_token={1}'.format(username, access_token),
         match_querystring=True,
         body=upload_response_body, status=201,
         content_type='application/json')
 
-    res = mapbox.Uploader(username, access_token='pk.test').create(
+    res = mapbox.Uploader(access_token=access_token).create(
         'http://example.com/test.json', 'test1')  # without username prefix
     assert res.status_code == 201
     job = res.json()
     assert job['tileset'] == "{0}.test1".format(username)
 
-    res2 = mapbox.Uploader(username, access_token='pk.test').create(
+    res2 = mapbox.Uploader(access_token=access_token).create(
         'http://example.com/test.json', 'testuser.test1')  # also takes full tileset
     assert res2.status_code == 201
     job = res2.json()
@@ -86,11 +90,11 @@ def test_create_name():
 
     responses.add_callback(
         responses.POST,
-        'https://api.mapbox.com/uploads/v1/{0}?access_token=pk.test'.format(username),
+        'https://api.mapbox.com/uploads/v1/{0}?access_token={1}'.format(username, access_token),
         match_querystring=True,
         callback=request_callback)
 
-    res = mapbox.Uploader(username, access_token='pk.test').create(
+    res = mapbox.Uploader(access_token=access_token).create(
         'http://example.com/test.json', 'testuser.test1', name="testname")
     assert res.status_code == 201
     job = res.json()
@@ -101,12 +105,12 @@ def test_create_name():
 def test_list():
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/uploads/v1/{0}?access_token=pk.test'.format(username),
+        'https://api.mapbox.com/uploads/v1/{0}?access_token={1}'.format(username, access_token),
         match_querystring=True,
         body="[{0}]".format(upload_response_body), status=200,
         content_type='application/json')
 
-    res = mapbox.Uploader(username, access_token='pk.test').list()
+    res = mapbox.Uploader(access_token=access_token).list()
     assert res.status_code == 200
     uploads = res.json()
     assert len(uploads) == 1
@@ -118,12 +122,14 @@ def test_status():
     job = json.loads(upload_response_body)
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/uploads/v1/{0}/{1}?access_token=pk.test'.format(username, job['id']),
+        'https://api.mapbox.com/uploads/v1/{0}/{1}?access_token={2}'.format(username, job['id'], access_token),
         match_querystring=True,
         body=upload_response_body, status=200,
         content_type='application/json')
 
-    res = mapbox.Uploader(username, access_token='pk.test').status(job)
+    res = mapbox.Uploader(access_token=access_token).status(job)
+    assert res.status_code == 200
+    res = mapbox.Uploader(access_token=access_token).status(job['id'])
     assert res.status_code == 200
     status = res.json()
     assert job == status
@@ -134,10 +140,63 @@ def test_delete():
     job = json.loads(upload_response_body)
     responses.add(
         responses.DELETE,
-        'https://api.mapbox.com/uploads/v1/{0}/{1}?access_token=pk.test'.format(username, job['id']),
+        'https://api.mapbox.com/uploads/v1/{0}/{1}?access_token={2}'.format(username, job['id'], access_token),
         match_querystring=True,
         body=None, status=204,
         content_type='application/json')
 
-    res = mapbox.Uploader(username, access_token='pk.test').delete(job)
+    res = mapbox.Uploader(access_token=access_token).delete(job)
+    assert res.status_code == 204
+
+    res = mapbox.Uploader(access_token=access_token).delete(job['id'])
     assert res.status_code == 204
+
+
+class MockSession(object):
+    """ Mocks a boto3 session,
+    specifically for the purposes of an s3 key put
+    """
+    def __init__(self, *args, **kwargs):
+        self.bucket = None
+        self.key = None
+        pass
+
+    def resource(self, name):
+        self.resource_name = name
+        return self
+
+    def Object(self, bucket, key):
+        assert self.resource_name == 's3'
+        self.bucket = bucket
+        self.key = key
+        return self
+
+    def put(self, Body):
+        assert self.bucket
+        assert self.key
+        self.body = Body
+        return True
+
+
+@responses.activate
+def test_stage():
+    # Credentials
+    query_body = """
+       {{"key": "_pending/{username}/key.test",
+         "accessKeyId": "ak.test",
+         "bucket": "tilestream-tilesets-production",
+         "url": "https://tilestream-tilesets-production.s3.amazonaws.com/_pending/{username}/key.test",
+         "secretAccessKey": "sak.test",
+         "sessionToken": "st.test"}}""".format(username=username)
+    responses.add(
+        responses.GET,
+        'https://api.mapbox.com/uploads/v1/{0}/credentials?access_token={1}'.format(username, access_token),
+        match_querystring=True,
+        body=query_body, status=200,
+        content_type='application/json')
+
+    stage_url = mapbox.Uploader(access_token=access_token).stage(
+        'tests/moors.json',
+        session_class=MockSession)
+
+    assert stage_url.startswith("https://tilestream-tilesets-production.s3.amazonaws.com/_pending")