Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add managements methods for web apps #474

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
184 changes: 157 additions & 27 deletions firebase_admin/project_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ def ios_app(app_id, app=None):
return IOSApp(app_id=app_id, service=_get_project_management_service(app))


def web_app(app_id, app=None):
"""Obtains a reference to a Web app in the associated Firebase project.

Args:
app_id: The app ID that identifies this Web app.
app: An App instance (optional).

Returns:
WebApp: An ``WebApp`` instance.
"""
return WebApp(app_id=app_id, service=_get_project_management_service(app))


def list_android_apps(app=None):
"""Lists all Android apps in the associated Firebase project.

Expand All @@ -87,6 +100,19 @@ def list_ios_apps(app=None):
return _get_project_management_service(app).list_ios_apps()


def list_web_apps(app=None):
"""Lists all Web apps in the associated Firebase project.

Args:
app: An App instance (optional).

Returns:
list: a list of ``WebApp`` instances referring to each Web app in the Firebase
project.
"""
return _get_project_management_service(app).list_web_apps()


def create_android_app(package_name, display_name=None, app=None):
"""Creates a new Android app in the associated Firebase project.

Expand Down Expand Up @@ -115,6 +141,19 @@ def create_ios_app(bundle_id, display_name=None, app=None):
return _get_project_management_service(app).create_ios_app(bundle_id, display_name)


def create_web_app(display_name=None, app=None):
"""Creates a new Web app in the associated Firebase project.

Args:
display_name: A nickname for this Web app (optional).
app: An App instance (optional).

Returns:
WebApp: An ``WebApp`` instance that is a reference to the newly created app.
"""
return _get_project_management_service(app).create_web_app(display_name)


def _check_is_string_or_none(obj, field_name):
if obj is None or isinstance(obj, str):
return obj
Expand Down Expand Up @@ -293,6 +332,61 @@ def get_config(self):
return self._service.get_ios_app_config(self._app_id)


class WebApp:
"""A reference to a Web app within a Firebase project.

Note: Unless otherwise specified, all methods defined in this class make an RPC.

Please use the module-level function ``web_app(app_id)`` to obtain instances of this class
instead of instantiating it directly.
"""
def __init__(self, app_id, service):
self._app_id = app_id
self._service = service

@property
def app_id(self):
"""Returns the app ID of the Web app to which this instance refers.

Note: This method does not make an RPC.

Returns:
string: The app ID of the Web app to which this instance refers.
"""
return self._app_id

def get_metadata(self):
"""Retrieves detailed information about this Web app.

Returns:
WebAppMetadata: An ``WebAppMetadata`` instance.

Raises:
FirebaseError: If an error occurs while communicating with the Firebase Project
Management Service.
"""
return self._service.get_web_app_metadata(self._app_id)

def set_display_name(self, new_display_name):
"""Updates the display name attribute of this Web app to the one given.

Args:
new_display_name: The new display name for this Web app.

Returns:
NoneType: None.

Raises:
FirebaseError: If an error occurs while communicating with the Firebase Project
Management Service.
"""
return self._service.set_web_app_display_name(self._app_id, new_display_name)

def get_config(self):
"""Retrieves the configuration artifact associated with this Web app."""
return self._service.get_web_app_config(self._app_id)


class _AppMetadata:
"""Detailed information about a Firebase Android or iOS app."""

Expand Down Expand Up @@ -332,6 +426,9 @@ def __eq__(self, other):
self.display_name == other.display_name and self.project_id == other.project_id)
# pylint: enable=protected-access

def __ne__(self, other):
return not self.__eq__(other)


class AndroidAppMetadata(_AppMetadata):
"""Android-specific information about an Android Firebase app."""
Expand All @@ -350,9 +447,6 @@ def __eq__(self, other):
return (super(AndroidAppMetadata, self).__eq__(other) and
self.package_name == other.package_name)

def __ne__(self, other):
return not self.__eq__(other)

def __hash__(self):
return hash(
(self._name, self.app_id, self.display_name, self.project_id, self.package_name))
Expand All @@ -374,13 +468,16 @@ def bundle_id(self):
def __eq__(self, other):
return super(IOSAppMetadata, self).__eq__(other) and self.bundle_id == other.bundle_id

def __ne__(self, other):
return not self.__eq__(other)

def __hash__(self):
return hash((self._name, self.app_id, self.display_name, self.project_id, self.bundle_id))


class WebAppMetadata(_AppMetadata):

def __hash__(self):
return hash((self._name, self.app_id, self.display_name, self.project_id))


class SHACertificate:
"""Represents a SHA-1 or SHA-256 certificate associated with an Android app."""

Expand Down Expand Up @@ -468,6 +565,7 @@ class _ProjectManagementService:
ANDROID_APP_IDENTIFIER_NAME = 'packageName'
IOS_APPS_RESOURCE_NAME = 'iosApps'
IOS_APP_IDENTIFIER_NAME = 'bundleId'
WEB_APPS_RESOURCE_NAME = 'webApps'

def __init__(self, app):
project_id = app.project_id
Expand Down Expand Up @@ -499,13 +597,25 @@ def get_ios_app_metadata(self, app_id):
metadata_class=IOSAppMetadata,
app_id=app_id)

def _get_app_metadata(self, platform_resource_name, identifier_name, metadata_class, app_id):
"""Retrieves detailed information about an Android or iOS app."""
def get_web_app_metadata(self, app_id):
return self._get_app_metadata(
platform_resource_name=_ProjectManagementService.WEB_APPS_RESOURCE_NAME,
metadata_class=WebAppMetadata,
app_id=app_id)

def _get_app_metadata(
self,
platform_resource_name,
metadata_class,
app_id,
identifier_name=None):
"""Retrieves detailed information about a Firebase app."""
_check_is_nonempty_string(app_id, 'app_id')
path = '/v1beta1/projects/-/{0}/{1}'.format(platform_resource_name, app_id)
response = self._make_request('get', path)
args = [] if not identifier_name else [response[identifier_name]]
return metadata_class(
response[identifier_name],
*args,
name=response['name'],
app_id=response['appId'],
display_name=response.get('displayName') or None,
Expand All @@ -523,6 +633,12 @@ def set_ios_app_display_name(self, app_id, new_display_name):
new_display_name=new_display_name,
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME)

def set_web_app_display_name(self, app_id, new_display_name):
self._set_display_name(
app_id=app_id,
new_display_name=new_display_name,
platform_resource_name=_ProjectManagementService.WEB_APPS_RESOURCE_NAME)

def _set_display_name(self, app_id, new_display_name, platform_resource_name):
"""Sets the display name of an Android or iOS app."""
path = '/v1beta1/projects/-/{0}/{1}?updateMask=displayName'.format(
Expand All @@ -540,6 +656,11 @@ def list_ios_apps(self):
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME,
app_class=IOSApp)

def list_web_apps(self):
return self._list_apps(
platform_resource_name=_ProjectManagementService.WEB_APPS_RESOURCE_NAME,
app_class=WebApp)

def _list_apps(self, platform_resource_name, app_class):
"""Lists all the Android or iOS apps within the Firebase project."""
path = '/v1beta1/projects/{0}/{1}?pageSize={2}'.format(
Expand Down Expand Up @@ -568,30 +689,28 @@ def _list_apps(self, platform_resource_name, app_class):
def create_android_app(self, package_name, display_name=None):
return self._create_app(
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME,
identifier_name=_ProjectManagementService.ANDROID_APP_IDENTIFIER_NAME,
identifier=package_name,
display_name=display_name,
app_class=AndroidApp)
app_class=AndroidApp,
identifier={_ProjectManagementService.ANDROID_APP_IDENTIFIER_NAME: package_name})

def create_ios_app(self, bundle_id, display_name=None):
return self._create_app(
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME,
identifier_name=_ProjectManagementService.IOS_APP_IDENTIFIER_NAME,
identifier=bundle_id,
display_name=display_name,
app_class=IOSApp)
app_class=IOSApp,
identifier={_ProjectManagementService.IOS_APP_IDENTIFIER_NAME: bundle_id})

def _create_app(
self,
platform_resource_name,
identifier_name,
identifier,
display_name,
app_class):
"""Creates an Android or iOS app."""
def create_web_app(self, display_name=None):
return self._create_app(
platform_resource_name=_ProjectManagementService.WEB_APPS_RESOURCE_NAME,
display_name=display_name,
app_class=WebApp)

def _create_app(self, platform_resource_name, display_name, app_class, identifier=None):
"""Creates a Firebase app."""
_check_is_string_or_none(display_name, 'display_name')
path = '/v1beta1/projects/{0}/{1}'.format(self._project_id, platform_resource_name)
request_body = {identifier_name: identifier}
request_body = identifier or {}
if display_name:
request_body['displayName'] = display_name
response = self._make_request('post', path, json=request_body)
Expand Down Expand Up @@ -628,12 +747,23 @@ def get_ios_app_config(self, app_id):
return self._get_app_config(
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME, app_id=app_id)

def get_web_app_config(self, app_id):
return self._get_app_config(
platform_resource_name=_ProjectManagementService.WEB_APPS_RESOURCE_NAME, app_id=app_id)

def _get_app_config(self, platform_resource_name, app_id):
"""Fetches Firebase app specific configuration"""
path = '/v1beta1/projects/-/{0}/{1}/config'.format(platform_resource_name, app_id)
response = self._make_request('get', path)
# In Python 2.7, the base64 module works with strings, while in Python 3, it works with
# bytes objects. This line works in both versions.
return base64.standard_b64decode(response['configFileContents']).decode(encoding='utf-8')
try:
config_file_contents = response['configFileContents']
except KeyError:
# Web apps return a plain dict
return response
else:
# In Python 2.7, the base64 module works with strings, while in Python 3, it works with
# bytes objects. This line works in both versions.
return base64.standard_b64decode(config_file_contents).decode(encoding='utf-8')

def get_sha_certificates(self, app_id):
path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id)
Expand Down
42 changes: 42 additions & 0 deletions integration/test_project_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ def ios_app(default_app):
bundle_id=TEST_APP_BUNDLE_ID, display_name=TEST_APP_DISPLAY_NAME_PREFIX)


@pytest.fixture(scope='module')
def web_app(default_app):
del default_app
web_apps = project_management.list_web_apps()
for web_app in web_apps:
if _starts_with(web_app.get_metadata().display_name, TEST_APP_DISPLAY_NAME_PREFIX):
return web_app
return project_management.create_web_app(display_name=TEST_APP_DISPLAY_NAME_PREFIX)


def test_create_android_app_already_exists(android_app):
del android_app

Expand Down Expand Up @@ -204,3 +214,35 @@ def test_get_ios_app_config(ios_app, project_id):
assert plist['BUNDLE_ID'] == TEST_APP_BUNDLE_ID
assert plist['PROJECT_ID'] == project_id
assert plist['GOOGLE_APP_ID'] == ios_app.app_id


def test_web_set_display_name_and_get_metadata(web_app, project_id):
app_id = web_app.app_id
web_app = project_management.web_app(app_id)
new_display_name = '{0} helloworld {1}'.format(
TEST_APP_DISPLAY_NAME_PREFIX, random.randint(0, 10000))

web_app.set_display_name(new_display_name)
metadata = project_management.web_app(app_id).get_metadata()
web_app.set_display_name(TEST_APP_DISPLAY_NAME_PREFIX) # Revert the display name.

assert metadata._name == 'projects/{0}/webApps/{1}'.format(project_id, app_id)
assert metadata.app_id == app_id
assert metadata.project_id == project_id
assert metadata.display_name == new_display_name


def test_list_web_apps(web_app):
del web_app

web_apps = project_management.list_web_apps()

assert any(_starts_with(web_app.get_metadata().display_name, TEST_APP_DISPLAY_NAME_PREFIX)
for web_app in web_apps)


def test_get_web_app_config(web_app, project_id):
config = web_app.get_config()

assert config['projectId'] == project_id
assert config['appId'] == web_app.app_id