diff --git a/.gitignore b/.gitignore
index 0048795..16cdbb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,8 @@ _build
dist
*.pyc
__pycache__
+.cache/
+.coverage
+htmlcov/
+pytest.ini
+*.ini
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2cd31d2..d7f57ba 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -7,6 +7,22 @@ Change Log
All library changes, in descending order.
+Version 0.5.0
+-------------
+
+**Released March 1, 2017.**
+
+- Refactored views from function based to class based.
+- Adding VerifyEmail and Me view.
+- Adding request parsing.
+- Upgrading social login.
+- Upgraded CSRF protection.
+- Adding new configuration loading.
+- Replaced settings with 'STORMPATH' prefix to one from our Stormpath Config
+ object.
+- Adding dynamic forms.
+
+
Version 0.4.8
-------------
diff --git a/docs/product.rst b/docs/product.rst
index 94e407c..cd79810 100644
--- a/docs/product.rst
+++ b/docs/product.rst
@@ -480,12 +480,12 @@ Next, copy the following code into ``templates/login.html``::
{# If social login is enabled, display social login buttons. #}
- {% if config['STORMPATH_ENABLE_FACEBOOK'] or config['STORMPATH_ENABLE_GOOGLE'] %}
+ {% if config['stormpath']['web']['social']['facebook']['enabled'] or config['stormpath']['web']['social']['google']['enabled'] %}
Or, log in using a social provider.
- {% if config['STORMPATH_ENABLE_FACEBOOK'] %}
+ {% if config['stormpath']['web']['social']['facebook']['enabled'] %}
{% include "flask_stormpath/facebook_login_form.html" %}
{% endif %}
- {% if config['STORMPATH_ENABLE_GOOGLE'] %}
+ {% if config['stormpath']['web']['social']['google']['enabled'] %}
{% include "flask_stormpath/google_login_form.html" %}
{% endif %}
{% endif %}
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index d1650f9..0049022 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -8,6 +8,12 @@ This page contains specific upgrading instructions to help you migrate between
Flask-Stormpath releases.
+Version 0.4.8 -> Version 0.5.0
+------------------------------
+
+**No changes needed!**
+
+
Version 0.4.7 -> Version 0.4.8
------------------------------
diff --git a/flask_stormpath/__init__.py b/flask_stormpath/__init__.py
index bd7798f..470dd78 100644
--- a/flask_stormpath/__init__.py
+++ b/flask_stormpath/__init__.py
@@ -1,4 +1,52 @@
# -*- coding: utf-8 -*-
+
+
+import os
+from datetime import timedelta
+from flask import Blueprint, __version__ as flask_version, current_app
+
+from flask_login import (
+ LoginManager,
+ current_user,
+ login_required,
+ login_user,
+ logout_user
+)
+
+from flask_login.utils import _get_user
+from stormpath.client import Client
+from stormpath.error import Error as StormpathError
+from stormpath_config.loader import ConfigLoader
+from stormpath_config.strategies import (
+ LoadEnvConfigStrategy,
+ LoadFileConfigStrategy,
+ LoadAPIKeyConfigStrategy,
+ LoadAPIKeyFromConfigStrategy,
+ ValidateClientConfigStrategy,
+ EnrichClientFromRemoteConfigStrategy,
+ EnrichIntegrationFromRemoteConfigStrategy,
+ MoveAPIKeyToClientAPIKeyStrategy,
+ ExtendConfigStrategy,
+ MoveSettingsToConfigStrategy)
+
+from werkzeug.local import LocalProxy
+from .context_processors import user_context_processor
+from .request_processors import request_wants_json
+from .models import User
+from .settings import StormpathSettings
+from .views import (
+ RegisterView,
+ LoginView,
+ ForgotPasswordView,
+ ChangePasswordView,
+ VerifyEmailView,
+ LogoutView,
+ MeView,
+ GoogleLoginView,
+ FacebookLoginView
+)
+
+
"""
flask-stormpath
---------------
@@ -15,52 +63,13 @@
"""
-__version__ = '0.4.8'
+__version__ = '0.5.0'
__version_info__ = __version__.split('.')
__author__ = 'Stormpath, Inc.'
__license__ = 'Apache'
__copyright__ = '(c) 2012 - 2015 Stormpath, Inc.'
-from flask import (
- Blueprint,
- __version__ as flask_version,
- _app_ctx_stack as stack,
- current_app,
-)
-
-from flask_login import (
- LoginManager,
- current_user,
- login_required,
- login_user,
- logout_user,
-)
-try:
- from flask_login.utils import _get_user
-except ImportError:
- from flask_login import _get_user
-
-from stormpath.client import Client
-from stormpath.error import Error as StormpathError
-
-from werkzeug.local import LocalProxy
-
-from .context_processors import user_context_processor
-from .decorators import groups_required
-from .models import User
-from .settings import check_settings, init_settings
-from .views import (
- google_login,
- facebook_login,
- forgot,
- forgot_change,
- login,
- logout,
- register,
-)
-
-
# A proxy for the current user.
user = LocalProxy(lambda: _get_user())
@@ -72,19 +81,26 @@ class StormpathManager(object):
specific apps, so you can create one in the main body of your code and
then bind it to your app in a factory function.
"""
- def __init__(self, app=None):
+ def __init__(self, app=None, csrf=None):
"""
Initialize this extension.
:param obj app: (optional) The Flask app.
+ :param obj csrf: (optional) CSRFProtect object.
"""
self.app = app
+ self.csrf = csrf
# If the user specifies an app, let's configure go ahead and handle all
# configuration stuff for the user's app.
if app is not None:
self.init_app(app)
+ @app.before_request
+ def check_csrf():
+ if self.csrf and not request_wants_json():
+ csrf.protect()
+
def init_app(self, app):
"""
Initialize this application.
@@ -100,11 +116,7 @@ def init_app(self, app):
"""
# Initialize all of the Flask-Stormpath configuration variables and
# settings.
- init_settings(app.config)
-
- # Check our user defined settings to ensure Flask-Stormpath is properly
- # configured.
- check_settings(app.config)
+ self.init_settings(app.config)
# Initialize the Flask-Login extension.
self.init_login(app)
@@ -113,7 +125,8 @@ def init_app(self, app):
self.init_routes(app)
# Initialize our blueprint. This lets us do cool template stuff.
- blueprint = Blueprint('flask_stormpath', 'flask_stormpath', template_folder='templates')
+ blueprint = Blueprint(
+ 'flask_stormpath', 'flask_stormpath', template_folder='templates')
app.register_blueprint(blueprint)
# Ensure the `user` context is available in templates. This makes it
@@ -125,6 +138,64 @@ def init_app(self, app):
# necessary!
self.app = app
+ def init_settings(self, config):
+ """
+ Initialize the Flask-Stormpath settings.
+
+ This function sets all default configuration values.
+
+ :param dict config: The Flask app config.
+ """
+ # Basic Stormpath credentials and configuration.
+ web_config_file = config.get('STORMPATH_CONFIG_PATH')
+
+ # Set default settings needed to init stormpath-flask.
+ self.set_default_settings(config)
+
+ config_loader = ConfigLoader(
+ load_strategies=[
+ LoadFileConfigStrategy(web_config_file),
+ LoadAPIKeyConfigStrategy("~/.stormpath/apiKey.properties"),
+ LoadFileConfigStrategy("~/.stormpath/stormpath.json"),
+ LoadFileConfigStrategy("~/.stormpath/stormpath.yaml"),
+ LoadAPIKeyConfigStrategy("./apiKey.properties"),
+ LoadFileConfigStrategy("./stormpath.yaml"),
+ LoadFileConfigStrategy("./stormpath.json"),
+ LoadEnvConfigStrategy(prefix='STORMPATH'),
+ ExtendConfigStrategy(extend_with={}), # FIXME: This is still unimplemented.
+ MoveSettingsToConfigStrategy(config=config)
+ ],
+ post_processing_strategies=[
+ LoadAPIKeyFromConfigStrategy(),
+ MoveAPIKeyToClientAPIKeyStrategy()
+ ],
+ validation_strategies=[ValidateClientConfigStrategy()])
+ config['stormpath'] = StormpathSettings(config_loader.load())
+
+ # Create our custom user agent. This allows us to see which
+ # version of this SDK are out in the wild!
+ user_agent = 'stormpath-flask/%s flask/%s' % (
+ __version__, flask_version)
+
+ # Instantiate client with apiKey id and secret from config.
+ self.client = Client(
+ id=self.app.config['stormpath']['client']['apiKey']['id'],
+ secret=self.app.config['stormpath']['client']['apiKey']['secret'],
+ user_agent=user_agent,
+ cache_options=self.app.config['stormpath']['cache'],
+ )
+
+ # Enrich config from API service, and validate crucial settings.
+ ecfrcs = EnrichClientFromRemoteConfigStrategy(
+ client_factory=lambda client: self.client)
+ ecfrcs.process(self.app.config['stormpath'].store)
+ eifrcs = EnrichIntegrationFromRemoteConfigStrategy(
+ client_factory=lambda client: self.client)
+ eifrcs.process(self.app.config['stormpath'].store)
+
+ self.application = self.client.applications.get(
+ self.app.config['stormpath']['application']['href'])
+
def init_login(self, app):
"""
Initialize the Flask-Login extension.
@@ -134,18 +205,21 @@ def init_login(self, app):
:param obj app: The Flask app.
"""
- app.config['REMEMBER_COOKIE_DURATION'] = app.config['STORMPATH_COOKIE_DURATION']
- app.config['REMEMBER_COOKIE_DOMAIN'] = app.config['STORMPATH_COOKIE_DOMAIN']
+ app.config['REMEMBER_COOKIE_DURATION'] = app.config[
+ 'stormpath']['cookie']['duration']
+ app.config['REMEMBER_COOKIE_DOMAIN'] = app.config[
+ 'stormpath']['cookie']['domain']
app.login_manager = LoginManager(app)
app.login_manager.user_callback = self.load_user
app.stormpath_manager = self
- if app.config['STORMPATH_ENABLE_LOGIN']:
+ if app.config['stormpath']['web']['login']['enabled']:
app.login_manager.login_view = 'stormpath.login'
# Make this Flask session expire automatically.
- app.config['PERMANENT_SESSION_LIFETIME'] = app.config['STORMPATH_COOKIE_DURATION']
+ app.config['PERMANENT_SESSION_LIFETIME'] = app.config[
+ 'stormpath']['cookie']['duration']
def init_routes(self, app):
"""
@@ -158,93 +232,99 @@ def init_routes(self, app):
:param obj app: The Flask app.
"""
- if app.config['STORMPATH_ENABLE_REGISTRATION']:
+ if app.config['stormpath']['web']['basePath']:
+ base_path = app.config['stormpath']['web']['basePath']
+ else:
+ base_path = '/'
+
+ if app.config['stormpath']['web']['register']['enabled']:
app.add_url_rule(
- app.config['STORMPATH_REGISTRATION_URL'],
+ os.path.join(
+ base_path,
+ app.config['stormpath']['web']['register'][
+ 'uri'].strip('/')),
'stormpath.register',
- register,
- methods = ['GET', 'POST'],
+ RegisterView.as_view('register'),
+ methods=['GET', 'POST'],
)
- if app.config['STORMPATH_ENABLE_LOGIN']:
+ if app.config['stormpath']['web']['login']['enabled']:
app.add_url_rule(
- app.config['STORMPATH_LOGIN_URL'],
+ os.path.join(
+ base_path, app.config['stormpath']['web']['login'][
+ 'uri'].strip('/')),
'stormpath.login',
- login,
- methods = ['GET', 'POST'],
+ LoginView.as_view('login'),
+ methods=['GET', 'POST'],
)
- if app.config['STORMPATH_ENABLE_FORGOT_PASSWORD']:
+ if app.config['stormpath']['web']['forgotPassword']['enabled']:
app.add_url_rule(
- app.config['STORMPATH_FORGOT_PASSWORD_URL'],
+ os.path.join(
+ base_path,
+ app.config['stormpath']['web']['forgotPassword'][
+ 'uri'].strip('/')),
'stormpath.forgot',
- forgot,
- methods = ['GET', 'POST'],
+ ForgotPasswordView.as_view('forgot'),
+ methods=['GET', 'POST'],
)
app.add_url_rule(
- app.config['STORMPATH_FORGOT_PASSWORD_CHANGE_URL'],
+ os.path.join(
+ base_path,
+ app.config['stormpath']['web']['changePassword'][
+ 'uri'].strip('/')),
'stormpath.forgot_change',
- forgot_change,
- methods = ['GET', 'POST'],
+ ChangePasswordView.as_view('change'),
+ methods=['GET', 'POST'],
)
- if app.config['STORMPATH_ENABLE_LOGOUT']:
+ if app.config['stormpath']['web']['verifyEmail']['enabled']:
app.add_url_rule(
- app.config['STORMPATH_LOGOUT_URL'],
+ app.config['stormpath']['web']['verifyEmail']['uri'],
+ 'stormpath.verify',
+ VerifyEmailView.as_view('verify'),
+ methods=['GET', 'POST'],
+ )
+
+ if app.config['stormpath']['web']['logout']['enabled']:
+ app.add_url_rule(
+ os.path.join(
+ base_path,
+ app.config['stormpath']['web']['logout'][
+ 'uri'].strip('/')),
'stormpath.logout',
- logout,
+ LogoutView.as_view('logout'),
)
- if app.config['STORMPATH_ENABLE_GOOGLE']:
+ if app.config['stormpath']['web']['me']['enabled']:
app.add_url_rule(
- app.config['STORMPATH_GOOGLE_LOGIN_URL'],
+ os.path.join(
+ base_path,
+ app.config['stormpath']['web']['me']['uri'].strip('/')),
+ 'stormpath.me',
+ MeView.as_view('me'),
+ )
+
+ if app.config['stormpath']['web']['social']['google']['enabled']:
+ app.add_url_rule(
+ os.path.join(
+ base_path,
+ app.config[
+ 'stormpath']['web']['social']['google']['login_url']),
'stormpath.google_login',
- google_login,
+ GoogleLoginView.as_view('google'),
)
- if app.config['STORMPATH_ENABLE_FACEBOOK']:
+ if app.config['stormpath']['web']['social']['facebook']['enabled']:
app.add_url_rule(
- app.config['STORMPATH_FACEBOOK_LOGIN_URL'],
+ os.path.join(
+ base_path,
+ app.config[
+ 'stormpath']['web']['social']['facebook']['login_url']),
'stormpath.facebook_login',
- facebook_login,
+ FacebookLoginView.as_view('facebook'),
)
- @property
- def client(self):
- """
- Lazily load the Stormpath Client object we need to access the raw
- Stormpath SDK.
- """
- ctx = stack.top.app
- if ctx is not None:
- if not hasattr(ctx, 'stormpath_client'):
-
- # Create our custom user agent. This allows us to see which
- # version of this SDK are out in the wild!
- user_agent = 'stormpath-flask/%s flask/%s' % (__version__, flask_version)
-
- # If the user is specifying their credentials via a file path,
- # we'll use this.
- if self.app.config['STORMPATH_API_KEY_FILE']:
- ctx.stormpath_client = Client(
- api_key_file_location = self.app.config['STORMPATH_API_KEY_FILE'],
- user_agent = user_agent,
- cache_options = self.app.config['STORMPATH_CACHE'],
- )
-
- # If the user isn't specifying their credentials via a file
- # path, it means they're using environment variables, so we'll
- # try to grab those values.
- else:
- ctx.stormpath_client = Client(
- id = self.app.config['STORMPATH_API_KEY_ID'],
- secret = self.app.config['STORMPATH_API_KEY_SECRET'],
- user_agent = user_agent,
- cache_options = self.app.config['STORMPATH_CACHE'],
- )
-
- return ctx.stormpath_client
-
@property
def login_view(self):
"""
@@ -260,25 +340,6 @@ def login_view(self, value):
"""
self.app.login_manager.login_view = value
- @property
- def application(self):
- """
- Lazily load the Stormpath Application object we need to handle user
- authentication, etc.
- """
- ctx = stack.top.app
- if ctx is not None:
- if not hasattr(ctx, 'stormpath_application'):
- applications = self.client.applications.search(
- self.app.config['STORMPATH_APPLICATION']
- )
- if applications is None:
- raise Exception('Failed to find ' + self.app.config['STORMPATH_APPLICATION'] + ' application. Please add it in the Stormpath console.')
-
- ctx.stormpath_application = applications[0]
-
- return ctx.stormpath_application
-
@staticmethod
def load_user(account_href):
"""
@@ -296,3 +357,41 @@ def load_user(account_href):
return user
except StormpathError:
return None
+
+ def set_default_settings(self, config):
+ """
+ Sets default settings for crucial settings needed to run
+ Flask-Stormpath.
+ """
+
+ # Set csrf default check to False, since our StormpathManager applies
+ # custom logic for applying csrf tokens.
+ config.setdefault('WTF_CSRF_CHECK_DEFAULT', False)
+
+ # Which fields should be displayed when registering new users?
+ config.setdefault('STORMPATH_ENABLE_FACEBOOK', False)
+ config.setdefault('STORMPATH_ENABLE_GOOGLE', False)
+
+ # Configure URL mappings. These URL mappings control which URLs will
+ # be used by Flask-Stormpath views.
+ config.setdefault('STORMPATH_GOOGLE_LOGIN_URL', '/google')
+ config.setdefault('STORMPATH_FACEBOOK_LOGIN_URL', '/facebook')
+
+ # Cache configuration.
+ config.setdefault('STORMPATH_CACHE', None)
+
+ # Configure templates. These template settings control which
+ # templates are used to render the Flask-Stormpath views.
+ config.setdefault(
+ 'STORMPATH_BASE_TEMPLATE', 'flask_stormpath/base.html')
+
+ # Social login configuration.
+ config.setdefault('STORMPATH_SOCIAL', {})
+
+ # Cookie configuration.
+ config.setdefault('STORMPATH_COOKIE_DOMAIN', None)
+ config.setdefault('STORMPATH_COOKIE_DURATION', timedelta(days=365))
+
+ # Cookie name (this is not overridable by users, at least
+ # not explicitly).
+ config.setdefault('REMEMBER_COOKIE_NAME', 'stormpath_token')
diff --git a/flask_stormpath/config/default-config.yml b/flask_stormpath/config/default-config.yml
new file mode 100644
index 0000000..30e9661
--- /dev/null
+++ b/flask_stormpath/config/default-config.yml
@@ -0,0 +1,310 @@
+web:
+
+ # The basePath is used as the default path for the cookies that are set by
+ # this library. If not defined, we will default to /
+ basePath: null
+
+ domainName: null # Required if using subdomain-based multi-tenancy
+
+ multiTenancy:
+
+ # When enabled, the framework will require the user to authenticate against
+ # a specific Organization. The authenticated organization is persisted in
+ # the access token that is issued.
+ #
+ # At the moment we only support a sub-domain based strategy, wherby the
+ # user must arrive on the subdomain that correlates with their tenant in
+ # order to authenticate. If they visit the parent domain, they will be
+ # required to identify the organization they wish to use.
+
+ enabled: false
+ strategy: "subdomain"
+
+ oauth2:
+ enabled: true
+ uri: "/oauth/token"
+ client_credentials:
+ enabled: true
+ accessToken:
+ ttl: 3600
+ password:
+ enabled: true
+ validationStrategy: "local"
+
+ accessTokenCookie:
+
+ # Controls the name of the cookie that we send to the browser.
+ name: "access_token"
+
+ # httpOnly is true, to prevent XSS attacks from hijacking your cookies. We
+ # highly recommend that you do not change this.
+ httpOnly: true
+
+ # The secure property controls the Secure flag on the cookie. The
+ # framework will auto-detect if the incoming request is over HTTPS, by
+ # looking at req.protocol === 'https' and turn on Secure if so. You can
+ # override auto-detection and force the Secure flag on by setting this
+ # property to true, or force off by setting this property to false.
+ secure: null
+
+ # Controls the path flag of the cookie. Inherits from basePath, but can be
+ # overridden here.
+ path: null
+
+ # Controls the domain flag on the cookie, will not be set unless specified.
+ domain: null
+
+ # Refresh Token Cookie has same options as the Access Token Cookie (above).
+ refreshTokenCookie:
+ name: "refresh_token"
+ httpOnly: true
+ secure: null
+ path: null
+ domain: null
+
+ # If the request does not specify an Accept header, or the preferred content
+ # type is */*, the Stormpath integration will respond with the first type in
+ # this list.
+ produces:
+ - application/json
+ - text/html
+
+ # By default the Stormpath integration will respond to JSON and HTML
+ # requests. If a requested type is not in this list, the Stormpath
+ # integration should pass on the request, and allow the developer or base
+ # framework to handle the response.
+ invalidRequest:
+ uri: "/invalid_request"
+
+ register:
+ enabled: true
+ uri: "/register"
+ nextUri: "/"
+
+ # autoLogin is possible only if the email verification feature is disabled
+ # on the default account store of the defined Stormpath application.
+ autoLogin: false
+ view: "register"
+ form:
+ fields:
+ givenName:
+ enabled: true
+ label: "First Name"
+ placeholder: "First Name"
+ required: true
+ type: "text"
+ middleName:
+ enabled: false
+ label: "Middle Name"
+ placeholder: "Middle Name"
+ required: true
+ type: "text"
+ surname:
+ enabled: true
+ label: "Last Name"
+ placeholder: "Last Name"
+ required: true
+ type: "text"
+ username:
+ enabled: true
+ label: "Username"
+ placeholder: "Username"
+ required: true
+ type: "text"
+ email:
+ enabled: true
+ label: "Email"
+ placeholder: "Email"
+ required: true
+ type: "email"
+ password:
+ enabled: true
+ label: "Password"
+ placeholder: "Password"
+ required: true
+ type: "password"
+ confirmPassword:
+ enabled: false
+ label: "Confirm Password"
+ placeholder: "Confirm Password"
+ required: true
+ type: "password"
+ fieldOrder:
+ - "username"
+ - "givenName"
+ - "middleName"
+ - "surname"
+ - "email"
+ - "password"
+ - "confirmPassword"
+
+ # Unless verifyEmail.enabled is specifically set to false, the email
+ # verification feature must be automatically enabled if the default account
+ # store for the defined Stormpath application has the email verification
+ # workflow enabled.
+ verifyEmail:
+ enabled: true
+ uri: "/verify"
+ nextUri: "/login?status=verified"
+ unverifiedUri: "/login?status=unverified"
+ view: "verify"
+ form:
+ fields:
+ email:
+ enabled: true
+ visible: true
+ label: "Email"
+ placeholder: "Email"
+ required: true
+ type: "email"
+ fieldOrder:
+ - "email"
+
+ login:
+ enabled: true
+ uri: "/login"
+ nextUri: "/"
+ view: "login"
+ form:
+ fields:
+ login:
+ enabled: true
+ label: "Username or Email"
+ placeholder: "Username or Email"
+ required: true
+ type: "text"
+ password:
+ enabled: true
+ label: "Password"
+ placeholder: "Password"
+ required: true
+ type: "password"
+ fieldOrder:
+ - "login"
+ - "password"
+
+ logout:
+ enabled: true
+ uri: "/logout"
+ nextUri: "/"
+
+ organizationSelect:
+ view: "organization-select"
+ form:
+ fields:
+ organizationNameKey:
+ label: "Enter your organization name to continue"
+ placeholder: "e.g. my-company"
+
+ # Unless forgotPassword.enabled is explicitly set to false, this feature
+ # will be automatically enabled if the default account store for the defined
+ # Stormpath application has the password reset workflow enabled.
+ forgotPassword:
+ enabled: true
+ uri: "/forgot"
+ nextUri: "/login?status=forgot"
+ view: "forgot-password"
+ form:
+ fields:
+ email:
+ enabled: true
+ visible: true
+ label: "Email"
+ placeholder: "Email"
+ required: true
+ type: "email"
+ fieldOrder:
+ - "email"
+
+ # Unless changePassword.enabled is explicitly set to false, this feature
+ # will be automatically enabled if the default account store for the defined
+ # Stormpath application has the password reset workflow enabled.
+ changePassword:
+ enabled: true
+ uri: "/change"
+ nextUri: "/login?status=reset"
+ errorUri: "/forgot?status=invalid_sptoken"
+ autoLogin: false
+ view: "change-password"
+ form:
+ fields:
+ password:
+ enabled: true
+ label: "Password"
+ placeholder: "Password"
+ required: true
+ type: "password"
+ confirmPassword:
+ enabled: true
+ label: "Confirm Password"
+ placeholder: "Confirm Password"
+ required: true
+ type: "password"
+ fieldOrder:
+ - "password"
+ - "confirmPassword"
+
+ # If idSite.enabled is true, the user should be redirected to ID site for
+ # login, registration, and password reset. They should also be redirected
+ # through ID Site on logout.
+ idSite:
+ enabled: false
+ uri: "/idSiteResult"
+ nextUri: "/"
+ loginUri: ""
+ forgotUri: "/#/forgot"
+ registerUri: "/#/register"
+
+ # Social login configuration. This defines the callback URIs for OAuth
+ # flows, and the scope that is requested of each provider. Some providers
+ # want space-separated scopes, some want comma-separated. As such, these
+ # string values should be passed directly, as defined.
+ #
+ # These settings have no affect if the application does not have an account
+ # store for the given provider.
+ social:
+ facebook:
+ uri: "/callbacks/facebook"
+ scope: "email"
+ github:
+ uri: "/callbacks/github"
+ scope: "user:email"
+ google:
+ uri: "/callbacks/google"
+ scope: "email profile"
+ linkedin:
+ uri: "/callbacks/linkedin"
+ scope: "r_basicprofile, r_emailaddress"
+
+ # The /me route is for front-end applications, it returns a JSON object with
+ # the current user object. The developer can opt-in to expanding account
+ # resources on this enpdoint.
+ me:
+ enabled: true
+ uri: "/me"
+ expand:
+ apiKeys: false
+ applications: false
+ customData: false
+ directory: false
+ groupMemberships: false
+ groups: false
+ providerData: false
+ tenant: false
+
+ # If the developer wants our integration to serve their Single Page
+ # Application (SPA) in response to HTML requests for our default routes,
+ # such as /login, then they will need to enable this feature and tell us
+ # where the root of their SPA is. This is likely a file path on the
+ # filesystem.
+ #
+ # If the developer does not want our integration to handle their SPA, they
+ # will need to configure the framework themselves and remove 'text/html'
+ # from `stormpath.web.produces`, so that we don't serve our default
+ # HTML views.
+ spa:
+ enabled: false
+ view: null
+
+ unauthorized:
+ view: "unauthorized"
diff --git a/flask_stormpath/context_processors.py b/flask_stormpath/context_processors.py
index 5ac3c4a..cae4796 100644
--- a/flask_stormpath/context_processors.py
+++ b/flask_stormpath/context_processors.py
@@ -1,11 +1,7 @@
"""Custom context processors to make template development simpler."""
-from flask import current_app
-try:
- from flask_login.utils import _get_user
-except ImportError:
- from flask_login import _get_user
+from flask_login.utils import _get_user
def user_context_processor():
diff --git a/flask_stormpath/errors.py b/flask_stormpath/errors.py
deleted file mode 100644
index 106f7ad..0000000
--- a/flask_stormpath/errors.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Custom errors."""
-
-
-class ConfigurationError(Exception):
- """
- This exception is raised if a user has misconfigured Flask-Stormpath.
- """
- pass
diff --git a/flask_stormpath/forms.py b/flask_stormpath/forms.py
index d99728c..737ba5f 100644
--- a/flask_stormpath/forms.py
+++ b/flask_stormpath/forms.py
@@ -2,98 +2,97 @@
from flask_wtf import FlaskForm
-from flask_wtf.form import _Auto
+from wtforms.widgets import HiddenInput
from wtforms.fields import PasswordField, StringField
-from wtforms.validators import Email, EqualTo, InputRequired, ValidationError
-
-
-class RegistrationForm(FlaskForm):
- """
- Register a new user.
-
- This class is used to provide safe user registration. The only required
- fields are `email` and `password` -- everything else is optional (and can
- be configured by the developer to be used or not).
-
- .. note::
- This form only includes the fields that are available to register
- users with Stormpath directly -- this doesn't include support for
- Stormpath's social login stuff.
-
- Since social login stuff is handled separately (registration happens
- through Javascript) we don't need to have a form for registering users
- that way.
- """
- username = StringField('Username')
- given_name = StringField('First Name')
- middle_name = StringField('Middle Name')
- surname = StringField('Last Name')
- email = StringField('Email', validators=[
- InputRequired('You must provide an email address.'),
- Email('You must provide a valid email address.')
- ])
- password = PasswordField('Password', validators=[InputRequired('You must supply a password.')])
-
- def __init__(self, formdata=_Auto, config=None, **kwargs):
- super(RegistrationForm, self).__init__(formdata=formdata, **kwargs)
-
- if config:
- if config['STORMPATH_ENABLE_USERNAME'] and config['STORMPATH_REQUIRE_USERNAME']:
- self.username.validators.append(InputRequired('Username is required.'))
-
- if config['STORMPATH_ENABLE_GIVEN_NAME'] and config['STORMPATH_REQUIRE_GIVEN_NAME']:
- self.given_name.validators.append(InputRequired('First name is required.'))
-
- if config['STORMPATH_ENABLE_MIDDLE_NAME'] and config['STORMPATH_REQUIRE_MIDDLE_NAME']:
- self.middle_name.validators.append(InputRequired('Middle name is required.'))
-
- if config['STORMPATH_ENABLE_SURNAME'] and config['STORMPATH_REQUIRE_SURNAME']:
- self.surname.validators.append(InputRequired('Surname is required.'))
-
-
-class LoginForm(FlaskForm):
- """
- Log in an existing user.
-
- This class is used to provide safe user login. A user can log in using
- a login identifier (either email or username) and password. Stormpath
- handles the username / email abstractions itself, so we don't need any
- special logic to handle those cases.
-
- .. note::
- This form only includes the fields that are available to log users in
- with Stormpath directly -- this doesn't include support for Stormpath's
- social login stuff.
-
- Since social login stuff is handled separately (login happens through
- Javascript) we don't need to have a form for logging in users that way.
- """
- login = StringField('Login', validators=[InputRequired('Login identifier required.')])
- password = PasswordField('Password', validators=[InputRequired('Password required.')])
-
-
-class ForgotPasswordForm(FlaskForm):
- """
- Retrieve a user's email address for initializing the password reset
- workflow.
-
- This class is used to retrieve a user's email address.
- """
- email = StringField('Email', validators=[
- InputRequired('Email address required.'),
- Email('You must provide a valid email address.')
- ])
-
-
-class ChangePasswordForm(FlaskForm):
- """
- Change a user's password.
-
- This class is used to retrieve a user's password twice to ensure it's valid
- before making a change.
- """
- password = PasswordField('Password', validators=[InputRequired('Password required.')])
- password_again = PasswordField('Password (again)', validators=[
- InputRequired('Please verify the password.'),
- EqualTo('password', 'Passwords do not match.')
- ])
+from wtforms.validators import InputRequired, EqualTo, Email
+from stormpath.resources import Resource
+import json
+
+
+class StormpathForm(FlaskForm):
+ @classmethod
+ def specialize_form(basecls, config):
+ """
+ Dynamic form.
+
+ This class is used to set fields dynamically based on the form fields
+ settings from the config.
+
+ .. note::
+ This doesn't include support for Stormpath's social login stuff.
+ Since social login stuff is handled separately (through
+ Javascript), we don't need to have a form for registering/logging
+ in users that way.
+ """
+
+ class cls(basecls):
+ # Make sure that the original class is left unaltered.
+ pass
+
+ field_list = config.get('fields', {})
+ field_order = config.get('fieldOrder', [])
+
+ setattr(cls, '_json', [])
+
+ for field in field_order:
+ if field_list[field]['enabled']:
+ validators = []
+ placeholder = field_list[field]['placeholder']
+
+ # Construct json fields
+ json_field = {'name': Resource.from_camel_case(field)}
+ json_field['placeholder'] = placeholder
+
+ # Apply validators.
+ if field_list[field]['required']:
+ validators.append(InputRequired(
+ message='%s is required.' % placeholder))
+
+ if field_list[field]['type'] == 'email':
+ validators.append(Email(
+ message='Email must be in valid format.'))
+
+ if field == 'confirmPassword':
+ validators.append(EqualTo(
+ 'password', message='Passwords do not match.'))
+ json_field['required'] = field_list[field]['required']
+
+ # Apply widgets.
+ if field_list[field].get('visible', True):
+ widget = None
+ json_field['visible'] = True
+ else:
+ widget = HiddenInput()
+ json_field['visible'] = False
+
+ # Apply field classes.
+ if field_list[field]['type'] == 'password':
+ field_class = PasswordField
+ else:
+ field_class = StringField
+ json_field['type'] = field_list[field]['type']
+
+ # Apply labels.
+ if 'label' in field_list[field] and isinstance(
+ field_list[field]['label'], str):
+ label = field_list[field]['label']
+ else:
+ label = ''
+ json_field['label'] = field_list[field]['label']
+
+ # Set json fields.
+ cls._json.append(json_field)
+
+ # Finally, create our fields dynamically.
+ setattr(
+ cls, Resource.from_camel_case(field),
+ field_class(
+ label, validators=validators,
+ render_kw={"placeholder": placeholder},
+ widget=widget))
+
+ return cls
+
+ @property
+ def json(self):
+ return json.dumps(self._json)
diff --git a/flask_stormpath/models.py b/flask_stormpath/models.py
index 53a48e2..18fb893 100644
--- a/flask_stormpath/models.py
+++ b/flask_stormpath/models.py
@@ -1,13 +1,16 @@
"""Custom data models."""
-from flask import current_app
+from flask import current_app, request
from six import text_type
from blinker import Namespace
from stormpath.resources.account import Account
from stormpath.resources.provider import Provider
+from . import StormpathError
+from datetime import datetime
+import json
stormpath_signals = Namespace()
@@ -60,9 +63,9 @@ def save(self):
"""
Send signal after user is updated.
"""
- return_value = super(User, self).save()
+ super(User, self).save()
user_updated.send(self, user=dict(self))
- return return_value
+ return self
def delete(self):
"""
@@ -73,8 +76,39 @@ def delete(self):
user_deleted.send(None, user=user_dict)
return return_value
+ def to_json(self):
+ def datetime_handler(obj):
+ if hasattr(obj, 'isoformat'):
+ return obj.isoformat()
+ else:
+ raise TypeError
+
+ attrs = (
+ 'href',
+ 'modified_at',
+ 'created_at',
+ 'status',
+ 'username',
+ 'email',
+ 'given_name',
+ 'middle_name',
+ 'surname',
+ 'full_name'
+ )
+ json_data = {
+ 'account': {attr: getattr(self, attr, None) for attr in attrs}}
+
+ # In case me view was called with expanded options enabled.
+ if hasattr(self._expand, 'items'):
+ json_data['account'].update(self._expand.items)
+
+ return json.dumps(json_data, default=datetime_handler)
+
@classmethod
- def create(cls, email, password, given_name, surname, username=None, middle_name=None, custom_data=None, status='ENABLED'):
+ def create(
+ self, email=None, password=None, given_name=None, surname=None,
+ username=None, middle_name=None, custom_data=None,
+ status='ENABLED'):
"""
Create a new User.
@@ -113,12 +147,12 @@ def create(cls, email, password, given_name, surname, username=None, middle_name
'status': status,
})
_user.__class__ = User
- user_created.send(cls, user=dict(_user))
+ user_created.send(self, user=dict(_user))
return _user
@classmethod
- def from_login(cls, login, password):
+ def from_login(self, login, password):
"""
Create a new User class given a login (`email` or `username`), and
password.
@@ -126,13 +160,86 @@ def from_login(cls, login, password):
If something goes wrong, this will raise an exception -- most likely --
a `StormpathError` (flask_stormpath.StormpathError).
"""
- _user = current_app.stormpath_manager.application.authenticate_account(login, password).account
+ _user = current_app.stormpath_manager.application.authenticate_account(
+ login, password).account
+ _user.refresh()
_user.__class__ = User
return _user
+ @staticmethod
+ def from_social(social_name, access_token, provider):
+ """
+ Helper method for our social methods.
+ """
+
+ kwargs = {'provider': provider.get('provider_id')}
+ if social_name == 'facebook':
+ kwargs['access_token'] = access_token
+ elif social_name == 'google':
+ kwargs['code'] = access_token
+ else:
+ raise ValueError('Social service is not supported.')
+
+ try:
+ _user = (
+ current_app.stormpath_manager.
+ application.get_provider_account(**kwargs))
+ except StormpathError as err:
+ social_directory_exists = False
+
+ # If we failed here, it usually means that this application doesn't
+ # have a social directory -- so we'll create one!
+ for asm in (
+ current_app.stormpath_manager.application.
+ account_store_mappings):
+
+ # If there is a social directory, we know this isn't the
+ # problem.
+ if (
+ getattr(asm.account_store, 'provider') and
+ asm.account_store.provider.provider_id == provider.get(
+ 'provider_id')
+ ):
+ social_directory_exists = True
+ break
+
+ # If there is a social directory already, we'll just pass on the
+ # exception we got.
+ if social_directory_exists:
+ raise err
+
+ # Otherwise, we'll try to create a social directory on the user's
+ # behalf (magic!).
+ dir = current_app.stormpath_manager.client.directories.create({
+ 'name': (
+ current_app.stormpath_manager.application.name +
+ '-' + social_name),
+ 'provider': provider
+ })
+
+ # Now that we have a social directory, we'll map it to our
+ # application so it is active.
+ asm = (
+ current_app.stormpath_manager.application.
+ account_store_mappings.create({
+ 'application': current_app.stormpath_manager.application,
+ 'account_store': dir,
+ 'list_index': 99,
+ 'is_default_account_store': False,
+ 'is_default_group_store': False,
+ }))
+
+ # Lastly, let's retry the social login one more time.
+ _user = (
+ current_app.stormpath_manager.
+ application.get_provider_account(**kwargs))
+
+ _user.__class__ = User
+ return _user
+
@classmethod
- def from_google(cls, code):
+ def from_google(self, code):
"""
Create a new User class given a Google access code.
@@ -142,29 +249,33 @@ def from_google(cls, code):
If something goes wrong, this will raise an exception -- most likely --
a `StormpathError` (flask_stormpath.StormpathError).
"""
- _user = current_app.stormpath_manager.application.get_provider_account(
- code = code,
- provider = Provider.GOOGLE,
- )
- _user.__class__ = User
-
- return _user
+ provider = {
+ 'client_id': current_app.config[
+ 'stormpath']['web']['social']['google']['clientId'],
+ 'client_secret': current_app.config[
+ 'stormpath']['web']['social']['google']['clientSecret'],
+ 'redirect_uri': request.url_root[:-1] + current_app.config[
+ 'stormpath']['web']['social']['google']['login_url'],
+ 'provider_id': Provider.GOOGLE,
+ }
+ return self.from_social('google', code, provider)
@classmethod
- def from_facebook(cls, access_token):
+ def from_facebook(self, access_token):
"""
Create a new User class given a Facebook user's access token.
- Access tokens must be retrieved from Facebooks's OAuth service (Facebook
- Login).
+ Access tokens must be retrieved from Facebooks's OAuth service
+ (Facebook Login).
If something goes wrong, this will raise an exception -- most likely --
a `StormpathError` (flask_stormpath.StormpathError).
"""
- _user = current_app.stormpath_manager.application.get_provider_account(
- access_token = access_token,
- provider = Provider.FACEBOOK,
- )
- _user.__class__ = User
-
- return _user
+ provider = {
+ 'client_id': current_app.config[
+ 'stormpath']['web']['social']['facebook']['clientId'],
+ 'client_secret': current_app.config[
+ 'stormpath']['web']['social']['facebook']['clientId'],
+ 'provider_id': Provider.FACEBOOK,
+ }
+ return self.from_social('facebook', access_token, provider)
diff --git a/flask_stormpath/request_processors.py b/flask_stormpath/request_processors.py
new file mode 100644
index 0000000..3def03f
--- /dev/null
+++ b/flask_stormpath/request_processors.py
@@ -0,0 +1,30 @@
+"""Description here."""
+
+from flask import request, current_app
+from stormpath_config.errors import ConfigurationError
+
+
+def get_accept_header():
+ """
+ Fetch the request content type and match it against our allowed types.
+ """
+ if current_app and 'stormpath' in current_app.config:
+ allowed_types = current_app.config['stormpath']['web']['produces']
+ request_accept_types = request.accept_mimetypes
+
+ # If no accept types are specified, or the preferred accept type is
+ # */*, response type will be the first element of self.allowed_types.
+ if not request_accept_types or request_accept_types[0][0] == '*/*':
+ return allowed_types[0]
+ else:
+ return request_accept_types.best_match(allowed_types)
+ else:
+ raise ConfigurationError(
+ 'You must initialize flask app before calling this function.')
+
+
+def request_wants_json():
+ """
+ Check if request wants json.
+ """
+ return get_accept_header() == 'application/json'
diff --git a/flask_stormpath/settings.py b/flask_stormpath/settings.py
index 29c06c1..08528d6 100644
--- a/flask_stormpath/settings.py
+++ b/flask_stormpath/settings.py
@@ -1,142 +1,116 @@
"""Helper functions for dealing with Flask-Stormpath settings."""
-from datetime import timedelta
-
-from .errors import ConfigurationError
-
-
-def init_settings(config):
- """
- Initialize the Flask-Stormpath settings.
-
- This function sets all default configuration values.
-
- :param dict config: The Flask app config.
- """
- # Basic Stormpath credentials and configuration.
- config.setdefault('STORMPATH_API_KEY_ID', None)
- config.setdefault('STORMPATH_API_KEY_SECRET', None)
- config.setdefault('STORMPATH_API_KEY_FILE', None)
- config.setdefault('STORMPATH_APPLICATION', None)
-
- # Which fields should be displayed when registering new users?
- config.setdefault('STORMPATH_ENABLE_FACEBOOK', False)
- config.setdefault('STORMPATH_ENABLE_GOOGLE', False)
- config.setdefault('STORMPATH_ENABLE_EMAIL', True) # If this is diabled,
- # only social login can
- # be used.
- config.setdefault('STORMPATH_ENABLE_USERNAME', False)
- config.setdefault('STORMPATH_ENABLE_EMAIL', True) # This MUST be True!
- config.setdefault('STORMPATH_ENABLE_PASSWORD', True) # This MUST be True!
- config.setdefault('STORMPATH_ENABLE_GIVEN_NAME', True)
- config.setdefault('STORMPATH_ENABLE_MIDDLE_NAME', True)
- config.setdefault('STORMPATH_ENABLE_SURNAME', True)
-
- # If the user attempts to create a non-social account, which fields should
- # we require? (Email and password are always required, so those are not
- # mentioned below.)
- config.setdefault('STORMPATH_REQUIRE_USERNAME', True)
- config.setdefault('STORMPATH_REQUIRE_EMAIL', True) # This MUST be True!
- config.setdefault('STORMPATH_REQUIRE_PASSWORD', True) # This MUST be True!
- config.setdefault('STORMPATH_REQUIRE_GIVEN_NAME', True)
- config.setdefault('STORMPATH_REQUIRE_MIDDLE_NAME', False)
- config.setdefault('STORMPATH_REQUIRE_SURNAME', True)
-
- # Will new users be required to verify new accounts via email before
- # they're made active?
- config.setdefault('STORMPATH_VERIFY_EMAIL', False)
-
- # Configure views. These views can be enabled or disabled. If they're
- # enabled (default), then you automatically get URL routes, working views,
- # and working templates for common operations: registration, login, logout,
- # forgot password, and changing user settings.
- config.setdefault('STORMPATH_ENABLE_REGISTRATION', True)
- config.setdefault('STORMPATH_ENABLE_LOGIN', True)
- config.setdefault('STORMPATH_ENABLE_LOGOUT', True)
- config.setdefault('STORMPATH_ENABLE_FORGOT_PASSWORD', False)
- config.setdefault('STORMPATH_ENABLE_SETTINGS', True)
-
- # Configure URL mappings. These URL mappings control which URLs will be
- # used by Flask-Stormpath views.
- config.setdefault('STORMPATH_REGISTRATION_URL', '/register')
- config.setdefault('STORMPATH_LOGIN_URL', '/login')
- config.setdefault('STORMPATH_LOGOUT_URL', '/logout')
- config.setdefault('STORMPATH_FORGOT_PASSWORD_URL', '/forgot')
- config.setdefault('STORMPATH_FORGOT_PASSWORD_CHANGE_URL', '/forgot/change')
- config.setdefault('STORMPATH_SETTINGS_URL', '/settings')
- config.setdefault('STORMPATH_GOOGLE_LOGIN_URL', '/google')
- config.setdefault('STORMPATH_FACEBOOK_LOGIN_URL', '/facebook')
-
- # After a successful login, where should users be redirected?
- config.setdefault('STORMPATH_REDIRECT_URL', '/')
-
- # Cache configuration.
- config.setdefault('STORMPATH_CACHE', None)
-
- # Configure templates. These template settings control which templates are
- # used to render the Flask-Stormpath views.
- config.setdefault('STORMPATH_BASE_TEMPLATE', 'flask_stormpath/base.html')
- config.setdefault('STORMPATH_REGISTRATION_TEMPLATE', 'flask_stormpath/register.html')
- config.setdefault('STORMPATH_LOGIN_TEMPLATE', 'flask_stormpath/login.html')
- config.setdefault('STORMPATH_FORGOT_PASSWORD_TEMPLATE', 'flask_stormpath/forgot.html')
- config.setdefault('STORMPATH_FORGOT_PASSWORD_EMAIL_SENT_TEMPLATE', 'flask_stormpath/forgot_email_sent.html')
- config.setdefault('STORMPATH_FORGOT_PASSWORD_CHANGE_TEMPLATE', 'flask_stormpath/forgot_change.html')
- config.setdefault('STORMPATH_FORGOT_PASSWORD_COMPLETE_TEMPLATE', 'flask_stormpath/forgot_complete.html')
- config.setdefault('STORMPATH_SETTINGS_TEMPLATE', 'flask_stormpath/settings.html')
-
- # Social login configuration.
- config.setdefault('STORMPATH_SOCIAL', {})
-
- # Cookie configuration.
- config.setdefault('STORMPATH_COOKIE_DOMAIN', None)
- config.setdefault('STORMPATH_COOKIE_DURATION', timedelta(days=365))
-
- # Cookie name (this is not overridable by users, at least not explicitly).
- config.setdefault('REMEMBER_COOKIE_NAME', 'stormpath_token')
-
-
-def check_settings(config):
- """
- Ensure the user-specified settings are valid.
-
- This will raise a ConfigurationError if anything mandatory is not
- specified.
-
- :param dict config: The Flask app config.
- """
- if not (
- all([
- config['STORMPATH_API_KEY_ID'],
- config['STORMPATH_API_KEY_SECRET'],
- ]) or config['STORMPATH_API_KEY_FILE']
- ):
- raise ConfigurationError('You must define your Stormpath credentials.')
-
- if not config['STORMPATH_APPLICATION']:
- raise ConfigurationError('You must define your Stormpath application.')
-
- if config['STORMPATH_ENABLE_GOOGLE']:
- google_config = config['STORMPATH_SOCIAL'].get('GOOGLE')
-
- if not google_config or not all([
- google_config.get('client_id'),
- google_config.get('client_secret'),
- ]):
- raise ConfigurationError('You must define your Google app settings.')
-
- if config['STORMPATH_ENABLE_FACEBOOK']:
- facebook_config = config['STORMPATH_SOCIAL'].get('FACEBOOK')
-
- if not facebook_config or not all([
- facebook_config,
- facebook_config.get('app_id'),
- facebook_config.get('app_secret'),
- ]):
- raise ConfigurationError('You must define your Facebook app settings.')
-
- if config['STORMPATH_COOKIE_DOMAIN'] and not isinstance(config['STORMPATH_COOKIE_DOMAIN'], str):
- raise ConfigurationError('STORMPATH_COOKIE_DOMAIN must be a string.')
-
- if config['STORMPATH_COOKIE_DURATION'] and not isinstance(config['STORMPATH_COOKIE_DURATION'], timedelta):
- raise ConfigurationError('STORMPATH_COOKIE_DURATION must be a timedelta object.')
+import collections
+
+
+class StormpathSettings(collections.MutableMapping):
+ STORMPATH_PREFIX = 'STORMPATH'
+ DELIMITER = '_'
+ REGEX_SIGN = '*'
+ MAPPINGS = { # used for backwards compatibility
+ 'API_KEY_ID': 'client_apiKey_id',
+ 'API_KEY_SECRET': 'client_apiKey_secret',
+ 'APPLICATION': 'application_name',
+
+ 'ENABLE_LOGIN': 'web_login_enabled',
+ 'ENABLE_REGISTRATION': 'web_register_enabled',
+ 'ENABLE_FORGOT_PASSWORD': 'web_forgotPassword_enabled',
+
+ 'LOGIN_URL': 'web_login_uri',
+ 'REGISTRATION_URL': 'web_register_uri',
+ 'LOGOUT_URL': 'web_logout_uri',
+
+ 'REDIRECT_URL': 'web_login_nextUri',
+
+ 'REGISTRATION_TEMPLATE': 'web_register_template',
+ 'LOGIN_TEMPLATE': 'web_login_template',
+
+ 'REGISTRATION_REDIRECT_URL': 'web_register_nextUri',
+ 'REQUIRE_*': 'web_register_form_fields_*_required',
+ 'ENABLE_*': 'web_register_form_fields_*_enabled',
+
+ 'FORGOT_PASSWORD_TEMPLATE': 'web_forgotPassword_template',
+ 'FORGOT_PASSWORD_CHANGE_TEMPLATE': 'web_changePassword_template'
+ # 'FORGOT_PASSWORD_EMAIL_SENT_TEMPLATE'
+ # 'FORGOT_PASSWORD_COMPLETE_TEMPLATE'
+ # 'ENABLE_FACEBOOK'
+ # 'ENABLE_GOOGLE'
+ # 'SOCIAL'
+ # 'CACHE'
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.store = dict(*args, **kwargs)
+
+ @staticmethod
+ def _from_camel(key):
+ cs = []
+ for c in key:
+ cl = c.lower()
+ if c == cl:
+ cs.append(c)
+ else:
+ cs.append('_')
+ cs.append(c.lower())
+ return ''.join(cs).upper()
+
+ def __search__(self, root, key, root_string):
+ for node in root.keys():
+ search_string = '%s%s%s' % (
+ root_string, self.DELIMITER,
+ self._from_camel(node)
+ )
+ if key == search_string:
+ return root, node
+ if key.startswith(search_string):
+ return self.__search__(root[node], key, search_string)
+ raise KeyError
+
+ def __traverse__(self, parent, descendants):
+ child = descendants.pop(0)
+ if descendants:
+ if child not in parent:
+ parent[child] = {}
+ return self.__traverse__(parent[child], descendants)
+ return parent, child
+
+ def __nodematch__(self, key):
+ if key.startswith(self.STORMPATH_PREFIX):
+ store_key = key.lstrip(self.STORMPATH_PREFIX).strip(self.DELIMITER)
+ if store_key in self.MAPPINGS:
+ members = self.MAPPINGS[store_key].split(self.DELIMITER)
+ store = self.__traverse__(self.store, members)
+ else:
+ store = self.__search__(self.store, key, self.STORMPATH_PREFIX)
+ else:
+ store = self.store, key
+ return store
+
+ def __getitem__(self, key):
+ node, child = self.__nodematch__(key)
+ return node[child]
+
+ def __setitem__(self, key, value):
+ node, child = self.__nodematch__(key)
+ node[child] = value
+
+ def __delitem__(self, key):
+ node, child = self.__keytransform__(key)
+ del node[child]
+
+ def __contains__(self, key):
+ try:
+ # FIXME: passwordPolicy breaks the code, in
+ # stormpath-python-config.stormpath_config:strategies._enrich_with_directory_policies
+ # self.__nodematch__(key)
+ self[key]
+ return True
+ except KeyError:
+ return False
+
+ def __iter__(self):
+ return iter(self.store)
+
+ def __len__(self):
+ return len(self.store)
diff --git a/flask_stormpath/templates/flask_stormpath/base.html b/flask_stormpath/templates/flask_stormpath/base.html
index 64ccdf6..767cd25 100644
--- a/flask_stormpath/templates/flask_stormpath/base.html
+++ b/flask_stormpath/templates/flask_stormpath/base.html
@@ -15,6 +15,7 @@
html,
body {
height: 100%;
+ margin: auto;
}
@media (max-width: 767px) {
@@ -23,12 +24,12 @@
padding: 0 4px;
}
}
-
+
body {
margin-left: auto;
margin-right: auto;
}
-
+
body,
div,
p,
diff --git a/flask_stormpath/templates/flask_stormpath/forgot_change.html b/flask_stormpath/templates/flask_stormpath/change_password.html
similarity index 91%
rename from flask_stormpath/templates/flask_stormpath/forgot_change.html
rename to flask_stormpath/templates/flask_stormpath/change_password.html
index 4f437a9..adbddac 100644
--- a/flask_stormpath/templates/flask_stormpath/forgot_change.html
+++ b/flask_stormpath/templates/flask_stormpath/change_password.html
@@ -1,4 +1,4 @@
-{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+{% extends config['stormpath']['base_template'] %}
{% block title %}Change Your Password{% endblock %}
{% block description %}Change your password here.{% endblock %}
@@ -38,7 +38,7 @@
diff --git a/flask_stormpath/templates/flask_stormpath/forgot_complete.html b/flask_stormpath/templates/flask_stormpath/change_password_success.html
similarity index 84%
rename from flask_stormpath/templates/flask_stormpath/forgot_complete.html
rename to flask_stormpath/templates/flask_stormpath/change_password_success.html
index 0d83d1d..18ee9f5 100644
--- a/flask_stormpath/templates/flask_stormpath/forgot_complete.html
+++ b/flask_stormpath/templates/flask_stormpath/change_password_success.html
@@ -1,10 +1,10 @@
-{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+{% extends config['stormpath']['base_template'] %}
{% block title %}Password Change Complete{% endblock %}
{% block description %}You have successfully changed your password!{% endblock %}
{% block bodytag %}login{% endblock %}
{% block head %}
-
+
{% endblock %}
{% block body %}
diff --git a/flask_stormpath/templates/flask_stormpath/facebook_login_form.html b/flask_stormpath/templates/flask_stormpath/facebook_login_form.html
index 3609ffb..31c2b4a 100644
--- a/flask_stormpath/templates/flask_stormpath/facebook_login_form.html
+++ b/flask_stormpath/templates/flask_stormpath/facebook_login_form.html
@@ -7,17 +7,17 @@
if (response.status === 'connected') {
var queryStr = window.location.search.replace('?', '');
if (queryStr) {
- window.location.replace('{{ config['STORMPATH_FACEBOOK_LOGIN_URL'] }}?' + queryStr);
+ window.location.replace('{{ config['stormpath']['web']['social']['facebook']['login_url'] }}?' + queryStr);
} else {
- window.location.replace('{{ config['STORMPATH_FACEBOOK_LOGIN_URL'] }}');
+ window.location.replace('{{ config['stormpath']['web']['social']['facebook']['login_url'] }}');
}
}
- }, {scope: 'email{% if config["STORMPATH_SOCIAL"]["FACEBOOK"].get("scopes") %},{{ ",".join(config["STORMPATH_SOCIAL"]["FACEBOOK"]["scopes"]) }}{% endif %}'});
+ }, {scope: 'email{% if config["stormpath"]["web"]["social"]["facebook"].get("scopes") %},{{ ",".join(config["stormpath"]["web"]["social"]["facebook"]["scopes"]) }}{% endif %}'});
}
window.fbAsyncInit = function() {
FB.init({
- appId : '{{ config['STORMPATH_SOCIAL']['FACEBOOK']['app_id'] }}',
+ appId : '{{ config['stormpath']['web']['social']['facebook']['clientId'] }}',
cookie : true,
xfbml : true,
version : 'v2.0'
diff --git a/flask_stormpath/templates/flask_stormpath/forgot.html b/flask_stormpath/templates/flask_stormpath/forgot_password.html
similarity index 94%
rename from flask_stormpath/templates/flask_stormpath/forgot.html
rename to flask_stormpath/templates/flask_stormpath/forgot_password.html
index b8352b7..6af2d4a 100644
--- a/flask_stormpath/templates/flask_stormpath/forgot.html
+++ b/flask_stormpath/templates/flask_stormpath/forgot_password.html
@@ -1,4 +1,4 @@
-{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+{% extends config['stormpath']['base_template'] %}
{% block title %}Forgot Your Password?{% endblock %}
{% block description %}Forgot your password? No worries!{% endblock %}
@@ -41,7 +41,7 @@
- {% if config['STORMPATH_ENABLE_LOGIN'] %}
+ {% if config['stormpath']['web']['login']['enabled'] %}
Back to Log In
{% endif %}
diff --git a/flask_stormpath/templates/flask_stormpath/forgot_email_sent.html b/flask_stormpath/templates/flask_stormpath/forgot_password_success.html
similarity index 94%
rename from flask_stormpath/templates/flask_stormpath/forgot_email_sent.html
rename to flask_stormpath/templates/flask_stormpath/forgot_password_success.html
index a2072c0..f5defe9 100644
--- a/flask_stormpath/templates/flask_stormpath/forgot_email_sent.html
+++ b/flask_stormpath/templates/flask_stormpath/forgot_password_success.html
@@ -1,4 +1,4 @@
-{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+{% extends config['stormpath']['base_template'] %}
{% block title %}Password Reset Email Sent{% endblock %}
{% block description %}Your password reset email has been sent!{% endblock %}
diff --git a/flask_stormpath/templates/flask_stormpath/google_login_form.html b/flask_stormpath/templates/flask_stormpath/google_login_form.html
index a605b7c..71fe72e 100644
--- a/flask_stormpath/templates/flask_stormpath/google_login_form.html
+++ b/flask_stormpath/templates/flask_stormpath/google_login_form.html
@@ -1,6 +1,6 @@
diff --git a/flask_stormpath/templates/flask_stormpath/login.html b/flask_stormpath/templates/flask_stormpath/login.html
index b4f924e..dd5ff2b 100644
--- a/flask_stormpath/templates/flask_stormpath/login.html
+++ b/flask_stormpath/templates/flask_stormpath/login.html
@@ -1,4 +1,4 @@
-{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+{% extends config['stormpath']['base_template'] %}
{% block title %}Log In{% endblock %}
{% block description %}Log into your account!{% endblock %}
@@ -9,10 +9,10 @@
-
+
@@ -27,41 +27,37 @@
{% endwith %}
- {% if config['STORMPATH_ENABLE_FORGOT_PASSWORD'] %}
+ {% if config['stormpath']['web']['forgotPassword']['enabled'] %}
Forgot Password?
{% endif %}
diff --git a/flask_stormpath/templates/flask_stormpath/register.html b/flask_stormpath/templates/flask_stormpath/register.html
index f8b1184..db56576 100644
--- a/flask_stormpath/templates/flask_stormpath/register.html
+++ b/flask_stormpath/templates/flask_stormpath/register.html
@@ -1,4 +1,4 @@
-{% extends config['STORMPATH_BASE_TEMPLATE'] %}
+{% extends config['stormpath']['base_template'] %}
{% block title %}Create an Account{% endblock %}
{% block description %}Create a new account.{% endblock %}
@@ -24,71 +24,23 @@
{% endif %}
{% endwith %}
- {% if config['STORMPATH_ENABLE_USERNAME'] %}
+ {% for field in form if field.widget.input_type != 'hidden' %}
- {% endif %}
- {% if config['STORMPATH_ENABLE_GIVEN_NAME'] %}
-
- {% endif %}
- {% if config['STORMPATH_ENABLE_MIDDLE_NAME'] %}
-
- {% endif %}
- {% if config['STORMPATH_ENABLE_SURNAME'] %}
-
- {% endif %}
-
-
+ {% endfor %}
- {% if config['STORMPATH_ENABLE_LOGIN'] %}
+ {% if config['stormpath']['web']['login']['enabled'] %}
Back to Log In
{% endif %}
diff --git a/flask_stormpath/templates/flask_stormpath/verify_email.html b/flask_stormpath/templates/flask_stormpath/verify_email.html
new file mode 100644
index 0000000..92e6a7d
--- /dev/null
+++ b/flask_stormpath/templates/flask_stormpath/verify_email.html
@@ -0,0 +1,46 @@
+{% extends config['stormpath']['base_template'] %}
+
+{% block title %}Resend Verification Email{% endblock %}
+{% block description %}Set your verification email here.{% endblock %}
+{% block bodytag %}login{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/flask_stormpath/views.py b/flask_stormpath/views.py
index f00f4a5..315ef22 100644
--- a/flask_stormpath/views.py
+++ b/flask_stormpath/views.py
@@ -1,8 +1,7 @@
"""Our pluggable views."""
-import sys
-from facebook import get_user_from_cookie
+import json
from flask import (
abort,
current_app,
@@ -10,22 +9,151 @@
redirect,
render_template,
request,
+ make_response
)
-from flask_login import login_user
+from flask.views import View
+from flask_login import (
+ login_user, logout_user, login_required, current_user)
from six import string_types
from stormpath.resources.provider import Provider
-
-from . import StormpathError, logout_user
-from .forms import (
- ChangePasswordForm,
- ForgotPasswordForm,
- LoginForm,
- RegistrationForm,
-)
+from stormpath.resources import Expansion
+from . import StormpathError
+from .forms import StormpathForm
from .models import User
+from .request_processors import get_accept_header, request_wants_json
+from facebook import get_user_from_cookie
-def register():
+""" Views parent class. """
+
+
+class StormpathView(View):
+ """
+ Class for Stormpath views.
+
+ This class initializes form building through config specs and handles
+ both html and json responses.
+ Specialized logic for each view is handled in the process_request method
+ and should be specified on each child class.
+ """
+
+ def __init__(self, config, *args, **kwargs):
+ self.config = config
+ self.accept_header = get_accept_header()
+ self.allowed_types = current_app.config['stormpath']['web']['produces']
+
+ # Set a default value for the error redirect uri.
+ self.error_redirect_url = None
+
+ # If the request type is specified, but not html or json, mark the
+ # invalid_request flag.
+ if self.accept_header not in self.allowed_types:
+ self.invalid_request = True
+ else:
+ self.invalid_request = False
+
+ # Build the form
+ if request_wants_json():
+ form_kwargs = {'meta': {'csrf': False}}
+ else:
+ form_kwargs = {
+ 'meta': {'csrf': current_app.config['WTF_CSRF_ENABLED']}}
+ self.form = StormpathForm.specialize_form(
+ config.get('form'))(**form_kwargs)
+
+ def make_stormpath_response(
+ self, data, template=None, return_json=True, status_code=200):
+ """ Create a response based on request type (html or json). """
+ if return_json:
+ stormpath_response = make_response(data, status_code)
+ stormpath_response.mimetype = 'application/json'
+ else:
+ stormpath_response = render_template(template, **data)
+ return stormpath_response
+
+ def process_request(self):
+ """ Custom logic specialized for each view. Must be implemented in
+ the subclass. """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def process_stormpath_error(self, error):
+ """ Check for StormpathErrors. """
+
+ # If an error.user_message is inadequate, then catch regular message.
+ # Otherwise try to catch user_message.
+ if error.code in [7102, ]:
+ error_message = error.message
+ else:
+ error_message = (
+ error.user_message if error.user_message else error.message)
+
+ if request_wants_json():
+ status_code = error.status if error.status else 400
+ return self.make_stormpath_response(
+ data=json.dumps({
+ 'status': status_code,
+ 'message': error_message}),
+ status_code=status_code)
+ flash(error_message)
+ return None
+
+ def dispatch_request(self):
+ """ Basic view skeleton. """
+
+ # If the request is not valid, pass the response to the
+ # 'invalid_request' view.
+ if self.invalid_request:
+ invalid_request_uri = current_app.config[
+ 'stormpath']['web']['invalidRequest']['uri']
+ endpoints = [
+ rule.rule for rule in current_app.url_map.iter_rules()]
+
+ # Redirect to a flask view for invalid requests (if implemented).
+ # If not, return a 501.
+ if invalid_request_uri in endpoints:
+ return redirect(invalid_request_uri)
+ else:
+ abort(501)
+
+ # Redirect to an error uri if one is set.
+ if self.error_redirect_url:
+ return redirect(self.error_redirect_url)
+
+ if request.method == 'POST':
+ # If we received a POST request with valid information, we'll
+ # continue processing.
+
+ if not self.form.validate_on_submit():
+ # If form.data is not valid, return error messages.
+ if request_wants_json():
+ return self.make_stormpath_response(
+ data=json.dumps({
+ 'status': 400,
+ 'message': self.form.errors}),
+ status_code=400)
+ for field_error in self.form.errors.keys():
+ flash(self.form.errors[field_error][0])
+
+ else:
+ try:
+ return self.process_request()
+ except StormpathError as error:
+ stormpath_error = self.process_stormpath_error(error)
+ if stormpath_error:
+ return stormpath_error
+
+ if request_wants_json():
+ return self.make_stormpath_response(data=self.form.json)
+
+ return self.make_stormpath_response(
+ template=self.template, data={'form': self.form},
+ return_json=False)
+
+
+""" Child views. """
+
+
+class RegisterView(StormpathView):
"""
Register a new user with Stormpath.
@@ -36,55 +164,55 @@ def register():
template that is used to render this page can all be controlled via
Flask-Stormpath settings.
"""
- form = RegistrationForm(config=current_app.config)
-
- # If we received a POST request with valid information, we'll continue
- # processing.
- if form.validate_on_submit():
- data = form.data
- # Attempt to create the user's account on Stormpath.
- try:
- # Create the user account on Stormpath. If this fails, an
- # exception will be raised.
- optional_params = {k: data[k] for k in ('username','middle_name','custom_data', 'status') if k in data}
- account = User.create(
- data.get('email'),
- data.get('password'),
- # Since Stormpath requires both the given_name and surname
- # fields be set, we'll just set the both to 'Anonymous' if
- # the user has # explicitly said they don't want to collect
- # those fields.
- data.get('given_name', 'Anonymous') or 'Anonymous',
- data.get('surname', 'Anonymous') or 'Anonymous',
- **optional_params
- )
-
- # If we're able to successfully create the user's account,
- # we'll log the user in (creating a secure session using
- # Flask-Login), then redirect the user to the
- # STORMPATH_REDIRECT_URL setting.
+ template = 'flask_stormpath/register.html'
+
+ def __init__(self, *args, **kwargs):
+ config = current_app.config['stormpath']['web']['register']
+ super(RegisterView, self).__init__(config, *args, **kwargs)
+ self.data = self.form.data
+
+ def process_request(self):
+ # We'll just set the field values to 'Anonymous' if the user
+ # has explicitly said they don't want to collect those fields.
+ for field in ['given_name', 'surname']:
+ if not self.data.get(field):
+ self.data[field] = 'Anonymous'
+ # Remove the confirmation password so it won't cause an error
+ if 'confirm_password' in self.data:
+ self.data.pop('confirm_password')
+
+ # Create the user account on Stormpath. If this fails, an
+ # exception will be raised.
+ self.data.pop('csrf_token', None)
+ account = User.create(**self.data)
+
+ # If verifyEmail is enabled, send a verification email.
+ # FIXME: Edit templates to show a verification email has been sent.
+ # FIXME: error message is None
+ pass
+
+ # If we're able to successfully create the user's account,
+ # we'll log the user in (creating a secure session using
+ # Flask-Login), then redirect the user to the
+ # Stormpath login nextUri setting but only if autoLogin is enabled.
+ if (self.config['autoLogin'] and not current_app.config[
+ 'stormpath']['web']['verifyEmail']['enabled']):
login_user(account, remember=True)
- # The email address must be verified, so pop an alert about it.
- if current_app.config['STORMPATH_VERIFY_EMAIL'] is True:
- flash('You must validate your email address before logging in. Please check your email for instructions.')
-
- if 'STORMPATH_REGISTRATION_REDIRECT_URL' in current_app.config:
- redirect_url = current_app.config['STORMPATH_REGISTRATION_REDIRECT_URL']
- else:
- redirect_url = current_app.config['STORMPATH_REDIRECT_URL']
- return redirect(redirect_url)
-
- except StormpathError as err:
- flash(err.message)
+ if request_wants_json():
+ return self.make_stormpath_response(data=account.to_json())
- return render_template(
- current_app.config['STORMPATH_REGISTRATION_TEMPLATE'],
- form = form,
- )
+ # Set redirect priority
+ redirect_url = self.config['nextUri']
+ if not redirect_url:
+ redirect_url = current_app.config['stormpath'][
+ 'web']['login']['nextUri']
+ if not redirect_url:
+ redirect_url = '/'
+ return redirect(redirect_url)
-def login():
+class LoginView(StormpathView):
"""
Log in an existing Stormpath user.
@@ -95,33 +223,37 @@ def login():
template that is used to render this page can all be controlled via
Flask-Stormpath settings.
"""
- form = LoginForm()
+ template = 'flask_stormpath/login.html'
- # If we received a POST request with valid information, we'll continue
- # processing.
- if form.validate_on_submit():
- try:
- # Try to fetch the user's account from Stormpath. If this
- # fails, an exception will be raised.
- account = User.from_login(form.login.data, form.password.data)
-
- # If we're able to successfully retrieve the user's account,
- # we'll log the user in (creating a secure session using
- # Flask-Login), then redirect the user to the ?next=
- # query parameter, or the STORMPATH_REDIRECT_URL setting.
- login_user(account, remember=True)
+ def __init__(self, *args, **kwargs):
+ config = current_app.config['stormpath']['web']['login']
+ super(LoginView, self).__init__(config, *args, **kwargs)
+
+ def process_request(self):
+ # Try to fetch the user's account from Stormpath. If this
+ # fails, an exception will be raised.
+ account = User.from_login(
+ self.form.login.data, self.form.password.data)
+
+ # If we're able to successfully retrieve the user's account,
+ # we'll log the user in (creating a secure session using
+ # Flask-Login), then redirect the user to the ?next=
+ # query parameter, or the Stormpath login nextUri setting.
+ login_user(account, remember=True)
- return redirect(request.args.get('next') or current_app.config['STORMPATH_REDIRECT_URL'])
- except StormpathError as err:
- flash(err.message)
+ if request_wants_json():
+ return self.make_stormpath_response(data=current_user.to_json())
- return render_template(
- current_app.config['STORMPATH_LOGIN_TEMPLATE'],
- form = form,
- )
+ # Set redirect priority
+ redirect_url = request.args.get('next')
+ if not redirect_url:
+ redirect_url = self.config['nextUri']
+ if not redirect_url:
+ redirect_url = '/'
+ return redirect(redirect_url)
-def forgot():
+class ForgotPasswordView(StormpathView):
"""
Initialize 'password reset' functionality for a user who has forgotten his
password.
@@ -132,42 +264,51 @@ def forgot():
The URL this view is bound to, and the template that is used to render
this page can all be controlled via Flask-Stormpath settings.
"""
- form = ForgotPasswordForm()
-
- # If we received a POST request with valid information, we'll continue
- # processing.
- if form.validate_on_submit():
- try:
- # Try to fetch the user's account from Stormpath. If this
- # fails, an exception will be raised.
- account = current_app.stormpath_manager.application.send_password_reset_email(form.email.data)
- account.__class__ = User
-
- # If we're able to successfully send a password reset email to this
- # user, we'll display a success page prompting the user to check
- # their inbox to complete the password reset process.
- return render_template(
- current_app.config['STORMPATH_FORGOT_PASSWORD_EMAIL_SENT_TEMPLATE'],
- user = account,
- )
- except StormpathError as err:
- # If the error message contains 'https', it means something failed
- # on the network (network connectivity, most likely).
- if isinstance(err.message, string_types) and 'https' in err.message.lower():
- flash('Something went wrong! Please try again.')
-
- # Otherwise, it means the user is trying to reset an invalid email
- # address.
- else:
- flash('Invalid email address.')
-
- return render_template(
- current_app.config['STORMPATH_FORGOT_PASSWORD_TEMPLATE'],
- form = form,
- )
-
-
-def forgot_change():
+ template = 'flask_stormpath/forgot_password.html'
+ template_success = 'flask_stormpath/forgot_password_success.html'
+
+ def __init__(self, *args, **kwargs):
+ config = current_app.config['stormpath']['web']['forgotPassword']
+ super(ForgotPasswordView, self).__init__(config, *args, **kwargs)
+
+ def process_stormpath_error(self, error):
+ # If the error message contains 'https', it means something
+ # failed on the network (network connectivity, most likely).
+ if (isinstance(error.message, string_types) and
+ 'https' in error.message.lower()):
+ error.user_message = 'Something went wrong! Please try again.'
+
+ # Otherwise, it means the user is trying to reset an invalid
+ # email address.
+ else:
+ error.user_message = 'Invalid email address.'
+ return super(ForgotPasswordView, self).process_stormpath_error(error)
+
+ def process_request(self):
+ # Try to fetch the user's account from Stormpath. If this
+ # fails, an exception will be raised.
+ account = (
+ current_app.stormpath_manager.application.
+ send_password_reset_email(self.form.email.data))
+ account.__class__ = User
+
+ # If we're able to successfully send a password reset email to
+ # this user, we'll display a success page prompting the user
+ # to check their inbox to complete the password reset process.
+
+ if request_wants_json():
+ return self.make_stormpath_response(
+ data=json.dumps({
+ 'status': 200,
+ 'message': {'email': self.form.data.get('email')}}),
+ status_code=200)
+
+ return self.make_stormpath_response(
+ template=self.template_success, data={'user': account},
+ return_json=False)
+
+
+class ChangePasswordView(StormpathView):
"""
Allow a user to change his password.
@@ -178,44 +319,268 @@ def forgot_change():
The URL this view is bound to, and the template that is used to render
this page can all be controlled via Flask-Stormpath settings.
"""
- try:
- account = current_app.stormpath_manager.application.verify_password_reset_token(request.args.get('sptoken'))
- except StormpathError as err:
- abort(400)
-
- form = ChangePasswordForm()
+ template = "flask_stormpath/change_password.html"
+ template_success = "flask_stormpath/change_password_success.html"
- # If we received a POST request with valid information, we'll continue
- # processing.
- if form.validate_on_submit():
+ def __init__(self, *args, **kwargs):
+ config = current_app.config['stormpath']['web']['changePassword']
+ super(ChangePasswordView, self).__init__(config, *args, **kwargs)
try:
- # Update this user's passsword.
- account.password = form.password.data
- account.save()
+ self.account = (
+ current_app.stormpath_manager.application.
+ verify_password_reset_token(request.args.get('sptoken')))
+ except StormpathError:
+ self.error_redirect_url = config['errorUri']
+ if not self.error_redirect_url:
+ self.error_redirect_url = '/'
+
+ def process_stormpath_error(self, error):
+ # If the error message contains 'https', it means something
+ # failed on the network (network connectivity, most likely).
+ if (isinstance(error.message, string_types) and
+ 'https' in error.message.lower()):
+ error.user_message = 'Something went wrong! Please try again.'
+ return super(ChangePasswordView, self).process_stormpath_error(error)
+
+ def process_request(self):
+ # Update this user's passsword.
+ self.account.password = self.form.password.data
+ self.account.save()
+
+ # Log this user into their account.
+ account = User.from_login(self.account.email, self.form.password.data)
+ login_user(account, remember=True)
+
+ if request_wants_json():
+ return self.make_stormpath_response(data=current_user.to_json())
+
+ return self.make_stormpath_response(
+ template=self.template_success, data={'form': self.form},
+ return_json=False)
+
+
+class VerifyEmailView(StormpathView):
+ """
+ Verify a newly created Stormpath user.
- # Log this user into their account.
- account = User.from_login(account.email, form.password.data)
- login_user(account, remember=True)
+ This view will activate a user's account with the token specified in the
+ activation link the user received via email. If the token is invalid or
+ missing, the user can request a new activation link.
- return render_template(current_app.config['STORMPATH_FORGOT_PASSWORD_COMPLETE_TEMPLATE'])
- except StormpathError as err:
- if isinstance(err.message, string_types) and 'https' in err.message.lower():
- flash('Something went wrong! Please try again.')
+ The URL this view is bound to, and the template that is used to render
+ this page can all be controlled via Flask-Stormpath settings.
+ """
+ template = "flask_stormpath/verify_email.html"
+
+ def __init__(self, *args, **kwargs):
+ config = current_app.config['stormpath']['web']['verifyEmail']
+ super(VerifyEmailView, self).__init__(config, *args, **kwargs)
+
+ def process_stormpath_error(self, error):
+ # If the error message contains 'https', it means something
+ # failed on the network (network connectivity, most likely).
+ if (isinstance(error.message, string_types) and
+ 'https' in error.message.lower()):
+ error.user_message = 'Something went wrong! Please try again.'
+ return super(VerifyEmailView, self).process_stormpath_error(error)
+
+ def dispatch_request(self):
+ # If the request is POST, resend the confirmation email.
+ if request.method == 'POST':
+ # If form.data is not valid, return error messages.
+ if not self.form.validate_on_submit():
+ if request_wants_json():
+ return self.make_stormpath_response(
+ data=json.dumps({
+ 'status': 400,
+ 'message': self.form.errors}),
+ status_code=400)
+ for field_error in self.form.errors.keys():
+ flash(self.form.errors[field_error][0])
+ redirect_url = request.url
else:
- flash(err.message)
+ # Try to retrieve an account associated with the email.
+ search_query = (
+ current_app.stormpath_manager.client.tenant.accounts.
+ search(self.form.data.get('email')))
+
+ if search_query.items:
+ account = search_query.items[0]
+
+ # Resend the activation token
+ (current_app.stormpath_manager.application.
+ verification_emails.resend(account, account.directory))
+
+ if request_wants_json():
+ return self.make_stormpath_response(
+ data=json.dumps({}))
+ redirect_url = self.config['unverifiedUri']
+
+ # If the request is GET, try to parse and verify the authorization
+ # token.
+ else:
+ verification_token = request.args.get('sptoken', '')
+ try:
+ # Try to verify the sptoken.
+ account = (
+ current_app.stormpath_manager.client.accounts.
+ verify_email_token(verification_token)
+ )
+ account.__class__ = User
+
+ # If autologin is enabled, log the user in and redirect him
+ # to login nextUri. If not, redirect to verifyEmail nextUri.
+ if current_app.config[
+ 'stormpath']['web']['register']['autoLogin']:
+ login_user(account, remember=True)
+ account.refresh()
+ if request_wants_json():
+ return self.make_stormpath_response(
+ data=account.to_json())
+ redirect_url = current_app.config[
+ 'stormpath']['web']['login']['nextUri']
+ else:
+ if request_wants_json():
+ return self.make_stormpath_response(
+ data=json.dumps({}))
+ redirect_url = self.config['nextUri']
+
+ except StormpathError as error:
+ # If the sptoken is invalid or missing, render an email
+ # form that will resend an sptoken to the new email provided.
+
+ # Sets an error message.
+ if request_wants_json():
+ if error.status == 400:
+ error_message = 'sptoken parameter not provided.'
+ else:
+ error_message = (
+ error.user_message if error.user_message
+ else error.message)
+
+ return self.make_stormpath_response(
+ data=json.dumps({
+ 'status': error.status,
+ 'message': error_message}),
+ status_code=error.status)
+
+ return self.make_stormpath_response(
+ template=self.template, data={'form': self.form},
+ return_json=False)
+
+ # Set redirect priority
+ if not redirect_url:
+ redirect_url = '/'
+ return redirect(redirect_url)
+
+
+class LogoutView(StormpathView):
+ """
+ Log a user out of their account.
+
+ This view will log a user out of their account (destroying their session),
+ then redirect the user to the home page of the site.
+
+ .. note::
+ We'll override the default StormpathView logic, since we don't need
+ form and api request validation.
+ """
- # If this is a POST request, and the form isn't valid, this means the
- # user's password was no good, so we'll display a message.
- elif request.method == 'POST':
- flash("Passwords don't match.")
+ def __init__(self, *args, **kwargs):
+ config = current_app.config['stormpath']['web']['logout'].copy()
- return render_template(
- current_app.config['STORMPATH_FORGOT_PASSWORD_CHANGE_TEMPLATE'],
- form = form,
- )
+ # We'll pass login form here since logout needs the form for the json
+ # response. (Successful logout redirects to login view.)
+ config['form'] = current_app.config['stormpath']['web']['login'][
+ 'form']
+ super(LogoutView, self).__init__(config, *args, **kwargs)
+ def dispatch_request(self):
+ logout_user()
-def facebook_login():
+ # Set redirect priority
+ redirect_url = self.config['nextUri']
+ if not redirect_url:
+ redirect_url = '/'
+ return redirect(redirect_url)
+
+
+class MeView(View):
+ """
+ Get a JSON object with the current user information.
+
+ .. note::
+ We'll override the default StormpathView logic, since we don't need
+ json support or form and api request validation.
+ """
+ decorators = [login_required]
+
+ def dispatch_request(self):
+ expansion = Expansion()
+ for attr, flag in current_app.config['stormpath']['web']['me'][
+ 'expand'].items():
+ if flag:
+ expansion.add_property(attr)
+ if expansion.items:
+ current_user._expand = expansion
+ current_user.refresh()
+
+ response = make_response(current_user.to_json(), 200)
+ response.mimetype = 'application/json'
+ return response
+
+
+""" Social views. """
+
+
+class SocialView(View):
+ """ Parent class for social login views. """
+ def __init__(self, *args, **kwargs):
+ # First validate social view call
+ social_name = kwargs.pop('social_name')
+ if social_name not in ['facebook', 'google']:
+ raise ValueError('Social service is not supported.')
+ self.social_name = social_name
+
+ # Then set the access token and the provider
+ self.access_token = kwargs.pop('access_token')
+ self.provider_social = getattr(Provider, self.social_name.upper())
+
+ # Set a user error message in case the login fails.
+ self.error_message = (
+ 'Oops! We encountered an unexpected error. Please contact ' +
+ 'support and explain what you were doing at the time this ' +
+ 'error occurred.'
+ )
+
+ def get_account(self):
+ return getattr(
+ User, 'from_%s' % self.social_name)(self.access_token)
+
+ def dispatch_request(self):
+ """ Basic social view skeleton. """
+ # We'll try to have Stormpath either create or update this user's
+ # Stormpath account, by automatically handling the social API stuff
+ # for us.
+ try:
+ account = self.get_account()
+ except StormpathError:
+ flash(self.error_message)
+ redirect_url = current_app.config[
+ 'stormpath']['web']['login']['uri']
+ return redirect(redirect_url if redirect_url else '/')
+
+ # Now we'll log the new user into their account. From this point on,
+ # this social user will be treated exactly like a normal Stormpath
+ # user!
+ login_user(account, remember=True)
+
+ return redirect(
+ request.args.get('next') or
+ current_app.config['stormpath']['web']['login']['nextUri'])
+
+
+class FacebookLoginView(SocialView):
"""
Handle Facebook login.
@@ -228,8 +593,8 @@ def facebook_login():
- Read the user's session using the Facebook SDK, extracting the user's
Facebook access token.
- - Once we have the user's access token, we send it to Stormpath, so that
- we can either create (or update) the user on Stormpath's side.
+ - Once we have the user's access token, we send it to Stormpath, so
+ that we can either create (or update) the user on Stormpath's side.
- Then we retrieve the Stormpath account object for the user, and log
them in using our normal session support (powered by Flask-Login).
@@ -240,71 +605,27 @@ def facebook_login():
The location this view redirects users to can be configured via
Flask-Stormpath settings.
"""
- # First, we'll try to grab the Facebook user's data by accessing their
- # session data.
- facebook_user = get_user_from_cookie(
- request.cookies,
- current_app.config['STORMPATH_SOCIAL']['FACEBOOK']['app_id'],
- current_app.config['STORMPATH_SOCIAL']['FACEBOOK']['app_secret'],
- )
-
- # Now, we'll try to have Stormpath either create or update this user's
- # Stormpath account, by automatically handling the Facebook Graph API stuff
- # for us.
- try:
- account = User.from_facebook(facebook_user['access_token'])
- except StormpathError as err:
- social_directory_exists = False
-
- # If we failed here, it usually means that this application doesn't have
- # a Facebook directory -- so we'll create one!
- for asm in current_app.stormpath_manager.application.account_store_mappings:
-
- # If there is a Facebook directory, we know this isn't the problem.
- if (
- getattr(asm.account_store, 'provider') and
- asm.account_store.provider.provider_id == Provider.FACEBOOK
- ):
- social_directory_exists = True
- break
-
- # If there is a Facebook directory already, we'll just pass on the
- # exception we got.
- if social_directory_exists:
- raise err
-
- # Otherwise, we'll try to create a Facebook directory on the user's
- # behalf (magic!).
- dir = current_app.stormpath_manager.client.directories.create({
- 'name': current_app.stormpath_manager.application.name + '-facebook',
- 'provider': {
- 'client_id': current_app.config['STORMPATH_SOCIAL']['FACEBOOK']['app_id'],
- 'client_secret': current_app.config['STORMPATH_SOCIAL']['FACEBOOK']['app_secret'],
- 'provider_id': Provider.FACEBOOK,
- },
- })
-
- # Now that we have a Facebook directory, we'll map it to our application
- # so it is active.
- asm = current_app.stormpath_manager.application.account_store_mappings.create({
- 'application': current_app.stormpath_manager.application,
- 'account_store': dir,
- 'list_index': 99,
- 'is_default_account_store': False,
- 'is_default_group_store': False,
- })
-
- # Lastly, let's retry the Facebook login one more time.
- account = User.from_facebook(facebook_user['access_token'])
-
- # Now we'll log the new user into their account. From this point on, this
- # Facebook user will be treated exactly like a normal Stormpath user!
- login_user(account, remember=True)
-
- return redirect(request.args.get('next') or current_app.config['STORMPATH_REDIRECT_URL'])
-
-
-def google_login():
+ def __init__(self, *args, **kwargs):
+ # We'll try to grab the Facebook user's data by accessing their
+ # session data. If this doesn't exist, we'll abort with a
+ # 400 BAD REQUEST (since something horrible must have happened).
+ facebook_user = get_user_from_cookie(
+ request.cookies,
+ current_app.config[
+ 'stormpath']['web']['social']['facebook']['clientId'],
+ current_app.config[
+ 'stormpath']['web']['social']['facebook']['clientSecret'],
+ )
+ if facebook_user:
+ access_token = facebook_user.get('access_token')
+ else:
+ abort(400)
+
+ super(FacebookLoginView, self).__init__(
+ social_name='facebook', access_token=access_token)
+
+
+class GoogleLoginView(SocialView):
"""
Handle Google login.
@@ -319,75 +640,13 @@ def google_login():
The location this view redirects users to can be configured via
Flask-Stormpath settings.
"""
- # First, we'll try to grab the 'code' query string that Google should be
- # passing to us. If this doesn't exist, we'll abort with a 400 BAD REQUEST
- # (since something horrible must have happened).
- code = request.args.get('code')
- if not code:
- abort(400)
-
- # Next, we'll try to have Stormpath either create or update this user's
- # Stormpath account, by automatically handling the Google API stuff for us.
- try:
- account = User.from_google(code)
- except StormpathError as err:
- social_directory_exists = False
-
- # If we failed here, it usually means that this application doesn't
- # have a Google directory -- so we'll create one!
- for asm in current_app.stormpath_manager.application.account_store_mappings:
-
- # If there is a Google directory, we know this isn't the problem.
- if (
- getattr(asm.account_store, 'provider') and
- asm.account_store.provider.provider_id == Provider.GOOGLE
- ):
- social_directory_exists = True
- break
-
- # If there is a Google directory already, we'll just pass on the
- # exception we got.
- if social_directory_exists:
- raise err
-
- # Otherwise, we'll try to create a Google directory on the user's
- # behalf (magic!).
- dir = current_app.stormpath_manager.client.directories.create({
- 'name': current_app.stormpath_manager.application.name + '-google',
- 'provider': {
- 'client_id': current_app.config['STORMPATH_SOCIAL']['GOOGLE']['client_id'],
- 'client_secret': current_app.config['STORMPATH_SOCIAL']['GOOGLE']['client_secret'],
- 'redirect_uri': request.url_root[:-1] + current_app.config['STORMPATH_GOOGLE_LOGIN_URL'],
- 'provider_id': Provider.GOOGLE,
- },
- })
-
- # Now that we have a Google directory, we'll map it to our application
- # so it is active.
- asm = current_app.stormpath_manager.application.account_store_mappings.create({
- 'application': current_app.stormpath_manager.application,
- 'account_store': dir,
- 'list_index': 99,
- 'is_default_account_store': False,
- 'is_default_group_store': False,
- })
-
- # Lastly, let's retry the Google login one more time.
- account = User.from_google(code)
-
- # Now we'll log the new user into their account. From this point on, this
- # Google user will be treated exactly like a normal Stormpath user!
- login_user(account, remember=True)
-
- return redirect(request.args.get('next') or current_app.config['STORMPATH_REDIRECT_URL'])
-
-
-def logout():
- """
- Log a user out of their account.
-
- This view will log a user out of their account (destroying their session),
- then redirect the user to the home page of the site.
- """
- logout_user()
- return redirect('/')
+ def __init__(self, *args, **kwargs):
+ # We'll try to grab the 'code' query string that Google should
+ # be passing to us. If this doesn't exist, we'll abort with a
+ # 400 BAD REQUEST (since something horrible must have happened).
+ code = request.args.get('code')
+ if not code:
+ abort(400)
+
+ super(GoogleLoginView, self).__init__(
+ social_name='google', access_token=code)
diff --git a/setup.py b/setup.py
index 3bb73f2..dd39a8d 100644
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,7 @@ def run(self):
setup(
name = 'Flask-Stormpath',
- version = '0.4.8',
+ version = '0.5.0',
url = 'https://github.com/stormpath/stormpath-flask',
license = 'Apache',
author = 'Stormpath, Inc.',
@@ -48,21 +48,22 @@ def run(self):
include_package_data = True,
platforms = 'any',
install_requires = [
- 'Flask>=0.9.0',
- 'Flask-Login==0.3.2',
+ 'Flask>=0.11.1',
+ 'Flask-Login==0.4.0',
'Flask-WTF>=0.13.1',
'facebook-sdk==2.0.0',
- 'oauth2client==1.5.2',
- 'stormpath==2.4.4',
+ 'oauth2client==4.0.0',
+ 'stormpath==2.4.5',
'blinker==1.4'
],
extras_require = {
- 'test': ['coverage', 'pytest', 'pytest-cov', 'python-coveralls', 'Sphinx==1.3.6', 'pytest-xdist'],
+ 'test': ['coverage', 'pytest', 'pytest-cov', 'pytest-env', 'python-coveralls', 'Sphinx', 'pytest-xdist', 'ruamel.yaml'],
},
dependency_links=[
- 'git+git://github.com/pythonforfacebook/facebook-sdk.git@e65d06158e48388b3932563f1483ca77065951b3#egg=facebook-sdk-1.0.0-alpha',
+ 'git+git://github.com/pythonforfacebook/facebook-sdk.git@e65d06158' +
+ 'e48388b3932563f1483ca77065951b3#egg=facebook-sdk-1.0.0-alpha',
],
- classifiers = [
+ classifiers=[
'Environment :: Web Environment',
'Framework :: Flask',
'Intended Audience :: Developers',
@@ -80,6 +81,5 @@ def run(self):
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
],
)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..7493654
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,11 @@
+from .helpers import bootstrap_client
+
+
+def pytest_keyboard_interrupt(excinfo):
+ collection_resources = ['applications', 'directories']
+ test_prefix = 'flask-stormpath-tests'
+ client = bootstrap_client()
+
+ for collection in collection_resources:
+ for resource in list(getattr(client, collection).search(test_prefix)):
+ resource.delete()
diff --git a/tests/helpers.py b/tests/helpers.py
index 0b2327b..1b18808 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -6,20 +6,24 @@
"""
-from os import environ
+import os
from unittest import TestCase
from uuid import uuid4
from flask import Flask
-from flask_stormpath import StormpathManager
+from flask_stormpath import StormpathManager, StormpathError, User
+from facebook import GraphAPI, GraphAPIError
from stormpath.client import Client
-from stormpath.error import Error
+from stormpath.resources.provider import Provider
+from oauth2client.client import OAuth2WebServerFlow
+import requests
+import json
class StormpathTestCase(TestCase):
"""
- Custom test case which bootstraps a Stormpath client, application, and Flask
- app.
+ Custom test case which bootstraps a Stormpath client, application, and
+ Flask app.
This makes writing tests significantly easier as there's no work to do for
setUp / tearDown.
@@ -30,31 +34,99 @@ class StormpathTestCase(TestCase):
def setUp(self):
"""Provision a new Client, Application, and Flask app."""
self.client = bootstrap_client()
- self.application = bootstrap_app(self.client)
+ self.name = 'flask-stormpath-tests-%s' % uuid4().hex
+ self.application = bootstrap_app(self.client, self.name)
self.app = bootstrap_flask_app(self.application)
+ # Create social directories if necessary.
+ if self.app.config['STORMPATH_ENABLE_FACEBOOK']:
+ facebook_provider = {
+ 'client_id': os.environ.get('FACEBOOK_API_ID'),
+ 'client_secret': os.environ.get('FACEBOOK_API_SECRET'),
+ 'provider_id': Provider.FACEBOOK,
+ }
+ self.create_social_directory(
+ social_name='facebook', provider=facebook_provider)
+
+ if self.app.config['STORMPATH_ENABLE_GOOGLE']:
+ google_provider = {
+ 'client_id': os.environ.get('GOOGLE_CLIENT_ID'),
+ 'client_secret': os.environ.get('GOOGLE_CLIENT_SECRET'),
+ 'redirect_uri': (
+ ''.join(
+ (os.environ.get('ROOT_URL'), ':',
+ os.environ.get('PORT'))) + '/google'),
+ 'provider_id': Provider.GOOGLE,
+ }
+
+ self.create_social_directory(
+ social_name='google', provider=google_provider)
+
+ self.manager = StormpathManager(self.app)
+
+ # html and json header settings
+ self.html_header = 'text/html'
+ self.json_header = 'application/json'
+
+ # Remember default wsgi_app instance for dynamically changing request
+ # type later in tests.
+ self.default_wsgi_app = self.app.wsgi_app
+
+ # Make sure our requests don't trigger a json response.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.html_header)
+
+ # Create a user.
+ with self.app.app_context():
+ self.user = User.create(
+ username='rdegges',
+ given_name='Randall',
+ surname='Degges',
+ email='r@rdegges.com',
+ password='woot1LoveCookies!',
+ )
+
def tearDown(self):
- """Destroy all provisioned Stormpath resources."""
- # Clean up the application.
- app_name = self.application.name
- self.application.delete()
+ """ Destroy all provisioned Stormpath resources. """
+ destroy_resources(self.application, self.client)
- # Clean up the directories.
- for directory in self.client.directories.search(app_name):
- directory.delete()
+ def assertDictList(self, list1, list2, key_name):
+ # Sorts list of dictionaries by key name and compares them.
- # Clean up API keys
- self.delete_api_key(app_name)
+ sorted_list1 = sorted(list1, key=lambda k: k[key_name])
+ sorted_list2 = sorted(list2, key=lambda k: k[key_name])
+ self.assertEqual(sorted_list1, sorted_list2)
- def delete_api_key(self, app_name):
- try:
- for account in self.client.accounts.items:
- for key in account.api_keys.items:
- if key.name and app_name in key.name:
- self.client.data_store.delete_resource(key.href)
- except Error:
- # Resource not found - ignore.
- pass
+ def reinit_app(self):
+ # Reinitializes our testing application.
+
+ self.app = bootstrap_flask_app(self.application)
+ self.manager = StormpathManager(self.app)
+
+ # Make sure our requests don't trigger a json response.
+ self.default_wsgi_app = self.app.wsgi_app
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.html_header)
+
+ def create_social_directory(self, social_name=None, provider=None):
+ # Creates a stormpath social directory.
+ social_dir = (
+ self.application._client.directories.create({
+ 'name': self.application.name + '-' + social_name,
+ 'provider': provider
+ })
+ )
+
+ # Now we'll map the new directory to our application.
+ self.application.account_store_mappings.create(
+ {
+ 'application': self.application,
+ 'account_store': social_dir,
+ 'list_index': 99,
+ 'is_default_account_store': False,
+ 'is_default_group_store': False,
+ }
+ )
class SignalReceiver(object):
@@ -66,6 +138,92 @@ def signal_user_receiver_function(self, sender, user):
self.received_signals.append((sender, user))
+class HttpAcceptWrapper(object):
+ """
+ Helper class for injecting HTTP headers.
+ """
+ def __init__(self, app, accept_header):
+ self.app = app
+ self.accept_header = accept_header
+
+ def __call__(self, environ, start_response):
+ environ['HTTP_ACCEPT'] = (self.accept_header)
+ return self.app(environ, start_response)
+
+
+class CredentialsValidator(object):
+ """
+ Helper class for validating all the environment variables.
+ """
+
+ def validate_stormpath_credentials(self, client):
+ """
+ Ensure that we have proper credentials needed to properly test our
+ Flask-Stormpath integration.
+ """
+ try:
+ # Trying to access a resource that requires an api call
+ # (like a tenant key) without the proper id and secret should
+ # raise an error.
+ client.tenant.key
+ except StormpathError:
+ raise ValueError(
+ 'Stormpath api id and secret invalid or missing. Set your ' +
+ 'credentials as environment variables before testing.')
+
+ def validate_facebook_credentials(self, app):
+ # Ensure that Facebook api id and secret are valid:
+ graph_api = GraphAPI()
+ try:
+ graph_api.get_app_access_token(
+ os.environ.get('FACEBOOK_API_ID'),
+ os.environ.get('FACEBOOK_API_SECRET'))
+ except GraphAPIError:
+ raise ValueError(
+ 'Facebook app id and secret invalid or missing. Set your ' +
+ 'credentials as environment variables before testing.')
+
+ def validate_google_credentials(self, app):
+ root_url = os.environ.get('ROOT_URL')
+ port = os.environ.get('PORT')
+
+ # Ensure that our url parameters are present
+ if not root_url or not port:
+ raise ValueError(
+ 'Root url and port invalid or missing. Set your ' +
+ 'values as environment variables before testing.')
+ redirect_uri = ''.join((root_url, ':', port, '/google'))
+
+ # Ensure that Google api id and secret are valid:
+ flow = OAuth2WebServerFlow(
+ client_id=os.environ.get('GOOGLE_CLIENT_ID'),
+ client_secret=os.environ.get('GOOGLE_CLIENT_SECRET'),
+ scope=(
+ 'https://www.googleapis.com/auth/userinfo.email',
+ 'https://www.googleapis.com/auth/userinfo.profile'),
+ redirect_uri=redirect_uri)
+ url = flow.step1_get_authorize_url()
+
+ resp = requests.get(url)
+ if resp.status_code != 200:
+ raise ValueError(
+ 'Google client id and secret invalid or missing. Set your ' +
+ 'credentials as environment variables before testing.')
+
+ def validate_credentials(self, app, flask_app, client):
+ """
+ Ensure that we have proper credentials needed to properly test our
+ social login stuff.
+ """
+ try:
+ self.validate_stormpath_credentials(client)
+ self.validate_facebook_credentials(flask_app)
+ self.validate_google_credentials(flask_app)
+ except ValueError as error:
+ destroy_resources(app, client)
+ raise error
+
+
def bootstrap_client():
"""
Create a new Stormpath Client from environment variables.
@@ -74,12 +232,12 @@ def bootstrap_client():
:returns: A new Stormpath Client, fully initialized.
"""
return Client(
- id = environ.get('STORMPATH_API_KEY_ID'),
- secret = environ.get('STORMPATH_API_KEY_SECRET'),
+ id=os.environ.get('STORMPATH_API_KEY_ID'),
+ secret=os.environ.get('STORMPATH_API_KEY_SECRET'),
)
-def bootstrap_app(client):
+def bootstrap_app(client, name):
"""
Create a new, uniquely named, Stormpath Application.
@@ -95,8 +253,10 @@ def bootstrap_app(client):
:returns: A new Stormpath Application, fully initialized.
"""
return client.applications.create({
- 'name': 'flask-stormpath-tests-%s' % uuid4().hex,
- 'description': 'This application is ONLY used for testing the Flask-Stormpath library. Please do not use this for anything serious.',
+ 'name': name,
+ 'description': 'This application is ONLY used for testing the ' +
+ 'Flask-Stormpath library. Please do not use this for anything ' +
+ 'serious.',
}, create_directory=True)
@@ -111,10 +271,65 @@ def bootstrap_flask_app(app):
a = Flask(__name__)
a.config['DEBUG'] = True
a.config['SECRET_KEY'] = uuid4().hex
- a.config['STORMPATH_API_KEY_ID'] = environ.get('STORMPATH_API_KEY_ID')
- a.config['STORMPATH_API_KEY_SECRET'] = environ.get('STORMPATH_API_KEY_SECRET')
+ a.config['STORMPATH_API_KEY_ID'] = os.environ.get('STORMPATH_API_KEY_ID')
+ a.config['STORMPATH_API_KEY_SECRET'] = os.environ.get(
+ 'STORMPATH_API_KEY_SECRET')
a.config['STORMPATH_APPLICATION'] = app.name
a.config['WTF_CSRF_ENABLED'] = False
- StormpathManager(a)
+ a.config['STORMPATH_ENABLE_FACEBOOK'] = True
+ a.config['STORMPATH_ENABLE_GOOGLE'] = True
+
+ # Set file path for yaml config file. We do this since we need to test out
+ # our application with different yaml configurations.
+ a.config['STORMPATH_CONFIG_PATH'] = create_config_path(
+ **json.loads(os.environ.get('TEST_CONFIG', '{}')))
+
+ # Add secrets and ids for social login stuff.
+ a.config['STORMPATH_SOCIAL'] = {
+ 'FACEBOOK': {
+ 'app_id': os.environ.get('FACEBOOK_API_ID'),
+ 'app_secret': os.environ.get('FACEBOOK_API_SECRET')},
+ 'GOOGLE': {
+ 'client_id': os.environ.get('GOOGLE_CLIENT_ID'),
+ 'client_secret': os.environ.get('GOOGLE_CLIENT_SECRET')}
+ }
return a
+
+
+def create_config_path(filename='', default=True):
+ # Creates a path for yaml config files in our tests directory.
+ if default:
+ return os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ 'flask_stormpath', 'config', 'default-config' + '.yml')
+ else:
+ return os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ 'config', filename + '.yml')
+
+
+def destroy_resources(app, client):
+ """Destroy all provisioned Stormpath resources."""
+ # Clean up the application.
+ app_name = app.name
+ app.delete()
+
+ # Clean up the directories.
+ for directory in client.directories.search(app_name):
+ directory.delete()
+
+
+""" Stormpath and social login credentials validation. """
+
+# Create resources needed for validation.
+client = bootstrap_client()
+app = bootstrap_app(client, 'flask-stormpath-test-social-%s' % uuid4().hex)
+flask_app = bootstrap_flask_app(app)
+
+# Validate credentials.
+cred_validator = CredentialsValidator()
+cred_validator.validate_credentials(app, flask_app, client)
+
+# Destroy resources.
+destroy_resources(app, client)
diff --git a/tests/test_context_processors.py b/tests/test_context_processors.py
index c5aefe1..18117fc 100644
--- a/tests/test_context_processors.py
+++ b/tests/test_context_processors.py
@@ -3,35 +3,10 @@
from flask_stormpath import User, user
from flask_stormpath.context_processors import user_context_processor
-from stormpath.error import Error
-
from .helpers import StormpathTestCase
class TestUserContextProcessor(StormpathTestCase):
-
- def setUp(self):
- """Provision a single user account for testing."""
- # Call the parent setUp method first -- this will bootstrap our tests.
- super(TestUserContextProcessor, self).setUp()
-
- # Create our Stormpath user.
- with self.app.app_context():
- self.user = User.create(
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
-
- def tearDown(self):
- super(TestUserContextProcessor, self).tearDown()
- try:
- self.user.delete()
- except Error:
- # Resource not found - ignore.
- pass
-
def test_raw_works(self):
with self.app.test_client() as c:
c.post('/login', data={
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 33cb325..8152167 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -1,10 +1,7 @@
"""Run tests against our custom decorators."""
-from flask_stormpath import User
from flask_stormpath.decorators import groups_required
-from stormpath.error import Error
-
from .helpers import StormpathTestCase
@@ -16,15 +13,6 @@ def setUp(self):
super(TestGroupsRequired, self).setUp()
with self.app.app_context():
-
- # Create our Stormpath user.
- self.user = User.create(
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
-
# Create two groups.
self.admins = self.application.groups.create({
'name': 'admins',
@@ -33,24 +21,6 @@ def setUp(self):
'name': 'developers',
})
- def tearDown(self):
- super(TestGroupsRequired, self).tearDown()
- try:
- self.user.delete()
- except Error:
- # Resource not found - ignore.
- pass
- try:
- self.admins.delete()
- except Error:
- # Resource not found - ignore.
- pass
- try:
- self.developers.delete()
- except Error:
- # Resource not found - ignore.
- pass
-
def test_defaults_to_all(self):
@self.app.route('/test')
@groups_required(['admins', 'developers'])
diff --git a/tests/test_forms.py b/tests/test_forms.py
new file mode 100644
index 0000000..7a5ef6b
--- /dev/null
+++ b/tests/test_forms.py
@@ -0,0 +1,261 @@
+"""Tests for our custom forms."""
+
+
+from .helpers import StormpathTestCase
+from flask_stormpath.forms import StormpathForm
+from wtforms.fields import PasswordField, StringField
+from wtforms.validators import InputRequired, Email, EqualTo
+from stormpath.resources import Resource
+import json
+
+
+class TestStormpathForm(StormpathTestCase):
+ """Test the StormpathForm."""
+
+ def assertFormFields(self, form, config):
+ # Iterate through form config and check if the settings are
+ # properly applied to our form class.
+
+ for field in config['fieldOrder']:
+ # Convert the key to underscore format
+ form_field = Resource.from_camel_case(field)
+
+ if config['fields'][field]['enabled']:
+ # Check if all enabled fields are set.
+ self.assertTrue(hasattr(form, form_field))
+
+ # Get validators.
+ validators = getattr(form, form_field).kwargs.get(
+ 'validators')
+
+ # Check if field type is set.
+ if config['fields'][field]['type'] == 'text':
+ self.assertTrue(getattr(
+ form, form_field).field_class, StringField)
+ elif config['fields'][field]['type'] == 'password':
+ self.assertTrue(getattr(
+ form, form_field).field_class, PasswordField)
+ elif config['fields'][field]['type'] == 'email':
+ self.assertTrue(any(isinstance(
+ validator, Email) for validator in validators))
+
+ # Check if required validator is set.
+ if config['fields'][field]['required']:
+ self.assertTrue(any(isinstance(
+ validator, InputRequired) for validator in validators))
+
+ # If 'confirmPassword' field is enabled, check that the proper
+ # validator is applied.
+ if (field == 'confirmPassword' and config['fields'][field][
+ 'enabled']):
+ self.assertTrue(any(isinstance(
+ validator, EqualTo) for validator in validators))
+
+ # Check if placeholders are set.
+ placeholder = config['fields'][field].get('placeholder')
+ if placeholder:
+ self.assertTrue(getattr(
+ form, form_field).kwargs['render_kw']['placeholder'],
+ config['fields'][field]['placeholder'])
+
+ # Check if labels are set.
+ label = config['fields'][field].get('label')
+ if label:
+ self.assertTrue(getattr(form, form_field).args[0], config[
+ 'fields'][field]['label'])
+
+ def assertFormBuilding(self, form_config):
+ # Ensure that forms are built based on the config specs.
+ with self.app.app_context():
+ form = StormpathForm.specialize_form(form_config)
+ self.assertFormFields(form, form_config)
+
+ # Check to see if the StormpathFrom base class is left unaltered
+ # after form building.
+ new_form = StormpathForm()
+ field_diff = list(set(form_config['fieldOrder']) - set(
+ dir(new_form)))
+ self.assertEqual(
+ sorted(field_diff), sorted(form_config['fieldOrder']))
+
+ def test_login_form_building(self):
+ form_config = self.app.config['stormpath']['web']['login']['form']
+ self.assertFormBuilding(form_config)
+
+ def test_registration_form_building(self):
+ form_config = self.app.config['stormpath']['web']['register']['form']
+ form_config['fields']['confirmPassword']['enabled'] = True
+ self.assertFormBuilding(form_config)
+
+ def test_forgot_password_form_building(self):
+ form_config = self.app.config['stormpath']['web']['forgotPassword'][
+ 'form']
+ self.assertFormBuilding(form_config)
+
+ def test_change_password_form_building(self):
+ form_config = self.app.config['stormpath']['web']['changePassword'][
+ 'form']
+ self.assertFormBuilding(form_config)
+
+ def test_empty_form(self):
+ # Ensure that an empty config will return an empty form.
+ with self.app.app_context():
+ form = StormpathForm.specialize_form({})
+ self.assertEqual(form._json, [])
+
+ def test_error_messages(self):
+ # We'll use register form fields for this test, since they cover
+ # every error message case.
+ form_config = self.app.config['stormpath']['web']['register']['form']
+
+ # We are creating requests, since wtforms pass request.form to form
+ # init automatically.
+ with self.app.test_client() as c:
+ # Ensure that an error is raised if a required field is left
+ # empty.
+ c.post('', data={
+ 'username': 'rdegges',
+ 'surname': 'Degges',
+ 'email': 'r@rdegges.com',
+ 'password': 'woot1LoveCookies!',
+ })
+ form = StormpathForm.specialize_form(form_config)()
+ self.assertFalse(form.validate_on_submit())
+ self.assertTrue(form.errors, {
+ 'given_name': ['First Name is required.']})
+
+ # Ensure that an error is raised if the email format is invalid.
+ c.post('', data={
+ 'username': 'rdegges',
+ 'given_name': 'Randall',
+ 'surname': 'Degges',
+ 'email': 'rrdegges.com',
+ 'password': 'woot1LoveCookies!',
+ })
+ form = StormpathForm.specialize_form(form_config)()
+ self.assertFalse(form.validate_on_submit())
+ self.assertTrue(form.errors, {
+ 'email': ['Email must be in valid format.']})
+
+ # Ensure that an error is raised if confirm password is enabled
+ # the two passwords mismatch.
+ form_config['fields']['confirmPassword']['enabled'] = True
+
+ c.post('', data={
+ 'username': 'rdegges',
+ 'given_name': 'Randall',
+ 'surname': 'Degges',
+ 'email': 'r@rdegges.com',
+ 'password': 'woot1LoveCookies!',
+ 'confirm_password': 'woot1LoveCookies!...NOT!!'
+ })
+ form = StormpathForm.specialize_form(form_config)()
+ self.assertFalse(form.validate_on_submit())
+ self.assertTrue(form.errors, {
+ 'confirm_password': ['Passwords do not match.']})
+
+ # Ensure that proper form will result in success.
+ c.post('', data={
+ 'username': 'rdegges',
+ 'given_name': 'Randall',
+ 'surname': 'Degges',
+ 'email': 'r@rdegges.com',
+ 'password': 'woot1LoveCookies!',
+ 'confirm_password': 'woot1LoveCookies!'
+ })
+ form = StormpathForm.specialize_form(form_config)()
+ self.assertTrue(form.validate_on_submit())
+
+ # Ensure that enabled but optional fields won't cause an error.
+ form_config['fields']['givenName']['required'] = False
+
+ c.post('', data={
+ 'username': 'rdegges2',
+ 'surname': 'Degges2',
+ 'email': 'r@rdegges2.com',
+ 'password': 'woot1LoveCookies!2',
+ 'confirm_password': 'woot1LoveCookies!2'
+ })
+ form = StormpathForm.specialize_form(form_config)()
+ self.assertTrue(form.validate_on_submit())
+
+ def test_json_fields(self):
+ # Specify expected fields
+ expected_fields = [
+ {
+ 'name': 'login',
+ 'type': 'text',
+ 'required': True,
+ 'visible': True,
+ 'label': 'Username or Email',
+ 'placeholder': 'Username or Email'},
+ {
+ 'name': 'password',
+ 'type': 'password',
+ 'required': True,
+ 'visible': True,
+ 'label': 'Password',
+ 'placeholder': 'Password'}
+ ]
+
+ with self.app.app_context():
+ form_config = self.app.config['stormpath']['web']['login']['form']
+ form = StormpathForm.specialize_form(form_config)()
+
+ # Construct field settings from the config.
+ field_specs = []
+ for key in form_config['fields'].keys():
+ field = form_config['fields'][key].copy()
+ field.pop('enabled')
+ field['name'] = key
+ field_specs.append(field)
+
+ # Ensure that _json fields are the same as expected fields.
+ self.assertDictList(form._json, expected_fields, 'name')
+
+ # Ensure that _json fields are the same as config settings.
+ self.assertDictList(form._json, expected_fields, 'name')
+
+ def test_json_property(self):
+ # Specify expected fields
+ expected_fields = [
+ {
+ 'name': 'login',
+ 'type': 'text',
+ 'required': True,
+ 'visible': True,
+ 'label': 'Username or Email',
+ 'placeholder': 'Username or Email'},
+ {
+ 'name': 'password',
+ 'type': 'password',
+ 'required': True,
+ 'visible': True,
+ 'label': 'Password',
+ 'placeholder': 'Password'}
+ ]
+
+ # Ensure that json property returns a proper json value.
+ with self.app.app_context():
+ form_config = self.app.config['stormpath']['web']['login']['form']
+ form = StormpathForm.specialize_form(form_config)()
+
+ # Construct field settings from the config.
+ field_specs = []
+ for key in form_config['fields'].keys():
+ field = form_config['fields'][key].copy()
+ field.pop('enabled')
+ field['name'] = Resource.from_camel_case(key)
+ field['visible'] = True
+ field_specs.append(field)
+
+ # Ensure that json return value is the same as config settings.
+ self.assertDictList(json.loads(form.json), field_specs, 'name')
+
+ # We cannot compare expected_fields directly, so we'll first
+ # compare that both values are strings.
+ self.assertEqual(
+ type(form.json), type(json.dumps(expected_fields)))
+
+ # Then compare that they both contain the same values.
+ self.assertDictList(json.loads(form.json), expected_fields, 'name')
diff --git a/tests/test_models.py b/tests/test_models.py
index 2bc13ea..9e79cb5 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,153 +1,110 @@
"""Tests for our data models."""
+import sys
from flask_stormpath.models import User
+from flask_stormpath import StormpathError
from stormpath.resources.account import Account
-
+from stormpath.resources.provider import Provider
from .helpers import StormpathTestCase
+from os import environ
+import json
+
+if sys.version_info.major == 3:
+ from unittest.mock import patch
+else:
+ from mock import patch
class TestUser(StormpathTestCase):
"""Our User test suite."""
def test_subclass(self):
- with self.app.app_context():
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
-
- # Ensure that our lazy construction of the subclass works as
- # expected for users (a `User` should be a valid Stormpath
- # `Account`.
- self.assertTrue(user.writable_attrs)
- self.assertIsInstance(user, Account)
- self.assertIsInstance(user, User)
+ # Ensure that our lazy construction of the subclass works as
+ # expected for users (a `User` should be a valid Stormpath
+ # `Account`.
+ self.assertTrue(self.user.writable_attrs)
+ self.assertIsInstance(self.user, Account)
+ self.assertIsInstance(self.user, User)
def test_repr(self):
- with self.app.app_context():
+ # Ensure `username` is shown in the output if specified.
+ self.assertTrue(self.user.username in self.user.__repr__())
- # Ensure `email` is shown in the output if no `username` is
- # specified.
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
- self.assertTrue(user.email in user.__repr__())
+ # Ensure Stormpath `href` is shown in the output.
+ self.assertTrue(self.user.href in self.user.__repr__())
- # Delete this user.
- user.delete()
+ # Delete this user.
+ self.user.delete()
- # Ensure `username` is shown in the output if specified.
- user = User.create(
- username = 'omgrandall',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
+ with self.app.app_context():
+ self.user = User.create(
+ given_name='Randall',
+ surname='Degges',
+ email='r@rdegges.com',
+ password='woot1LoveCookies!',
)
- self.assertTrue(user.username in user.__repr__())
- # Ensure Stormpath `href` is shown in the output.
- self.assertTrue(user.href in user.__repr__())
+ # Ensure `email` is shown in the output if no `username` is
+ # specified.
+ self.assertTrue(self.user.email in self.user.__repr__())
def test_get_id(self):
- with self.app.app_context():
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
- self.assertEqual(user.get_id(), user.href)
+ self.assertEqual(self.user.get_id(), self.user.href)
def test_is_active(self):
- with self.app.app_context():
+ # Ensure users are active by default.
+ self.assertEqual(self.user.is_active, True)
- # Ensure users are active by default.
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
- self.assertEqual(user.is_active, True)
-
- # Ensure users who have their accounts explicitly disabled actually
- # return a proper status when `is_active` is called.
- user.status = User.STATUS_DISABLED
- self.assertEqual(user.is_active, False)
+ # Ensure users who have their accounts explicitly disabled actually
+ # return a proper status when `is_active` is called.
+ self.user.status = User.STATUS_DISABLED
+ self.assertEqual(self.user.is_active, False)
- # Ensure users who have not verified their accounts return a proper
- # status when `is_active` is called.
- user.status = User.STATUS_UNVERIFIED
- self.assertEqual(user.is_active, False)
+ # Ensure users who have not verified their accounts return a proper
+ # status when `is_active` is called.
+ self.user.status = User.STATUS_UNVERIFIED
+ self.assertEqual(self.user.is_active, False)
def test_is_anonymous(self):
- with self.app.app_context():
-
- # There is no way we can be anonymous, as Stormpath doesn't support
- # anonymous users (that is a job better suited for a cache or
- # something).
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
- self.assertEqual(user.is_anonymous, False)
+ # There is no way we can be anonymous, as Stormpath doesn't support
+ # anonymous users (that is a job better suited for a cache or
+ # something).
+ self.assertEqual(self.user.is_anonymous, False)
def test_is_authenticated(self):
- with self.app.app_context():
-
- # This should always return true. If a user account can be
- # fetched, that means it must be authenticated.
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
- self.assertEqual(user.is_authenticated, True)
+ # This should always return true. If a user account can be
+ # fetched, that means it must be authenticated.
+ self.assertEqual(self.user.is_authenticated, True)
def test_create(self):
- with self.app.app_context():
- # Ensure all requied fields are properly set.
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
- self.assertEqual(user.email, 'r@testmail.stormpath.com')
- self.assertEqual(user.given_name, 'Randall')
- self.assertEqual(user.surname, 'Degges')
- self.assertEqual(user.username, 'r@testmail.stormpath.com')
- self.assertEqual(user.middle_name, None)
- self.assertEqual(
- dict(user.custom_data),
- {
- 'created_at': user.custom_data.created_at,
- 'modified_at': user.custom_data.modified_at,
- })
+ # Ensure all requied fields are properly set.
+ self.assertEqual(self.user.email, 'r@rdegges.com')
+ self.assertEqual(self.user.given_name, 'Randall')
+ self.assertEqual(self.user.surname, 'Degges')
+ self.assertEqual(self.user.username, 'rdegges')
+ self.assertEqual(self.user.middle_name, None)
+ self.assertEqual(
+ dict(self.user.custom_data),
+ {
+ 'created_at': self.user.custom_data.created_at,
+ 'modified_at': self.user.custom_data.modified_at,
+ })
- # Delete this user.
- user.delete()
+ # Delete this user.
+ self.user.delete()
- # Ensure all optional parameters are properly set.
+ # Ensure all optional parameters are properly set.
+ with self.app.app_context():
user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- username = 'rdegges',
- middle_name = 'Clark',
- custom_data = {
+ email='r@rdegges.com',
+ password='woot1LoveCookies!',
+ given_name='Randall',
+ surname='Degges',
+ username='rdegges',
+ middle_name='Clark',
+ custom_data={
'favorite_shows': ['Code Monkeys', 'The IT Crowd'],
'friends': ['Sami', 'Alven'],
'favorite_place': {
@@ -173,31 +130,243 @@ def test_create(self):
'modified_at': user.custom_data.modified_at,
})
+ def test_save(self):
+ # Ensure that save will save the new instance.
+ self.assertEqual(self.user.username, 'rdegges')
+ self.user.username = 'something else'
+ self.user.save()
+ self.assertEqual(self.user.username, 'something else')
+
+ # Ensure that save will return a user instance. (Signal sent during
+ # save is tested in test_signals.py)
+ self.assertTrue(isinstance(self.user.save(), User))
+
def test_from_login(self):
with self.app.app_context():
-
- # First we'll create a user.
+ # Create a user (we need a new user instance, one with a specific
+ # username).
user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- username = 'rdegges',
- )
+ username='rdegges2',
+ email='r2@rdegges.com',
+ password='woot1LoveCookies2!',
+ given_name='Randall2',
+ surname='Degges2')
+
+ # Get user href
original_href = user.href
# Now we'll try to retrieve that user by specifing the user's
# `email` and `password`.
user = User.from_login(
- 'r@testmail.stormpath.com',
- 'woot1LoveCookies!',
+ 'r2@rdegges.com',
+ 'woot1LoveCookies2!',
)
self.assertEqual(user.href, original_href)
-
# Now we'll try to retrieve that user by specifying the user's
# `username` and `password`.
user = User.from_login(
- 'rdegges',
- 'woot1LoveCookies!',
+ 'rdegges2',
+ 'woot1LoveCookies2!',
)
self.assertEqual(user.href, original_href)
+
+ def test_to_json(self):
+ # Ensure that to_json method returns user json representation.
+ self.assertTrue(isinstance(self.user.to_json(), str))
+ json_data = json.loads(self.user.to_json())
+ expected_json_data = {'account': {
+ 'href': self.user.href,
+ 'modified_at': self.user.modified_at.isoformat(),
+ 'created_at': self.user.created_at.isoformat(),
+ 'status': 'ENABLED',
+ 'username': 'rdegges',
+ 'email': 'r@rdegges.com',
+ 'given_name': 'Randall',
+ 'middle_name': None,
+ 'surname': 'Degges',
+ 'full_name': 'Randall Degges'
+ }}
+ self.assertEqual(json_data, expected_json_data)
+
+ def test_to_json_datetime_handler(self):
+ # Ensure that our custom datetime_handler serializes only datetime
+ # objects.
+ self.user.surname = set([1, 2])
+ with self.assertRaises(TypeError):
+ self.user.to_json()
+
+ self.user.surname = 'foobar'
+ self.user.to_json()
+
+
+class SocialMethodsTestMixin(object):
+ """Our mixin for testing User social methods."""
+
+ def __init__(self, social_name, *args, **kwargs):
+ # Validate social_name
+ if social_name not in ['facebook', 'google']:
+ raise ValueError('Wrong social name.')
+ self.social_name = social_name
+
+ # Set our error message
+ self.error_message = (
+ 'Stormpath was not able to complete the request to %s:'
+ % self.social_name)
+
+ @property
+ def social_dir_name(self):
+ # Get directory name
+ with self.app.app_context():
+ return (
+ self.app.stormpath_manager.application.name + '-' +
+ self.social_name)
+
+ @property
+ def search_query(self):
+ return self.app.stormpath_manager.client.tenant.directories.query(
+ name=self.social_dir_name)
+
+ def user_from_social(self, access_token):
+ return getattr(
+ User, 'from_%s' % self.social_name)(access_token)
+
+ @patch('stormpath.resources.application.Application.get_provider_account')
+ def test_from_social_supported_service(self, user_mock):
+ # Ensure that the proper social_name will continue processing the
+ # social login.
+ with self.app.app_context():
+ self.assertTrue(
+ isinstance(self.user.from_social(
+ self.social_name,
+ 'mocked access token', self.provider), User))
+
+ # Ensure that the wrong social name will raise an error.
+ with self.assertRaises(ValueError) as error:
+ self.user.from_social(
+ 'foobar', 'mocked access token', self.provider)
+
+ self.assertEqual(
+ str(error.exception), 'Social service is not supported.')
+
+ @patch('stormpath.resources.application.Application.get_provider_account')
+ def test_from_social_valid(self, user_mock):
+ # We'll mock the social account getter since we cannot replicate the
+ # access token needed for social login.
+ user_mock.return_value = self.user
+
+ # Ensure that from_ will return a User instance if access token
+ # is valid.
+ with self.app.app_context() and self.app.test_request_context(
+ ':%s' % environ.get('PORT')):
+ user = self.user_from_social('mocked access token')
+ self.assertTrue(isinstance(user, User))
+
+ @patch('stormpath.resources.application.Application.get_provider_account')
+ def test_from_social_create_directory(self, user_mock):
+ # We'll mock the social account getter since we cannot replicate the
+ # access token needed for social login.
+ user_mock.return_value = self.user
+ user_mock.side_effect = StormpathError(
+ {'developerMessage': 'Mocked message.'})
+
+ # Ensure that from_ will create a directory if the
+ # access token is valid but a directory doesn't exist.
+ with self.app.app_context():
+ # Ensure that a social directory is not present.
+ if self.search_query.items:
+ self.search_query.items[0].delete()
+
+ # We have to catch our exception since we're the one raising it
+ # with our mocking.
+ with self.assertRaises(StormpathError):
+ # Create a directory by creating the user for the first time.
+ with self.app.test_request_context(
+ ':%s' % environ.get('PORT')):
+ user = self.user_from_social('mocked access token')
+ self.assertTrue(isinstance(user, User))
+
+ # To ensure that this error is caught at the right time
+ # however, we will assert the number of mock calls.
+ self.assertEqual(user_mock.call_count, 2)
+
+ # Ensure that the social directory is now present.
+ self.assertEqual(len(self.search_query.items), 1)
+ self.assertEqual(
+ self.search_query.items[0].name, self.social_dir_name)
+
+ def test_from_social_invalid_access_token(self):
+ # Ensure that from_ will raise a StormpathError if access
+ # token is invalid.
+ with self.app.app_context() and self.app.test_request_context(
+ ':%s' % environ.get('PORT')):
+ with self.assertRaises(StormpathError) as error:
+ self.user_from_social('foobar')
+
+ self.assertTrue(self.error_message in str(error.exception))
+
+ def test_from_social_invalid_access_token_with_existing_directory(self):
+ # First we will create a social directory if one doesn't already
+ # exist.
+ with self.app.app_context() and self.app.test_request_context(
+ ':%s' % environ.get('PORT')):
+ if not self.search_query.items:
+ self.create_social_directory(
+ social_name=self.social_name, provider=self.provider)
+
+ # Ensure that from_ will raise a StormpathError if access
+ # token is invalid and social directory present.
+ with self.assertRaises(StormpathError) as error:
+ self.user_from_social('foobar')
+
+ self.assertTrue(self.error_message in str(error.exception))
+
+
+class TestFacebookLogin(StormpathTestCase, SocialMethodsTestMixin):
+ """Our User facebook login test suite."""
+ def __init__(self, *args, **kwargs):
+ super(TestFacebookLogin, self).__init__(*args, **kwargs)
+ SocialMethodsTestMixin.__init__(self, 'facebook')
+
+ def setUp(self):
+ super(TestFacebookLogin, self).setUp()
+
+ # Set a provider.
+ self.provider = {
+ 'client_id': environ.get('FACEBOOK_API_ID'),
+ 'client_secret': environ.get('FACEBOOK_API_SECRET'),
+ 'provider_id': Provider.FACEBOOK,
+ }
+
+ # Set client ID and secret.
+ self.app.config['stormpath']['web']['social']['facebook'][
+ 'clientId'] = environ.get('FACEBOOK_API_ID')
+ self.app.config['stormpath']['web']['social']['facebook'][
+ 'clientSecret'] = environ.get('FACEBOOK_API_SECRET')
+
+
+class TestGoogleLogin(StormpathTestCase, SocialMethodsTestMixin):
+ """Our User google login test suite."""
+ def __init__(self, *args, **kwargs):
+ super(TestGoogleLogin, self).__init__(*args, **kwargs)
+ SocialMethodsTestMixin.__init__(self, 'google')
+
+ def setUp(self):
+ super(TestGoogleLogin, self).setUp()
+
+ with self.app.app_context():
+ # Set a provider
+ self.provider = {
+ 'client_id': environ.get('GOOGLE_CLIENT_ID'),
+ 'client_secret': environ.get('GOOGLE_CLIENT_SECRET'),
+ 'redirect_uri': (
+ ''.join(
+ (environ.get('ROOT_URL'), ':', environ.get('PORT'))) +
+ self.app.config['STORMPATH_GOOGLE_LOGIN_URL']),
+ 'provider_id': Provider.GOOGLE,
+ }
+
+ # Set client ID and secret.
+ self.app.config['stormpath']['web']['social']['google'][
+ 'clientId'] = environ.get('GOOGLE_CLIENT_ID')
+ self.app.config['stormpath']['web']['social']['google'][
+ 'clientSecret'] = environ.get('GOOGLE_CLIENT_SECRET')
diff --git a/tests/test_request_processors.py b/tests/test_request_processors.py
new file mode 100644
index 0000000..90a0a36
--- /dev/null
+++ b/tests/test_request_processors.py
@@ -0,0 +1,75 @@
+"""Run tests against our custom request processors."""
+
+from flask import current_app
+from .helpers import StormpathTestCase, HttpAcceptWrapper
+from stormpath_config.errors import ConfigurationError
+from flask_stormpath.request_processors import (
+ get_accept_header,
+ request_wants_json
+)
+
+
+class TestRequestProcessor(StormpathTestCase):
+ """Test request_processor functions."""
+
+ def test_get_accept_header(self):
+ # Ensure that get_accept_header will return a proper accept header.
+
+ with self.app.app_context():
+ allowed_types = current_app.config['stormpath']['web']['produces']
+
+ with self.app.test_client() as c:
+ # HTML header.
+ c.get('/')
+ self.assertEqual(get_accept_header(), 'text/html')
+
+ # JSON header.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.json_header)
+ c.get('/')
+ self.assertEqual(get_accept_header(), 'application/json')
+
+ # Accept header empty.
+ self.app.wsgi_app = HttpAcceptWrapper(self.default_wsgi_app, None)
+ c.get('/')
+ self.assertEqual(get_accept_header(), allowed_types[0])
+
+ # Accept header */*.
+ self.app.wsgi_app = HttpAcceptWrapper(self.default_wsgi_app, '*/*')
+ c.get('/')
+ self.assertEqual(get_accept_header(), allowed_types[0])
+
+ # Ensure that get_accept_header will throw an error if called from
+ # outside application context or without stormpath config.
+ with self.assertRaises(ConfigurationError) as error:
+ get_accept_header()
+ self.assertEqual(
+ error.exception.message,
+ 'You must initialize flask app before calling this function.')
+
+ def test_request_wants_json(self):
+ # Ensure that a request with a json accept header will return a
+ # json response.
+
+ with self.app.test_client() as c:
+ # JSON header.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.json_header)
+ c.get('/')
+ self.assertTrue(request_wants_json())
+
+ # HTML header
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.html_header)
+ c.get('/')
+ self.assertFalse(request_wants_json())
+
+ # If the accept header is empty, get_accept_header will return
+ # the first type in produces list, which is currently set to json.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, None)
+ c.get('/')
+ self.assertEqual(
+ current_app.config['stormpath']['web']['produces'][0],
+ 'application/json')
+ self.assertTrue(request_wants_json())
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 2d9b419..cdb6857 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -1,132 +1,207 @@
"""Tests for our settings stuff."""
-from datetime import timedelta
-from os import close, environ, remove, write
-from tempfile import mkstemp
-
-from flask_stormpath.errors import ConfigurationError
-from flask_stormpath.settings import check_settings, init_settings
-
+from flask_stormpath.settings import (
+ StormpathSettings)
+from flask_stormpath import __version__ as stormpath_flask_version
+from flask import __version__ as flask_version
from .helpers import StormpathTestCase
+try:
+ from mock import MagicMock, patch
+except ImportError:
+ from unittest.mock import MagicMock, patch
+
class TestInitSettings(StormpathTestCase):
"""Ensure we can properly initialize Flask app settings."""
def test_works(self):
- init_settings(self.app.config)
+ self.manager.init_settings(self.app.config)
# Ensure a couple of settings exist that we didn't explicitly specify
# anywhere.
- self.assertEqual(self.app.config['STORMPATH_ENABLE_FACEBOOK'], False)
- self.assertEqual(self.app.config['STORMPATH_ENABLE_GIVEN_NAME'], True)
-
-
-class TestCheckSettings(StormpathTestCase):
- """Ensure our settings checker is working properly."""
-
- def setUp(self):
- """Create an apiKey.properties file for testing."""
- super(TestCheckSettings, self).setUp()
-
- # Generate our file locally.
- self.fd, self.file = mkstemp()
- api_key_id = 'apiKey.id = %s\n' % environ.get('STORMPATH_API_KEY_ID')
- api_key_secret = 'apiKey.secret = %s\n' % environ.get(
- 'STORMPATH_API_KEY_SECRET')
- write(self.fd, api_key_id.encode('utf-8') + b'\n')
- write(self.fd, api_key_secret.encode('utf-8') + b'\n')
-
- def test_requires_api_credentials(self):
- # We'll remove our default API credentials, and ensure we get an
- # exception raised.
- self.app.config['STORMPATH_API_KEY_ID'] = None
- self.app.config['STORMPATH_API_KEY_SECRET'] = None
- self.app.config['STORMPATH_API_KEY_FILE'] = None
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Now we'll check to see that if we specify an API key ID and secret
- # things work.
- self.app.config['STORMPATH_API_KEY_ID'] = environ.get('STORMPATH_API_KEY_ID')
- self.app.config['STORMPATH_API_KEY_SECRET'] = environ.get('STORMPATH_API_KEY_SECRET')
- check_settings(self.app.config)
-
- # Now we'll check to see that if we specify an API key file things work.
- self.app.config['STORMPATH_API_KEY_ID'] = None
- self.app.config['STORMPATH_API_KEY_SECRET'] = None
- self.app.config['STORMPATH_API_KEY_FILE'] = self.file
- check_settings(self.app.config)
-
- def test_requires_application(self):
- # We'll remove our default Application, and ensure we get an exception
- # raised.
- self.app.config['STORMPATH_APPLICATION'] = None
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- def test_google_settings(self):
- # Ensure that if the user has Google login enabled, they've specified
- # the correct settings.
- self.app.config['STORMPATH_ENABLE_GOOGLE'] = True
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Ensure that things don't work if not all social configs are specified.
- self.app.config['STORMPATH_SOCIAL'] = {}
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- self.app.config['STORMPATH_SOCIAL'] = {'GOOGLE': {}}
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- self.app.config['STORMPATH_SOCIAL']['GOOGLE']['client_id'] = 'xxx'
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Now that we've configured things properly, it should work.
- self.app.config['STORMPATH_SOCIAL']['GOOGLE']['client_secret'] = 'xxx'
- check_settings(self.app.config)
-
- def test_facebook_settings(self):
- # Ensure that if the user has Facebook login enabled, they've specified
- # the correct settings.
- self.app.config['STORMPATH_ENABLE_FACEBOOK'] = True
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Ensure that things don't work if not all social configs are specified.
- self.app.config['STORMPATH_SOCIAL'] = {}
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- self.app.config['STORMPATH_SOCIAL'] = {'FACEBOOK': {}}
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- self.app.config['STORMPATH_SOCIAL']['FACEBOOK']['app_id'] = 'xxx'
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Now that we've configured things properly, it should work.
- self.app.config['STORMPATH_SOCIAL']['FACEBOOK']['app_secret'] = 'xxx'
- check_settings(self.app.config)
-
- def test_cookie_settings(self):
- # Ensure that if a user specifies a cookie domain which isn't a string,
- # an error is raised.
- self.app.config['STORMPATH_COOKIE_DOMAIN'] = 1
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Now that we've configured things properly, it should work.
- self.app.config['STORMPATH_COOKIE_DOMAIN'] = 'test'
- check_settings(self.app.config)
-
- # Ensure that if a user specifies a cookie duration which isn't a
- # timedelta object, an error is raised.
- self.app.config['STORMPATH_COOKIE_DURATION'] = 1
- self.assertRaises(ConfigurationError, check_settings, self.app.config)
-
- # Now that we've configured things properly, it should work.
- self.app.config['STORMPATH_COOKIE_DURATION'] = timedelta(minutes=1)
- check_settings(self.app.config)
-
- def tearDown(self):
- """Remove our apiKey.properties file."""
- super(TestCheckSettings, self).tearDown()
-
- # Remove our file.
- close(self.fd)
- remove(self.file)
+ self.assertEqual(self.app.config['stormpath'][
+ 'STORMPATH_WEB_REGISTER_ENABLED'], True)
+ self.assertEqual(self.app.config['stormpath'][
+ 'STORMPATH_WEB_LOGIN_ENABLED'], True)
+
+ def test_helpers(self):
+ self.manager.init_settings(self.app.config)
+ settings = self.app.config['stormpath']
+
+ self.assertEqual(settings._from_camel('givenName'), 'GIVEN_NAME')
+ self.assertEqual(settings._from_camel('given_name'), 'GIVEN_NAME')
+ self.assertNotEqual(settings._from_camel('GivenName'), 'GIVEN_NAME')
+
+ settings.store = {
+ 'application': {
+ 'name': 'StormpathApp'
+ }
+ }
+
+ # test key search
+ node, child = settings.__search__(
+ settings.store, 'STORMPATH_APPLICATION_NAME', 'STORMPATH')
+ self.assertEqual(node, settings.store['application'])
+ self.assertEqual(node[child], settings.store['application']['name'])
+
+ # key node matching with no direct mapping
+ node, child = settings.__nodematch__('STORMPATH_APPLICATION_NAME')
+ self.assertEqual(node, settings.store['application'])
+ self.assertEqual(node[child], settings.store['application']['name'])
+
+ # key node matching with direct mapping
+ node, child = settings.__nodematch__('STORMPATH_APPLICATION')
+ self.assertEqual(node, settings.store['application'])
+ self.assertEqual(node[child], settings.store['application']['name'])
+
+ def test_settings_init(self):
+ self.manager.init_settings(self.app.config)
+ settings = self.app.config['stormpath']
+
+ # flattened settings with direct mapping
+ settings['STORMPATH_APPLICATION'] = 'StormpathApp'
+ self.assertEqual(settings.store['application']['name'], 'StormpathApp')
+ self.assertEqual(settings.get('STORMPATH_APPLICATION'), 'StormpathApp')
+ self.assertEqual(settings['STORMPATH_APPLICATION'], 'StormpathApp')
+ self.assertEqual(settings.get('application')['name'], 'StormpathApp')
+ self.assertEqual(settings['application']['name'], 'StormpathApp')
+
+ def test_set(self):
+ settings = StormpathSettings()
+ # flattened setting wasn't defined during init
+ with self.assertRaises(KeyError):
+ settings['STORMPATH_WEB_SETTING'] = 'StormWebSetting'
+
+ # flattened setting defined during init
+ settings = StormpathSettings(web={'setting': 'StormSetting'})
+ settings['STORMPATH_WEB_SETTING'] = 'StormWebSetting'
+ self.assertEqual(
+ settings['web']['setting'], 'StormWebSetting')
+ # dict setting defined during init
+ settings = StormpathSettings(web={'setting': 'StormSetting'})
+ settings['web']['setting'] = 'StormWebSetting'
+ self.assertEqual(
+ settings['web']['setting'], 'StormWebSetting')
+
+ # overriding flattened setting
+ settings = StormpathSettings(web={'setting': 'StormSetting'})
+ settings['STORMPATH_WEB'] = 'StormWebSetting'
+ self.assertEqual(settings['web'], 'StormWebSetting')
+ # overriding dict setting
+ settings = StormpathSettings(web={'setting': 'StormSetting'})
+ settings['web'] = 'StormWebSetting'
+ self.assertEqual(settings['web'], 'StormWebSetting')
+
+ def test_get(self):
+ self.manager.init_settings(self.app.config)
+ settings = self.app.config['stormpath']
+
+ register_setting = {
+ 'enabled': True,
+ 'form': {
+ 'fields': {
+ 'givenName': {
+ 'enabled': True
+ }
+ }
+ }
+ }
+
+ # flattened setting without mappings
+ settings['STORMPATH_WEB_REGISTER'] = register_setting
+ self.assertEqual(
+ settings.get('STORMPATH_WEB_REGISTER'), register_setting)
+ self.assertEqual(settings['STORMPATH_WEB_REGISTER'], register_setting)
+ self.assertEqual(settings.get('web')['register'], register_setting)
+ self.assertEqual(settings['web']['register'], register_setting)
+
+ # dict setting without mappings
+ settings['web']['register'] = register_setting
+ self.assertEqual(
+ settings.get('STORMPATH_WEB_REGISTER'), register_setting)
+ self.assertEqual(settings['STORMPATH_WEB_REGISTER'], register_setting)
+ self.assertEqual(settings.get('web')['register'], register_setting)
+ self.assertEqual(settings['web']['register'], register_setting)
+
+ def test_del(self):
+ self.manager.init_settings(self.app.config)
+ settings = self.app.config['stormpath']
+ register_setting = {
+ 'enabled': True,
+ 'form': {
+ 'fields': {
+ 'givenName': {
+ 'enabled': True
+ }
+ }
+ }
+ }
+ settings['STORMPATH_WEB_REGISTER'] = register_setting
+ del settings['web']['register']
+ with self.assertRaises(KeyError):
+ settings['STORMPATH_WEB_REGISTER']
+
+ def test_camel_case(self):
+ web_settings = {
+ 'register': {
+ 'enabled': True,
+ 'form': {
+ 'fields': {
+ 'givenName': {
+ 'enabled': True
+ }
+ }
+ }
+ }
+ }
+
+ settings = StormpathSettings(web=web_settings)
+ self.assertTrue(settings['web']['register']['form']['fields'][
+ 'givenName']['enabled'])
+ self.assertTrue(
+ settings['STORMPATH_WEB_REGISTER_FORM_FIELDS_GIVEN_NAME_ENABLED'])
+ settings[
+ 'STORMPATH_WEB_REGISTER_FORM_FIELDS_GIVEN_NAME_ENABLED'] = False
+ self.assertFalse(settings['web']['register']['form']['fields'][
+ 'givenName']['enabled'])
+ self.assertFalse(
+ settings['STORMPATH_WEB_REGISTER_FORM_FIELDS_GIVEN_NAME_ENABLED'])
+ settings[
+ 'web']['register']['form']['fields']['givenName']['enabled'] = True
+ self.assertTrue(settings['web']['register']['form']['fields'][
+ 'givenName']['enabled'])
+ self.assertTrue(
+ settings['STORMPATH_WEB_REGISTER_FORM_FIELDS_GIVEN_NAME_ENABLED'])
+
+ @patch('requests.sessions.PreparedRequest')
+ def test_user_agent(self, PreparedRequest):
+ # Ensure that every request sent to the Stormpath API has a proper
+ # user agent header.
+
+ # Set mock.
+ request_mock = PreparedRequest.return_value.prepare
+ request_mock.return_value = MagicMock()
+
+ # Attempt a login using email and password.
+ with self.app.test_client() as c:
+ c.post('/login', data={
+ 'login': 'r@rdegges.com',
+ 'password': 'woot1LoveCookies!',
+ })
+
+ # Ensure our login generated a request.
+ self.assertEqual(request_mock.call_count, 1)
+ call = request_mock._mock_call_args_list[0]
+
+ # Extract the User-Agent header.
+ user_agent_header = tuple(call)[1]['headers']['User-Agent']
+
+ # Ensure that stormpath-flask and flask version are included in
+ # user-agent string.
+ stormpath_flask_version_str = (
+ 'stormpath-flask/%s' % stormpath_flask_version)
+ flask_version_str = 'flask/%s' % flask_version
+ self.assertTrue(stormpath_flask_version_str in user_agent_header)
+ self.assertTrue(flask_version_str in user_agent_header)
diff --git a/tests/test_signals.py b/tests/test_signals.py
index 2184ce2..b3fed5a 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -7,25 +7,33 @@
user_deleted,
user_updated
)
-
-from .helpers import StormpathTestCase, SignalReceiver
+from .helpers import StormpathTestCase, SignalReceiver, HttpAcceptWrapper
class TestSignals(StormpathTestCase):
"""Test signals."""
+ def setUp(self):
+ super(TestSignals, self).setUp()
+ self.html_header = 'text/html,application/xhtml+xml,application/xml;'
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.app.wsgi_app, self.html_header)
def test_user_created_signal(self):
# Subscribe to signals for user creation
signal_receiver = SignalReceiver()
user_created.connect(signal_receiver.signal_user_receiver_function)
+ # Delete the user first, so we can create the same one again.
+ self.user.delete()
+
# Register new account
with self.app.test_client() as c:
resp = c.post('/register', data={
+ 'username': 'rdegges',
'given_name': 'Randall',
'middle_name': 'Clark',
'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
+ 'email': 'r@rdegges.com',
'password': 'woot1LoveCookies!',
})
self.assertEqual(resp.status_code, 302)
@@ -37,7 +45,7 @@ def test_user_created_signal(self):
self.assertIsInstance(received_signal[1], dict)
# Correct user instance is received
created_user = received_signal[1]
- self.assertEqual(created_user['email'], 'r@testmail.stormpath.com')
+ self.assertEqual(created_user['email'], 'r@rdegges.com')
self.assertEqual(created_user['surname'], 'Degges')
def test_user_logged_in_signal(self):
@@ -45,16 +53,6 @@ def test_user_logged_in_signal(self):
signal_receiver = SignalReceiver()
user_logged_in.connect(signal_receiver.signal_user_receiver_function)
- # Create a user.
- with self.app.app_context():
- User.create(
- username = 'rdegges',
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
-
# Attempt a login using username and password.
with self.app.test_client() as c:
resp = c.post('/login', data={
@@ -70,7 +68,7 @@ def test_user_logged_in_signal(self):
self.assertIsInstance(received_signal[1], User)
# Correct user instance is received
logged_in_user = received_signal[1]
- self.assertEqual(logged_in_user.email, 'r@testmail.stormpath.com')
+ self.assertEqual(logged_in_user.email, 'r@rdegges.com')
self.assertEqual(logged_in_user.surname, 'Degges')
def test_user_is_updated_signal(self):
@@ -78,18 +76,8 @@ def test_user_is_updated_signal(self):
signal_receiver = SignalReceiver()
user_updated.connect(signal_receiver.signal_user_receiver_function)
- with self.app.app_context():
-
- # Ensure all requied fields are properly set.
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
-
- user.middle_name = 'Clark'
- user.save()
+ self.user.middle_name = 'Clark'
+ self.user.save()
# Check that signal for user update is received
self.assertEqual(len(signal_receiver.received_signals), 1)
@@ -98,7 +86,7 @@ def test_user_is_updated_signal(self):
self.assertIsInstance(received_signal[1], dict)
# Correct user instance is received
updated_user = received_signal[1]
- self.assertEqual(updated_user['email'], 'r@testmail.stormpath.com')
+ self.assertEqual(updated_user['email'], 'r@rdegges.com')
self.assertEqual(updated_user['middle_name'], 'Clark')
def test_user_is_deleted_signal(self):
@@ -106,17 +94,7 @@ def test_user_is_deleted_signal(self):
signal_receiver = SignalReceiver()
user_deleted.connect(signal_receiver.signal_user_receiver_function)
- with self.app.app_context():
-
- # Ensure all requied fields are properly set.
- user = User.create(
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- given_name = 'Randall',
- surname = 'Degges',
- )
-
- user.delete()
+ self.user.delete()
# Check that signal for user deletion is received
self.assertEqual(len(signal_receiver.received_signals), 1)
@@ -125,4 +103,4 @@ def test_user_is_deleted_signal(self):
self.assertIsInstance(received_signal[1], dict)
# Correct user instance is received
deleted_user = received_signal[1]
- self.assertEqual(deleted_user['email'], 'r@testmail.stormpath.com')
+ self.assertEqual(deleted_user['email'], 'r@rdegges.com')
diff --git a/tests/test_stormpath.py b/tests/test_stormpath.py
index 0a65966..5f0d128 100644
--- a/tests/test_stormpath.py
+++ b/tests/test_stormpath.py
@@ -15,7 +15,6 @@
from flask_stormpath import (
StormpathManager,
User,
- groups_required,
login_user,
logout_user,
)
diff --git a/tests/test_views.py b/tests/test_views.py
index 1aa8186..ec19669 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,204 +1,775 @@
"""Run tests against our custom views."""
+import sys
+import os
from flask_stormpath.models import User
+from .helpers import StormpathTestCase, HttpAcceptWrapper, create_config_path
+from stormpath.resources import Resource
+from stormpath.resources.provider import Provider
+from stormpath.error import Error as StormpathError
+from flask_stormpath.views import (
+ StormpathView, FacebookLoginView, GoogleLoginView, VerifyEmailView, View)
+from flask import session, url_for, Response
+from flask_login import current_user
+from werkzeug.exceptions import BadRequest
+from ruamel.yaml import util, round_trip_dump
+import json
+import shutil
-from .helpers import StormpathTestCase
+if sys.version_info.major == 3:
+ from unittest.mock import patch
+else:
+ from mock import patch
-class TestRegister(StormpathTestCase):
+class StormpathViewTestCase(StormpathTestCase):
+ """Base test class for Stormpath views."""
+
+ def check_header(self, st, headers):
+ return any(st in header for header in headers)
+
+ def assertFormSettings(self, expected_fields):
+ """
+ Expected response set in json tests is based on the default settings
+ specified in the config file. This method ensures that the developer
+ didn't change the config file before running tests.
+ """
+
+ # Build form fields from the config and compare them to those
+ # specified in the expected response.
+ form_fields = []
+ for key in self.form_fields.keys():
+ field = self.form_fields[key].copy()
+
+ # Convert fields from config to json response format.
+ if field['enabled']:
+ field.pop('enabled')
+ field['name'] = Resource.from_camel_case(key)
+ field['visible'] = True
+ form_fields.append(field)
+
+ # Sort and compare form fields
+ self.assertDictList(form_fields, expected_fields, 'name')
+
+ def assertJsonResponse(
+ self, method, view, status_code, expected_response,
+ user_to_json=False, **kwargs):
+ """Custom assert for testing json responses on flask_stormpath
+ views."""
+
+ # Set our request type to json.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.json_header)
+
+ with self.app.test_client() as c:
+ # Create a request.
+ allowed_methods = {
+ 'get': c.get,
+ 'post': c.post}
+
+ if method in allowed_methods:
+ resp = allowed_methods[method]('/%s' % view, **kwargs)
+ else:
+ raise ValueError('\'%s\' is not a supported method.' % method)
+
+ # Ensure that the HTTP status code is correct.
+ self.assertEqual(resp.status_code, status_code)
+
+ # If we're expecting a redirect, follow the redirect flow so we
+ # can access the final response data.
+ if status_code == 302:
+ resp = allowed_methods[method](
+ '/%s' % view, follow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+
+ # Check that response is json.
+ self.assertFalse(self.check_header('text/html', resp.headers[0]))
+ self.assertTrue(self.check_header(
+ 'application/json', resp.headers[0]))
+
+ # If we're comparing json response with account info, make sure
+ # that the following values are present in the response and pop
+ # them, since we cannot predetermine these values in our expected
+ # response.
+ if user_to_json:
+ request_response = json.loads(resp.data.decode())
+ undefined_data = ('href', 'modified_at', 'created_at')
+ self.assertTrue(
+ all(key in request_response['account'].keys()
+ for key in undefined_data))
+ for key in undefined_data:
+ request_response['account'].pop(key)
+ else:
+ request_response = json.loads(resp.data.decode())
+
+ # Convert responses to dicts, sort them if necessary, and compare.
+ expected_response = json.loads(expected_response)
+ if hasattr(request_response, 'sort'):
+ self.assertDictList(request_response, expected_response, 'name')
+ else:
+ self.assertEqual(request_response, expected_response)
+
+ def assertDisabledView(self, view_name, post_data):
+ # Ensure that a disabled view will always return a 404 response.
+
+ # Create a config directory for storing different temporary yaml config
+ # files needed for testing.
+ self.config_dir = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), 'config')
+ if not os.path.exists(self.config_dir):
+ os.makedirs(self.config_dir)
+
+ # Disable the view.
+
+ # Create a new updated config file from the default one.
+ config, ind, bsi = util.load_yaml_guess_indent(
+ open(self.app.config['STORMPATH_CONFIG_PATH']))
+ config['web'][view_name]['enabled'] = False
+ config_name = 'test-config-%s' % view_name
+ round_trip_dump(
+ config, open(create_config_path(config_name, default=False), 'w'),
+ indent=ind, block_seq_indent=bsi)
+
+ # Set new config file before app init.
+ os.environ['TEST_CONFIG'] = json.dumps(
+ {'filename': config_name, 'default': False})
+
+ # Reinitialize the application.
+ self.reinit_app()
+
+ # Ensure that both GET and POST will return a 404.
+ with self.app.test_client() as c:
+ resp = c.get('/' + view_name)
+ self.assertEqual(resp.status_code, 404)
+
+ resp = c.post('/' + view_name, data=post_data)
+ self.assertEqual(resp.status_code, 404)
+
+ # Revert to the default config.
+ os.environ['TEST_CONFIG'] = json.dumps({})
+
+ def tearDown(self):
+ super(StormpathViewTestCase, self).tearDown()
+
+ # Destroy temporary yaml config resources.
+ if hasattr(self, 'config_dir') and os.path.exists(self.config_dir):
+ shutil.rmtree(self.config_dir)
+
+
+class TestHelperMethods(StormpathViewTestCase):
+ """Test our helper functions."""
+
+ def setUp(self):
+ super(TestHelperMethods, self).setUp()
+ # We need a config for a StormpathView, so we'll use login form config.
+ self.config = self.app.config['stormpath']['web']['login']
+
+ # Create an 'invalid_request' view. This view has to be implemented by
+ # the developer/framework, so it is not part of the stormpath-flask
+ # library. We will create one for testing purposes. Flask requires
+ # this do be done in setUp, before the first request is handled.
+ class InvalidRequestView(View):
+ def dispatch_request(self):
+ xml = 'Invalid request.'
+ return Response(xml, mimetype='text/xml', status=400)
+
+ self.app.add_url_rule(
+ self.app.config['stormpath']['web']['invalidRequest']['uri'],
+ 'stormpath.invalid_request',
+ InvalidRequestView.as_view('invalid_request'),
+ )
+
+ # Ensure that StormpathView.accept_header is properly set.
+ with self.app.test_client() as c:
+ # Create a request with html accept header
+ c.get('/')
+
+ with self.app.app_context():
+ self.view = StormpathView(self.config)
+
+ def test_make_stormpath_response(self):
+ data = {'foo': 'bar'}
+ with self.app.test_client() as c:
+ # Ensure that stormpath_response is json if request wants json.
+ c.get('/')
+ resp = self.view.make_stormpath_response(json.dumps(data))
+ self.assertFalse(self.check_header(
+ 'text/html', resp.headers[0]))
+ self.assertTrue(self.check_header(
+ 'application/json', resp.headers[0]))
+ self.assertEqual(resp.data.decode(), '{"foo": "bar"}')
+
+ # Ensure that stormpath_response is html if request wants html.
+ c.get('/')
+ resp = self.view.make_stormpath_response(
+ data, template='flask_stormpath/base.html', return_json=False)
+
+ # Python 3 support for testing html response.
+ if sys.version_info.major == 3:
+ self.assertTrue(isinstance(resp, str))
+ else:
+ self.assertTrue(isinstance(resp, unicode))
+
+ def test_validate_request(self):
+ with self.app.test_client() as c:
+ # Ensure that a request with an html accept header will return an
+ # html response.
+ c.get('/')
+ with self.app.app_context():
+ self.view.__init__(self.config)
+ self.assertEqual(self.view.accept_header, 'text/html')
+
+ # Ensure that a request with a json accept header will return a
+ # json response.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.json_header)
+ c.get('/')
+ with self.app.app_context():
+ self.view.__init__(self.config)
+ self.assertEqual(self.view.accept_header, 'application/json')
+
+ # Ensure that a request with no accept headers will return the
+ # first allowed type.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, '')
+ c.get('/')
+ with self.app.app_context():
+ self.view.__init__(self.config)
+ self.assertEqual(
+ self.view.accept_header,
+ self.app.config['stormpath']['web']['produces'][0])
+
+ # Ensure that a request with */* accept header will return the
+ # first allowed type.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, '*/*')
+ c.get('/')
+ with self.app.app_context():
+ self.view.__init__(self.config)
+ self.assertEqual(
+ self.view.accept_header,
+ self.app.config['stormpath']['web']['produces'][0])
+
+ # Ensure that an invalid accept header type will return None.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, 'text/plain')
+ c.get('/')
+ with self.app.app_context():
+ self.view.__init__(self.config)
+ self.assertEqual(self.view.accept_header, None)
+
+ def test_accept_header_valid(self):
+ # Ensure that StormpathView.accept_header is properly set.
+ with self.app.test_client() as c:
+ # Create a request with html accept header
+ c.get('/')
+
+ with self.app.app_context():
+ view = StormpathView(self.config)
+ self.assertEqual(view.accept_header, 'text/html')
+ self.assertFalse(view.invalid_request)
+
+ # Create a request with json accept header
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.json_header)
+ c.get('/')
+
+ with self.app.app_context():
+ view = StormpathView(self.config)
+ self.assertEqual(view.accept_header, 'application/json')
+ self.assertFalse(view.invalid_request)
+
+ def test_accept_header_invalid(self):
+ # If a request type is not HTML, JSON, */* or empty, request is
+ # deemed invalid and is passed to the developer to handle the response.
+ # The developer handles the response via uri specified in the config
+ # file, in:
+ # web > invalidRequest.
+ with self.app.test_client() as c:
+ # Create a request with an accept header not supported by
+ # flask_stormpath.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, 'text/plain')
+ # We'll use login since '/' is not an implemented route.
+ response = c.get('/login')
+
+ # Ensure that accept header and invalid_request are properly set.
+ with self.app.app_context():
+ view = StormpathView(self.config)
+ self.assertEqual(view.accept_header, None)
+ self.assertTrue(view.invalid_request)
+
+ # If a view for 'invalid_request' uri is implemented, the response
+ # is determined in that view. (We've implemented that as our
+ # InvalidRequestView).
+ response = c.get('/login', follow_redirects=True)
+ self.assertEqual(response.status, '400 BAD REQUEST')
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.content_type, 'text/xml; charset=utf-8')
+
+ # If view for that uri is not implemented, the response is 501.
+ self.app.config[
+ 'stormpath']['web']['invalidRequest']['uri'] = None
+ response = c.get('/login', follow_redirects=True)
+
+ self.assertEqual(response.status, '501 NOT IMPLEMENTED')
+ self.assertEqual(response.status_code, 501)
+ self.assertEqual(response.content_type, 'text/html')
+
+ @patch('flask_stormpath.views.flash')
+ @patch('flask_stormpath.request_processors.get_accept_header')
+ def test_process_stormpath_error(self, accept_header, flash):
+ # Ensure that process_stormpath_error properly parses the error
+ # message and returns a proper response (json or html).
+
+ error = StormpathError('This is a default message.')
+
+ # Ensure that process_stormpath_error will return a proper response.
+ with self.app.test_request_context():
+ # HTML (or other non JSON) response.
+ accept_header.return_value = 'text/html'
+ response = self.view.process_stormpath_error(error)
+ self.assertIsNone(response)
+ self.assertEqual(flash.call_count, 1)
+
+ # JSON response.
+ accept_header.return_value = 'application/json'
+ response = self.view.process_stormpath_error(error)
+ self.assertEqual(
+ response.headers['Content-Type'], 'application/json')
+ json_response = json.loads(response.response[0].decode())
+ self.assertEqual(
+ json_response['message'], 'This is a default message.')
+
+ # Ensure that self.error_message will check for error.user_message
+ # first, but will default to error.message otherwise.
+ error.user_message = 'This is a user message.'
+ response = self.view.process_stormpath_error(error)
+ json_response = json.loads(response.response[0].decode())
+ self.assertEqual(
+ json_response['message'], 'This is a user message.')
+
+ # Ensure that certain error codes will return error.message, not
+ # error.user_message.
+ error.code = 7102
+ response = self.view.process_stormpath_error(error)
+ json_response = json.loads(response.response[0].decode())
+ self.assertEqual(
+ json_response['message'], 'This is a default message.')
+
+ def test_csrf_disabled_on_json(self):
+ # Ensure that JSON requests have CSRF disabled.
+
+ self.app.config['WTF_CSRF_ENABLED'] = True
+ with self.app.test_client() as c:
+ # Ensure that HTML will have CSRF enabled.
+ c.get('/')
+ with self.app.app_context():
+ self.view = StormpathView(self.config)
+ self.assertTrue(self.view.form.meta.csrf)
+
+ # Ensure that JSON will have CSRF disabled.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, self.json_header)
+ c.get('/')
+ with self.app.app_context():
+ self.view = StormpathView(self.config)
+ self.assertFalse(self.view.form.meta.csrf)
+
+ # Ensure that non JSON will have CSRF enabled.
+ self.app.wsgi_app = HttpAcceptWrapper(
+ self.default_wsgi_app, 'text/plain')
+ c.get('/')
+ with self.app.app_context():
+ self.view = StormpathView(self.config)
+ self.assertTrue(self.view.form.meta.csrf)
+
+
+class TestRegister(StormpathViewTestCase):
"""Test our registration view."""
- def test_default_fields(self):
- # By default, we'll register new users with first name, last name,
- # email, and password.
+ def setUp(self):
+ super(TestRegister, self).setUp()
+ self.form_fields = self.app.config['stormpath']['web']['register'][
+ 'form']['fields']
+
+ def test_get(self):
+ # Ensure that a get request will only render the template and skip
+ # form validation and users creation.
with self.app.test_client() as c:
+ resp = c.get('/register')
+ self.assertEqual(resp.status_code, 200)
+ def test_default_fields(self):
+ # By default, we'll register new users with username, first name,
+ # last name, email, and password.
+ with self.app.test_client() as c:
# Ensure that missing fields will cause a failure.
resp = c.post('/register', data={
- 'email': 'r@testmail.stormpath.com',
- 'password': 'woot1LoveCookies!',
+ 'email': 'r@rdegges2.com',
+ 'password': 'thisisMy0therpassword...',
})
self.assertEqual(resp.status_code, 200)
# Ensure that valid fields will result in a success.
resp = c.post('/register', data={
- 'username': 'rdegges',
- 'given_name': 'Randall',
- 'middle_name': 'Clark',
- 'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
- 'password': 'woot1LoveCookies!',
+ 'username': 'randalldeg_registration',
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
+ 'password': 'thisisMy0therpassword...',
+ })
+ self.assertEqual(resp.status_code, 302)
+
+ def test_confirm_password(self):
+ # Register a user with confirmPassword enabled.
+ self.form_fields['confirmPassword']['enabled'] = True
+
+ with self.app.test_client() as c:
+ # Ensure that confirmPassword will be popped from data before
+ # creating the new User instance.
+ resp = c.post('/register', data={
+ 'username': 'randalldeg_registration',
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
+ 'password': 'thisisMy0therpassword...',
+ 'confirm_password': 'thisisMy0therpassword...'
})
self.assertEqual(resp.status_code, 302)
def test_disable_all_except_mandatory(self):
# Here we'll disable all the fields except for the mandatory fields:
# email and password.
- self.app.config['STORMPATH_ENABLE_USERNAME'] = False
- self.app.config['STORMPATH_ENABLE_GIVEN_NAME'] = False
- self.app.config['STORMPATH_ENABLE_MIDDLE_NAME'] = False
- self.app.config['STORMPATH_ENABLE_SURNAME'] = False
+ for field in ['givenName', 'middleName', 'surname', 'username']:
+ self.form_fields[field]['enabled'] = False
with self.app.test_client() as c:
-
# Ensure that missing fields will cause a failure.
resp = c.post('/register', data={
- 'email': 'r@testmail.stormpath.com',
+ 'email': 'r_registration@rdegges.com',
})
self.assertEqual(resp.status_code, 200)
# Ensure that valid fields will result in a success.
resp = c.post('/register', data={
- 'email': 'r@testmail.stormpath.com',
- 'password': 'woot1LoveCookies!',
+ 'email': 'r_registration@rdegges.com',
+ 'password': 'thisisMy0therpassword...',
})
self.assertEqual(resp.status_code, 302)
def test_require_settings(self):
# Here we'll change our backend behavior such that users *can* enter a
- # first and last name, but they aren't required server side.
- # email and password.
- self.app.config['STORMPATH_REQUIRE_GIVEN_NAME'] = False
- self.app.config['STORMPATH_REQUIRE_SURNAME'] = False
+ # username, first and last name, but they aren't required server side.
+ for field in ['givenName', 'surname', 'username']:
+ self.form_fields[field]['required'] = False
with self.app.test_client() as c:
-
# Ensure that registration works *without* given name and surname
# since they aren't required.
resp = c.post('/register', data={
- 'email': 'r@testmail.stormpath.com',
- 'password': 'woot1LoveCookies!',
+ 'email': 'r_registration@rdegges.com',
+ 'password': 'thisisMy0therpassword...'
})
self.assertEqual(resp.status_code, 302)
# Find our user account that was just created, and ensure the given
# name and surname fields were set to our default string.
- user = User.from_login('r@testmail.stormpath.com', 'woot1LoveCookies!')
+ user = User.from_login(
+ 'r_registration@rdegges.com', 'thisisMy0therpassword...')
self.assertEqual(user.given_name, 'Anonymous')
self.assertEqual(user.surname, 'Anonymous')
+ self.assertEqual(user.username, user.email)
def test_error_messages(self):
+ # We don't need a username field for this test. We'll disable it
+ # so the form can be valid.
+ self.form_fields['username']['enabled'] = False
+
with self.app.test_client() as c:
+ # Ensure that the form error is raised if the email already
+ # exists.
+ resp = c.post('/register', data={
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r@rdegges.com',
+ 'password': 'Hilolsds1',
+ })
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Account with that email already exists.'
+ in resp.data.decode('utf-8'))
+ self.assertFalse("developerMessage" in resp.data.decode('utf-8'))
# Ensure that an error is raised if an invalid password is
# specified.
resp = c.post('/register', data={
- 'given_name': 'Randall',
- 'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
'password': 'hilol',
})
self.assertEqual(resp.status_code, 200)
-
- self.assertTrue('Account password minimum length not satisfied.' in resp.data.decode('utf-8'))
- self.assertFalse('developerMessage' in resp.data.decode('utf-8'))
+ self.assertTrue(
+ 'Account password minimum length not satisfied.' in
+ resp.data.decode('utf-8'))
+ self.assertFalse("developerMessage" in resp.data.decode('utf-8'))
resp = c.post('/register', data={
- 'given_name': 'Randall',
- 'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
'password': 'hilolwoot1',
})
self.assertEqual(resp.status_code, 200)
-
- self.assertTrue('Password requires at least 1 uppercase character.' in resp.data.decode('utf-8'))
- self.assertFalse('developerMessage' in resp.data.decode('utf-8'))
+ self.assertTrue(
+ 'Password requires at least 1 uppercase character.' in
+ resp.data.decode('utf-8'))
+ self.assertFalse("developerMessage" in resp.data.decode('utf-8'))
resp = c.post('/register', data={
- 'given_name': 'Randall',
- 'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
'password': 'hilolwoothi',
})
self.assertEqual(resp.status_code, 200)
- self.assertTrue('Password requires at least 1 numeric character.' in resp.data.decode('utf-8'))
- self.assertFalse('developerMessage' in resp.data.decode('utf-8'))
+ self.assertTrue(
+ 'Password requires at least 1 numeric character.' in
+ resp.data.decode('utf-8'))
+ self.assertFalse("developerMessage" in resp.data.decode('utf-8'))
+
+ def test_autologin(self):
+ # If the autologin option is enabled the user must be logged in after
+ # successful registration.
+ self.app.config['stormpath']['web']['register']['autoLogin'] = True
+ stormpath_register_redirect_url = '/redirect_for_registration'
+ self.app.config['stormpath']['web']['register'][
+ 'nextUri'] = stormpath_register_redirect_url
+
+ with self.app.test_client() as c:
+ resp = c.get('/register')
+ self.assertFalse('user_id' in session)
+
+ # Check that the user was redirected to the proper url and is
+ # logged in after successful registration
+ resp = c.post('/register', data={
+ 'username': 'randalldeg_registration',
+ 'given_name': 'Randall registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
+ 'password': 'thisisMy0therpassword...',
+ })
+
+ # Get our user that was just created
+ user = User.from_login(
+ 'r_registration@rdegges.com', 'thisisMy0therpassword...')
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(stormpath_register_redirect_url in resp.location)
+ self.assertEqual(session['user_id'], user.href)
- def test_redirect_to_login_and_register_url(self):
+ resp = c.get('/logout')
+ self.assertFalse('user_id' in session)
+
+ def test_redirect_to_login_or_register_url(self):
# Setting redirect URL to something that is easy to check
- stormpath_redirect_url = '/redirect_for_login_and_registration'
- self.app.config['STORMPATH_REDIRECT_URL'] = stormpath_redirect_url
+ stormpath_login_redirect_url = '/redirect_for_login'
+ stormpath_register_redirect_url = '/redirect_for_registration'
+ self.app.config['stormpath']['web']['login'][
+ 'nextUri'] = stormpath_login_redirect_url
+ self.app.config['stormpath']['web']['register'][
+ 'nextUri'] = stormpath_register_redirect_url
+
+ # We don't need a username field for this test. We'll disable it
+ # so the form can be valid.
+ self.form_fields['username']['enabled'] = False
with self.app.test_client() as c:
# Ensure that valid registration will redirect to
- # STORMPATH_REDIRECT_URL
- resp = c.post(
- '/register',
- data=
- {
- 'given_name': 'Randall',
- 'middle_name': 'Clark',
- 'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
- 'password': 'woot1LoveCookies!',
- })
+ # register redirect url
+ resp = c.post('/register', data={
+ 'given_name': 'Randall registration',
+ 'middle_name': 'Clark registration',
+ 'surname': 'Degges registration',
+ 'email': 'r_registration@rdegges.com',
+ 'password': 'thisisMy0therpassword...',
+ })
self.assertEqual(resp.status_code, 302)
location = resp.headers.get('location')
- self.assertTrue(stormpath_redirect_url in location)
+ self.assertTrue(stormpath_register_redirect_url in location)
+ self.assertFalse(stormpath_login_redirect_url in location)
- def test_redirect_to_register_url(self):
- # Setting redirect URLs to something that is easy to check
- stormpath_redirect_url = '/redirect_for_login'
- stormpath_registration_redirect_url = '/redirect_for_registration'
- self.app.config['STORMPATH_REDIRECT_URL'] = stormpath_redirect_url
- self.app.config['STORMPATH_REGISTRATION_REDIRECT_URL'] = \
- stormpath_registration_redirect_url
+ # We're disabling the default register redirect so we can check if
+ # the login redirect will be applied
+ self.app.config['stormpath']['web']['register']['nextUri'] = None
- with self.app.test_client() as c:
# Ensure that valid registration will redirect to
- # STORMPATH_REGISTRATION_REDIRECT_URL if it exists
- resp = c.post(
- '/register',
- data=
- {
- 'given_name': 'Randall',
- 'middle_name': 'Clark',
- 'surname': 'Degges',
- 'email': 'r@testmail.stormpath.com',
- 'password': 'woot1LoveCookies!',
- })
+ # login redirect url
+ resp = c.post('/register', data={
+ 'given_name': 'Randall_registration2',
+ 'middle_name': 'Clark_registration2',
+ 'surname': 'Degges_registration2',
+ 'email': 'r_registration2@rdegges.com',
+ 'password': 'thisisMy0therpassword2...',
+ })
+
+ self.assertEqual(resp.status_code, 302)
+ location = resp.headers.get('location')
+ self.assertTrue(stormpath_login_redirect_url in location)
+ self.assertFalse(stormpath_register_redirect_url in location)
+
+ # We're disabling the default login redirect so we can check if
+ # the default redirect will be applied
+ self.app.config['stormpath']['web']['login']['nextUri'] = None
+
+ # Ensure that valid registration will redirect to
+ # default redirect url
+ resp = c.post('/register', data={
+ 'given_name': 'Randall_registration3',
+ 'middle_name': 'Clark_registration3',
+ 'surname': 'Degges_registration3',
+ 'email': 'r_registration3@rdegges.com',
+ 'password': 'thisisMy0therpassword3...',
+ })
self.assertEqual(resp.status_code, 302)
location = resp.headers.get('location')
- self.assertFalse(stormpath_redirect_url in location)
- self.assertTrue(stormpath_registration_redirect_url in location)
+ self.assertFalse(stormpath_login_redirect_url in location)
+ self.assertFalse(stormpath_register_redirect_url in location)
+
+ def test_json_response_get(self):
+ # Here we'll disable all the fields except for the mandatory fields:
+ # email and password.
+ for field in ['givenName', 'middleName', 'surname', 'username']:
+ self.form_fields[field]['enabled'] = False
+
+ # Specify expected response.
+ expected_response = [
+ {'label': 'Email',
+ 'name': 'email',
+ 'placeholder': 'Email',
+ 'required': True,
+ 'visible': True,
+ 'type': 'email'},
+ {'label': 'Password',
+ 'name': 'password',
+ 'placeholder': 'Password',
+ 'required': True,
+ 'visible': True,
+ 'type': 'password'}]
+
+ # Ensure that the form fields specified in the expected response
+ # match those specified in the config file.
+ self.assertFormSettings(expected_response)
+
+ self.assertJsonResponse(
+ 'get', 'register', 200, json.dumps(expected_response))
+
+ def test_json_response_valid_form(self):
+ # Specify user data
+ user_data = {
+ 'username': 'rdegges2',
+ 'email': 'r@rdegges2.com',
+ 'given_name': 'Randall2',
+ 'middle_name': None,
+ 'surname': 'Degges2',
+ 'password': 'woot1LoveCookies!2'
+ }
+
+ # Specify expected response.
+ expected_response = {'account': user_data.copy()}
+ expected_response['account']['status'] = 'ENABLED'
+ expected_response['account']['full_name'] = 'Randall2 Degges2'
+ expected_response['account'].pop('password')
+
+ # Specify post data
+ json_data = json.dumps(user_data)
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ self.assertJsonResponse(
+ 'post', 'register', 200, json.dumps(expected_response),
+ user_to_json=True, **request_kwargs)
+
+ def test_json_response_stormpath_error(self):
+ # Specify post data
+ json_data = json.dumps({
+ 'username': 'rdegges',
+ 'email': 'r@rdegges.com',
+ 'given_name': 'Randall',
+ 'middle_name': 'Clark',
+ 'surname': 'Degges',
+ 'password': 'woot1LoveCookies!'})
+
+ # Specify expected response
+ expected_response = {
+ 'message': (
+ 'Account with that email already exists.' +
+ ' Please choose another email.'),
+ 'status': 409}
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ self.assertJsonResponse(
+ 'post', 'register', 409, json.dumps(expected_response),
+ **request_kwargs)
+ def test_json_response_form_error(self):
+ # Specify post data
+ json_data = json.dumps({
+ 'username': 'rdegges',
+ 'email': 'r@rdegges.com',
+ 'middle_name': 'Clark',
+ 'surname': 'Degges',
+ 'password': 'woot1LoveCookies!'})
-class TestLogin(StormpathTestCase):
+ # Specify expected response
+ expected_response = {
+ 'message': {"given_name": ["First Name is required."]},
+ 'status': 400}
+
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ self.assertJsonResponse(
+ 'post', 'register', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+
+class TestLogin(StormpathViewTestCase):
"""Test our login view."""
- def test_email_login(self):
- # Create a user.
- with self.app.app_context():
- User.create(
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
+ def setUp(self):
+ super(TestLogin, self).setUp()
+ # We need to set form fields to test out json stuff in the
+ # assertJsonResponse method.
+ self.form_fields = self.app.config['stormpath']['web']['login'][
+ 'form']['fields']
+ def test_enabled(self):
+ # Ensure that a disabled login will return 404.
+ data = {
+ 'login': 'r@rdegges.com',
+ 'password': 'woot1LoveCookies!'
+ }
+ self.assertDisabledView('login', data)
+
+ def test_email_login(self):
# Attempt a login using email and password.
with self.app.test_client() as c:
resp = c.post('/login', data={
- 'login': 'r@testmail.stormpath.com',
+ 'login': 'r@rdegges.com',
'password': 'woot1LoveCookies!',
})
self.assertEqual(resp.status_code, 302)
def test_username_login(self):
- # Create a user.
- with self.app.app_context():
- User.create(
- username = 'rdegges',
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
-
# Attempt a login using username and password.
with self.app.test_client() as c:
resp = c.post('/login', data={
@@ -208,16 +779,6 @@ def test_username_login(self):
self.assertEqual(resp.status_code, 302)
def test_error_messages(self):
- # Create a user.
- with self.app.app_context():
- User.create(
- username = 'rdegges',
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
-
# Ensure that an error is raised if an invalid username or password is
# specified.
with self.app.test_client() as c:
@@ -227,66 +788,129 @@ def test_error_messages(self):
})
self.assertEqual(resp.status_code, 200)
- #self.assertTrue('Invalid username or password.' in resp.data.decode('utf-8'))
- self.assertTrue('Login attempt failed because the specified password is incorrect.' in resp.data.decode('utf-8'))
- self.assertFalse('developerMessage' in resp.data.decode('utf-8'))
+ self.assertTrue(
+ 'Invalid username or password.' in resp.data.decode('utf-8'))
+ self.assertFalse("developerMessage" in resp.data.decode('utf-8'))
- def test_redirect_to_login_and_register_url(self):
- # Create a user.
- with self.app.app_context():
- User.create(
- username = 'rdegges',
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
+ # Ensure that an error is raised if the account referencing
+ # login email address is not verified.
+ self.user.status = 'UNVERIFIED'
+ self.user.save()
+
+ resp = c.post('/login', data={
+ 'login': 'rdegges',
+ 'password': 'hilol',
+ })
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Login attempt failed because the Account is not verified.' in
+ resp.data.decode('utf-8'))
+ self.assertFalse("developerMessage" in resp.data.decode('utf-8'))
+ def test_redirect_to_login_or_register_url(self):
# Setting redirect URL to something that is easy to check
- stormpath_redirect_url = '/redirect_for_login_and_registration'
- self.app.config['STORMPATH_REDIRECT_URL'] = stormpath_redirect_url
+ stormpath_login_redirect_url = '/redirect_for_login'
+ stormpath_register_redirect_url = '/redirect_for_registration'
+ self.app.config['stormpath']['web']['login'][
+ 'nextUri'] = stormpath_login_redirect_url
+ self.app.config['stormpath']['web']['register'][
+ 'nextUri'] = stormpath_register_redirect_url
with self.app.test_client() as c:
# Attempt a login using username and password.
- resp = c.post(
- '/login',
- data={'login': 'rdegges', 'password': 'woot1LoveCookies!',})
+ resp = c.post('/login', data={
+ 'login': 'rdegges',
+ 'password': 'woot1LoveCookies!'
+ })
self.assertEqual(resp.status_code, 302)
location = resp.headers.get('location')
- self.assertTrue(stormpath_redirect_url in location)
+ self.assertTrue(stormpath_login_redirect_url in location)
+ self.assertFalse(stormpath_register_redirect_url in location)
- def test_redirect_to_register_url(self):
- # Create a user.
- with self.app.app_context():
- User.create(
- username = 'rdegges',
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
+ def test_json_response_get(self):
+ # Specify expected response.
+ expected_response = [
+ {'label': 'Username or Email',
+ 'name': 'login',
+ 'placeholder': 'Username or Email',
+ 'required': True,
+ 'visible': True,
+ 'type': 'text'},
+ {'label': 'Password',
+ 'name': 'password',
+ 'placeholder': 'Password',
+ 'required': True,
+ 'visible': True,
+ 'type': 'password'}]
- # Setting redirect URLs to something that is easy to check
- stormpath_redirect_url = '/redirect_for_login'
- stormpath_registration_redirect_url = '/redirect_for_registration'
- self.app.config['STORMPATH_REDIRECT_URL'] = stormpath_redirect_url
- self.app.config['STORMPATH_REGISTRATION_REDIRECT_URL'] = \
- stormpath_registration_redirect_url
+ # Ensure that the form fields specified in the expected response
+ # match those specified in the config file.
+ self.assertFormSettings(expected_response)
- with self.app.test_client() as c:
- # Attempt a login using username and password.
- resp = c.post(
- '/login',
- data={'login': 'rdegges', 'password': 'woot1LoveCookies!',})
+ self.assertJsonResponse(
+ 'get', 'login', 200, json.dumps(expected_response))
- self.assertEqual(resp.status_code, 302)
- location = resp.headers.get('location')
- self.assertTrue('redirect_for_login' in location)
- self.assertFalse('redirect_for_registration' in location)
+ def test_json_response_valid_form(self):
+ # Specify expected response.
+ expected_response = {'account': {
+ 'username': 'rdegges',
+ 'email': 'r@rdegges.com',
+ 'given_name': 'Randall',
+ 'middle_name': None,
+ 'surname': 'Degges',
+ 'full_name': 'Randall Degges',
+ 'status': 'ENABLED'}
+ }
+
+ # Specify post data
+ json_data = json.dumps({
+ 'login': 'r@rdegges.com',
+ 'password': 'woot1LoveCookies!'})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+ self.assertJsonResponse(
+ 'post', 'login', 200, json.dumps(expected_response),
+ user_to_json=True, **request_kwargs)
+ def test_json_response_stormpath_error(self):
+ # Specify post data
+ json_data = json.dumps({
+ 'login': 'wrong@email.com',
+ 'password': 'woot1LoveCookies!'})
-class TestLogout(StormpathTestCase):
+ # Specify expected response
+ expected_response = {
+ 'message': 'Invalid username or password.',
+ 'status': 400}
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+ self.assertJsonResponse(
+ 'post', 'login', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+ def test_json_response_form_error(self):
+ # Specify post data
+ json_data = json.dumps({
+ 'password': 'woot1LoveCookies!'})
+
+ # Specify expected response
+ expected_response = {
+ 'message': {"login": ["Username or Email is required."]},
+ 'status': 400}
+
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ self.assertJsonResponse(
+ 'post', 'login', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+
+class TestLogout(StormpathViewTestCase):
"""Test our logout view."""
def test_logout_works_with_anonymous_users(self):
@@ -295,19 +919,10 @@ def test_logout_works_with_anonymous_users(self):
self.assertEqual(resp.status_code, 302)
def test_logout_works(self):
- # Create a user.
- with self.app.app_context():
- User.create(
- given_name = 'Randall',
- surname = 'Degges',
- email = 'r@testmail.stormpath.com',
- password = 'woot1LoveCookies!',
- )
-
with self.app.test_client() as c:
# Log this user in.
resp = c.post('/login', data={
- 'login': 'r@testmail.stormpath.com',
+ 'login': 'r@rdegges.com',
'password': 'woot1LoveCookies!',
})
self.assertEqual(resp.status_code, 302)
@@ -315,3 +930,1026 @@ def test_logout_works(self):
# Log this user out.
resp = c.get('/logout')
self.assertEqual(resp.status_code, 302)
+
+ def test_json_response_get(self):
+ # We'll use login form for our json response
+ self.form_fields = self.app.config['stormpath']['web']['login'][
+ 'form']['fields']
+
+ # We'll set the redirect url login since test client cannot redirect
+ # to index view.
+ self.app.config['stormpath']['web']['logout']['nextUri'] = '/login'
+
+ # Specify expected response.
+ expected_response = [
+ {'label': 'Username or Email',
+ 'name': 'login',
+ 'placeholder': 'Username or Email',
+ 'required': True,
+ 'visible': True,
+ 'type': 'text'},
+ {'label': 'Password',
+ 'name': 'password',
+ 'placeholder': 'Password',
+ 'required': True,
+ 'visible': True,
+ 'type': 'password'}]
+
+ # Ensure that the form fields specified in the expected response
+ # match those specified in the config file.
+ self.assertFormSettings(expected_response)
+
+ self.assertJsonResponse(
+ 'get', 'logout', 302, json.dumps(expected_response))
+
+
+class TestForgot(StormpathViewTestCase):
+ """Test our forgot view."""
+
+ def setUp(self):
+ super(TestForgot, self).setUp()
+ # We need to set form fields to test out json stuff in the
+ # assertJsonResponse method.
+ self.form_fields = self.app.config['stormpath']['web'][
+ 'forgotPassword']['form']['fields']
+
+ def test_proper_template_rendering(self):
+ # Ensure that proper templates are rendered based on the request
+ # method.
+ with self.app.test_client() as c:
+ # Ensure request.GET will render the forgot_password.html template.
+ resp = c.get('/forgot')
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Enter your email address below to reset your password.' in
+ resp.data.decode('utf-8'))
+
+ # Ensure that request.POST will render the
+ # forgot_password_success.html
+ resp = c.post('/forgot', data={'email': 'r@rdegges.com'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Your password reset email has been sent!' in
+ resp.data.decode('utf-8'))
+
+ def test_error_messages(self):
+ with self.app.test_client() as c:
+ # Ensure than en email wasn't sent if an email that doesn't exist
+ # in our database was entered.
+ resp = c.post('/forgot', data={'email': 'idonot@exist.com'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Invalid email address.' in resp.data.decode('utf-8'))
+
+ # Ensure that an email was sent if a valid email was entered.
+ resp = c.post('/forgot', data={'email': 'r@rdegges.com'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Your password reset email has been sent!' in
+ resp.data.decode('utf-8'))
+
+ def test_json_response_get(self):
+ # Specify expected response.
+ expected_response = [
+ {'label': 'Email',
+ 'name': 'email',
+ 'placeholder': 'Email',
+ 'required': True,
+ 'visible': True,
+ 'type': 'email'}]
+
+ # Ensure that the form fields specified in the expected response
+ # match those specified in the config file.
+ self.assertFormSettings(expected_response)
+
+ self.assertJsonResponse(
+ 'get', 'forgot', 200, json.dumps(expected_response))
+
+ def test_json_response_valid_form(self):
+ # Specify expected response.
+ expected_response = {
+ 'status': 200,
+ 'message': {"email": "r@rdegges.com"}
+ }
+
+ # Specify post data
+ json_data = json.dumps({'email': 'r@rdegges.com'})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+ self.assertJsonResponse(
+ 'post', 'forgot', 200, json.dumps(expected_response),
+ **request_kwargs)
+
+ def test_json_response_stormpath_error(self):
+ # Specify post data
+ json_data = json.dumps({'email': 'wrong@email.com'})
+
+ # Specify expected response
+ expected_response = {
+ 'message': 'Invalid email address.',
+ 'status': 400}
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+ self.assertJsonResponse(
+ 'post', 'forgot', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+ def test_json_response_form_error(self):
+ # Specify post data
+ json_data = json.dumps({'email': 'rdegges'})
+
+ # Specify expected response
+ expected_response = {
+ 'message': {"email": ["Email must be in valid format."]},
+ 'status': 400}
+
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ self.assertJsonResponse(
+ 'post', 'forgot', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+
+class TestChange(StormpathViewTestCase):
+ """Test our change view."""
+
+ def setUp(self):
+ super(TestChange, self).setUp()
+ # We need to set form fields to test out json stuff in the
+ # assertJsonResponse method.
+ self.form_fields = self.app.config['stormpath']['web'][
+ 'changePassword']['form']['fields']
+
+ # Generate a token
+ self.token = self.application.password_reset_tokens.create(
+ {'email': 'r@rdegges.com'}).token
+
+ # Specify url for json
+ self.reset_password_url = ''.join(['change?sptoken=', self.token])
+
+ def test_proper_template_rendering(self):
+ # Ensure that proper templates are rendered based on the request
+ # method.
+ with self.app.test_client() as c:
+ # Ensure request.GET will render the change_password.html template.
+ resp = c.get(self.reset_password_url)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Enter your new account password below.' in
+ resp.data.decode('utf-8'))
+
+ # Ensure that request.POST will render the
+ # change_password_success.html
+ resp = c.post(self.reset_password_url, data={
+ 'password': 'woot1DontLoveCookies!',
+ 'confirm_password': 'woot1DontLoveCookies!'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Your password has been changed, and you have been logged' +
+ ' into' in resp.data.decode('utf-8'))
+
+ def test_error_messages(self):
+ with self.app.test_client() as c:
+ # Ensure than en email wasn't changed if passwords don't satisfy
+ # minimum requirements (one number, one uppercase letter, minimum
+ # length).
+ resp = c.post(
+ self.reset_password_url,
+ data={
+ 'password': 'woot',
+ 'confirm_password': 'woot'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Account password minimum length not satisfied.' in
+ resp.data.decode('utf-8'))
+
+ def test_sptoken(self):
+ # Ensure that a proper token will render the change view
+ with self.app.test_client() as c:
+ # Ensure request.GET will render the change_password.html template.
+ resp = c.get(self.reset_password_url)
+ self.assertEqual(resp.status_code, 200)
+
+ # Ensure that a missing token will redirect to errorUri specified in
+ # the config.
+ with self.app.test_client() as c:
+ # Ensure request.GET will render the change_password.html template.
+ resp = c.get('/change')
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(
+ 'You should be redirected automatically to target URL: ' +
+ '' % self.app.config[
+ 'stormpath']['web']['changePassword']['errorUri'] +
+ '/forgot?status=invalid_sptoken.' in
+ resp.data.decode('utf-8'))
+
+ # If errorUri is empty, we will redirect back to index page.
+ self.app.config[
+ 'stormpath']['web']['changePassword']['errorUri'] = None
+ resp = c.get('/change')
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(
+ 'You should be redirected automatically to target URL: ' +
+ '' % '/' + '/.' in resp.data.decode('utf-8'))
+
+ def test_password_changed_and_logged_in(self):
+ with self.app.test_client() as c:
+ # Ensure that a user will be logged in after successful password
+ # reset.
+ self.assertFalse(current_user)
+ resp = c.post(
+ self.reset_password_url,
+ data={
+ 'password': 'woot1DontLoveCookies!',
+ 'confirm_password': 'woot1DontLoveCookies!'})
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(current_user.email, 'r@rdegges.com')
+
+ # Ensure that our password changed.
+ with self.app.app_context():
+ User.from_login('r@rdegges.com', 'woot1DontLoveCookies!')
+
+ def test_json_response_get(self):
+ # Specify expected response.
+ expected_response = [
+ {'label': 'Password',
+ 'name': 'password',
+ 'placeholder': 'Password',
+ 'required': True,
+ 'visible': True,
+ 'type': 'password'},
+ {'label': 'Confirm Password',
+ 'name': 'confirm_password',
+ 'placeholder': 'Confirm Password',
+ 'required': True,
+ 'visible': True,
+ 'type': 'password'}]
+
+ # Ensure that the form fields specified in the expected response
+ # match those specified in the config file.
+ self.assertFormSettings(expected_response)
+
+ self.assertJsonResponse(
+ 'get', self.reset_password_url, 200, json.dumps(expected_response))
+
+ def test_json_response_valid_form(self):
+ # Specify expected response.
+ expected_response = {'account': {
+ 'username': 'rdegges',
+ 'email': 'r@rdegges.com',
+ 'given_name': 'Randall',
+ 'middle_name': None,
+ 'surname': 'Degges',
+ 'full_name': 'Randall Degges',
+ 'status': 'ENABLED'}
+ }
+
+ # Specify post data
+ json_data = json.dumps({
+ 'password': 'woot1DontLoveCookies!',
+ 'confirm_password': 'woot1DontLoveCookies!'})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+ self.assertJsonResponse(
+ 'post', self.reset_password_url, 200,
+ json.dumps(expected_response), user_to_json=True,
+ **request_kwargs)
+
+ # Ensure that our password changed.
+ with self.app.app_context():
+ User.from_login('r@rdegges.com', 'woot1DontLoveCookies!')
+
+ def test_json_response_stormpath_error(self):
+ # Specify post data
+ json_data = json.dumps({
+ 'password': 'woot',
+ 'confirm_password': 'woot'})
+
+ # Specify expected response
+ expected_response = {
+ 'message': 'Account password minimum length not satisfied.',
+ 'status': 400}
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+ self.assertJsonResponse(
+ 'post', self.reset_password_url, 400,
+ json.dumps(expected_response), **request_kwargs)
+
+ def test_json_response_form_error(self):
+ # Specify post data
+ json_data = json.dumps({'password': 'woot1DontLoveCookies!'})
+
+ # Specify expected response
+ expected_response = {
+ 'message': {"confirm_password": ["Confirm Password is required."]},
+ 'status': 400}
+
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ self.assertJsonResponse(
+ 'post', self.reset_password_url, 400,
+ json.dumps(expected_response), **request_kwargs)
+
+
+class TestVerify(StormpathViewTestCase):
+ """ Test our verify view. """
+
+ def setUp(self):
+ super(TestVerify, self).setUp()
+ # We need to set form fields to test out json stuff in the
+ # assertJsonResponse method.
+ self.form_fields = self.app.config['stormpath']['web'][
+ 'verifyEmail']['form']['fields']
+
+ # Set our verify route (by default is missing)
+ self.app.add_url_rule(
+ self.app.config['stormpath']['web']['verifyEmail']['uri'],
+ 'stormpath.verify',
+ VerifyEmailView.as_view('verify'),
+ methods=['GET', 'POST'],
+ )
+
+ # Enable verification flow.
+ self.directory = self.client.directories.search(self.name).items[0]
+ account_policy = self.directory.account_creation_policy
+ account_policy.verification_email_status = 'ENABLED'
+ account_policy.save()
+
+ # Create a new account
+ with self.app.app_context():
+ user = User.create(
+ username='rdegges_verify',
+ given_name='Randall',
+ surname='Degges',
+ email='r@verify.com',
+ password='woot1LoveCookies!',
+ )
+ self.account = self.directory.accounts.search(user.email)[0]
+
+ # Specify url for json
+ self.verify_url = ''.join([
+ 'verify?sptoken=', self.get_verification_token()])
+
+ def get_verification_token(self):
+ # Retrieves an email verification token.
+ self.account.refresh()
+ if self.account.email_verification_token:
+ return self.account.email_verification_token.href.split('/')[-1]
+ return None
+
+ def test_verify_token_valid(self):
+ # Ensure that a valid token will activate a users account. By default,
+ # autologin is set to false, so the response should be a redirect
+ # to verifyEmail next uri.
+
+ # Setting redirect URL to something that is easy to check
+ stormpath_verify_redirect_url = '/redirect_for_verify'
+ self.app.config['stormpath']['web']['verifyEmail'][
+ 'nextUri'] = stormpath_verify_redirect_url
+
+ # Get activation token
+ sptoken = self.get_verification_token()
+
+ # Ensure that a proper verify token will activate a user's account.
+ with self.app.test_client() as c:
+ resp = c.get('/verify', query_string={'sptoken': sptoken})
+ self.assertEqual(resp.status_code, 302)
+
+ # Ensure proper redirection if autologin is disabled
+ location = resp.headers.get('location')
+ self.assertTrue(stormpath_verify_redirect_url in location)
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ def test_verify_token_invalid(self):
+ # If the verification token is invalid, render a template with a form
+ # used to resend an activation token.
+
+ # Set invalid activation token
+ sptoken = 'foobar'
+
+ # Ensure that a proper verify token will activate a user's account.
+ with self.app.test_client() as c:
+ resp = c.get('/verify', query_string={'sptoken': sptoken})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'This verification link is no longer valid.' in
+ resp.data.decode('utf-8'))
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'UNVERIFIED')
+
+ def test_verify_token_missing(self):
+ # If the verification token is missing, render a template with a form
+ # used to resend an activation token.
+
+ # Set missing activation token
+ sptoken = None
+
+ # Ensure that a proper verify token will activate a user's account.
+ with self.app.test_client() as c:
+ resp = c.get('/verify', query_string={'sptoken': sptoken})
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'This verification link is no longer valid.' in
+ resp.data.decode('utf-8'))
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'UNVERIFIED')
+
+ def test_resend_verification_token(self):
+ # Ensure that submitting an email form will generate a new activation
+ # token. Make sure that a redirect uri will have an unverified status.
+
+ # Get current activation token
+ sptoken = self.get_verification_token()
+
+ with self.app.test_client() as c:
+ # Activate the account
+ resp = c.get('/verify', query_string={'sptoken': sptoken})
+ self.assertEqual(resp.status_code, 302)
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ # Set the account back to 'UNVERIFIED'
+ self.account.status = 'UNVERIFIED'
+ self.account.save()
+ self.account.refresh()
+
+ # Submit a form that will resend a new activation token
+ resp = c.post('/verify', data={'email': 'r@verify.com'})
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(
+ 'You should be redirected automatically to target URL: ' +
+ '' % self.app.config[
+ 'stormpath']['web']['verifyEmail']['unverifiedUri'] +
+ '/login?status=unverified.' in
+ resp.data.decode('utf-8'))
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'UNVERIFIED')
+
+ # Make sure that a new token has replaced the old one
+ new_sptoken = self.get_verification_token()
+ self.assertNotEqual(sptoken, new_sptoken)
+
+ # Activate an account with a new token
+ resp = c.get('/verify', query_string={'sptoken': new_sptoken})
+ self.assertEqual(resp.status_code, 302)
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ def test_resend_verification_token_unassociated_email(self):
+ # Ensure that submitting an unassociated email form will not
+ # generate a new activation token. Make sure that a redirect uri will
+ # still have an unverified status.
+
+ # Get current activation token
+ sptoken = self.get_verification_token()
+
+ with self.app.test_client() as c:
+ # Activate the account
+ resp = c.get('/verify', query_string={'sptoken': sptoken})
+ self.assertEqual(resp.status_code, 302)
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ # Set the account back to 'UNVERIFIED'
+ self.account.status = 'UNVERIFIED'
+ self.account.save()
+ self.account.refresh()
+
+ # Submit a form with an unassociated email
+ resp = c.post('/verify', data={'email': 'doesnot@exist.com'})
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(
+ 'You should be redirected automatically to target URL: ' +
+ '' % self.app.config[
+ 'stormpath']['web']['verifyEmail']['unverifiedUri'] +
+ '/login?status=unverified.' in
+ resp.data.decode('utf-8'))
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'UNVERIFIED')
+
+ # Make sure that a new token was not generated
+ new_sptoken = self.get_verification_token()
+ self.assertIsNone(new_sptoken)
+
+ def test_autologin_true(self):
+ # Ensure that the enabled autologin will log a user in and redirect
+ # him user to the uri specified in the login > nextUri.
+
+ # Set autologin to true
+ self.app.config['stormpath']['web']['register']['autoLogin'] = True
+
+ # Setting redirect URL to something that is easy to check
+ stormpath_login_redirect_url = '/redirect_for_login'
+ self.app.config['stormpath']['web']['login'][
+ 'nextUri'] = stormpath_login_redirect_url
+
+ # Get activation token
+ sptoken = self.get_verification_token()
+
+ # Ensure that a proper verify token will activate a user's account.
+ with self.app.test_client() as c:
+ resp = c.get('/verify', query_string={'sptoken': sptoken})
+ self.assertEqual(resp.status_code, 302)
+ location = resp.headers.get('location')
+ self.assertTrue(stormpath_login_redirect_url in location)
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ def test_response_form_error_missing(self):
+ # Ensure that a missing email will render a proper error.
+ # Get current activation token
+ with self.app.test_client() as c:
+ resp = c.post('/verify', data={}, follow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue('Email is required.' in resp.data.decode('utf-8'))
+
+ def test_response_form_error_invalid(self):
+ # Ensure that an invalid email will render a proper error.
+ with self.app.test_client() as c:
+ resp = c.post(
+ '/verify', data={'email': 'foobar'}, follow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(
+ 'Email must be in valid format.' in resp.data.decode('utf-8'))
+
+ def test_verify_token_valid_json(self):
+ # Ensure that a valid token will activate a users account. By default,
+ # autologin is set to false, so the response should be an empty body
+ # with 200 status code.
+
+ # Specify expected response.
+ expected_response = {}
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', self.verify_url, 200, json.dumps(expected_response))
+
+ def test_verify_token_invalid_json(self):
+ # If the verification token is invalid, return an error from the
+ # REST API.
+
+ # Set an invalid token
+ self.verify_url = 'verify?sptoken=foobar'
+
+ # Specify expected response.
+ expected_response = {
+ 'status': 404,
+ 'message': 'The requested resource does not exist.'
+ }
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', self.verify_url, 404, json.dumps(expected_response))
+
+ def test_verify_token_missing_json(self):
+ # If the verification token is missing, respond with our custom
+ # message and a 400 status code.
+
+ # Specify expected response.
+ expected_response = {
+ 'status': 400,
+ 'message': 'sptoken parameter not provided.'
+ }
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', 'verify', 400, json.dumps(expected_response))
+
+ def test_resend_verification_token_json(self):
+ # Ensure that submitting an email form will generate a new activation
+ # token. Response should be an empty body with a 200 status code.
+
+ # First we will activate the account
+
+ # Specify expected response.
+ expected_response = {}
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', self.verify_url, 200, json.dumps(expected_response))
+
+ # Ensure that the account is enabled
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ # Set the account back to 'UNVERIFIED'
+ self.account.status = 'UNVERIFIED'
+ self.account.save()
+ self.account.refresh()
+
+ # Submit a form that will resend a new activation token
+
+ # Specify expected response.
+ expected_response = {}
+
+ # Post data
+ json_data = json.dumps({'email': 'r@verify.com'})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'
+ }
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'post', 'verify', 200, json.dumps(expected_response),
+ **request_kwargs)
+
+ # Ensure that the account is still unverified.
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'UNVERIFIED')
+
+ # Retrieve a newly generated token
+ self.new_verify_url = ''.join([
+ 'verify?sptoken=', self.get_verification_token()])
+ self.assertNotEqual(self.verify_url, self.new_verify_url)
+
+ # Activate an account with a new token
+
+ # Specify expected response.
+ expected_response = {}
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', self.new_verify_url, 200, json.dumps(expected_response))
+
+ # Ensure that the account is now enabled
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ def test_resend_verification_token_unassociated_email_json(self):
+ # Ensure that submitting an unassociated email form will not
+ # generate a new activation token. Make sure that response will still
+ # be an empty body with a 200 status code.
+
+ # First we will activate the account
+
+ # Specify expected response.
+ expected_response = {}
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', self.verify_url, 200, json.dumps(expected_response))
+
+ # Ensure that the account is enabled
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'ENABLED')
+
+ # Set the account back to 'UNVERIFIED'
+ self.account.status = 'UNVERIFIED'
+ self.account.save()
+ self.account.refresh()
+
+ # Submit a form that will resend a new activation token
+
+ # Specify expected response.
+ expected_response = {}
+
+ # Post data
+ json_data = json.dumps({'email': 'doesnot@exist.com'})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'
+ }
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'post', 'verify', 200, json.dumps(expected_response),
+ **request_kwargs)
+
+ # Ensure that the account is still unverified.
+ self.account.refresh()
+ self.assertEqual(self.account.status, 'UNVERIFIED')
+
+ # Make sure that a new token was not generated
+ new_sptoken = self.get_verification_token()
+ self.assertIsNone(new_sptoken)
+
+ def test_autologin_true_json(self):
+ # Ensure that the enabled autologin will log a user in and return an
+ # account json response.
+
+ # Set autologin to true
+ self.app.config['stormpath']['web']['register']['autoLogin'] = True
+
+ # Specify expected response.
+ expected_response = {'account': {
+ 'username': 'rdegges_verify',
+ 'email': 'r@verify.com',
+ 'given_name': 'Randall',
+ 'middle_name': None,
+ 'surname': 'Degges',
+ 'full_name': 'Randall Degges',
+ 'status': 'ENABLED'}
+ }
+
+ # Check the json response.
+ self.assertJsonResponse(
+ 'get', self.verify_url, 200, json.dumps(expected_response),
+ user_to_json=True)
+
+ def test_response_form_error_missing_json(self):
+ # Ensure that a missing email will render a proper error.
+
+ # Specify expected response
+ expected_response = {
+ 'message': {"email": ["Email is required."]},
+ 'status': 400}
+
+ # Post data
+ json_data = json.dumps({})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'
+ }
+
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ # Check the json response-
+ self.assertJsonResponse(
+ 'post', 'verify', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+ def test_response_form_error_invalid_json(self):
+ # Ensure that an invalid email will render a proper error.
+
+ # Specify expected response
+ expected_response = {
+ 'message': {"email": ["Email must be in valid format."]},
+ 'status': 400}
+
+ # Post data
+ json_data = json.dumps({'email': 'foobar'})
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'
+ }
+
+ request_kwargs = {
+ 'data': json_data,
+ 'content_type': 'application/json'}
+
+ # Check the json response-
+ self.assertJsonResponse(
+ 'post', 'verify', 400, json.dumps(expected_response),
+ **request_kwargs)
+
+
+class TestMe(StormpathViewTestCase):
+ """Test our me view."""
+ def test_json_response(self):
+ with self.app.test_client() as c:
+ email = 'r@rdegges.com'
+ password = 'woot1LoveCookies!'
+
+ # Authenticate our user.
+ resp = c.post('/login', data={
+ 'login': email,
+ 'password': password,
+ })
+ resp = c.get('/me')
+ account = User.from_login(email, password)
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.data.decode(), account.to_json())
+
+ def test_redirect_to_login(self):
+
+ with self.app.test_client() as c:
+ # Ensure that the user will be redirected to login if he/she is not
+ # logged it.
+ resp = c.get('/me')
+
+ redirect_url = url_for('stormpath.login', next='/me')
+ self.assertEqual(resp.status_code, 302)
+ location = resp.headers.get('location')
+ self.assertTrue(redirect_url in location)
+
+ def test_added_expansion(self):
+ # NOTE: We're not testing expansion in models since we need to call
+ # the expanded me view.
+
+ # Enable expanded info on our me view
+ me_expand = self.app.config['stormpath']['web']['me']['expand']
+ for key in me_expand.keys():
+ me_expand[key] = True
+
+ with self.app.test_client() as c:
+ email = 'r@rdegges.com'
+ password = 'woot1LoveCookies!'
+
+ # Authenticate our user.
+ resp = c.post('/login', data={
+ 'login': email,
+ 'password': password,
+ })
+ resp = c.get('/me')
+ self.assertEqual(resp.status_code, 200)
+
+ # Get unexpanded account object
+ account = User.from_login(email, password)
+
+ json_data = {'account': {
+ 'href': account.href,
+ 'modified_at': account.modified_at.isoformat(),
+ 'created_at': account.created_at.isoformat(),
+ 'email': 'r@rdegges.com',
+ 'full_name': 'Randall Degges',
+ 'given_name': 'Randall',
+ 'middle_name': None,
+ 'status': 'ENABLED',
+ 'surname': 'Degges',
+ 'username': 'rdegges'
+ }}
+
+ # Ensure that the missing expanded info won't break
+ # User.to_json() flow.
+ self.assertEqual(json.loads(account.to_json()), json_data)
+
+ json_data['account'].update({
+ 'applications': {},
+ 'customData': {},
+ 'directory': {},
+ 'tenant': {},
+ 'providerData': {},
+ 'groupMemberships': {},
+ 'groups': {},
+ 'apiKeys': {}
+ })
+
+ # Ensure that expanded me response will return proper data.
+ self.assertEqual(json.loads(resp.data.decode()), json_data)
+
+
+class TestFacebookLogin(StormpathViewTestCase):
+ """ Test our Facebook login view. """
+
+ @patch('flask_stormpath.views.get_user_from_cookie')
+ def test_access_token(self, access_token_mock):
+ # Ensure that proper access code fetching will continue processing
+ # the view.
+ access_token_mock.return_value = {
+ 'access_token': 'mocked access token'}
+ with self.app.test_request_context():
+ FacebookLoginView()
+
+ # Ensure that invalid access code fetching will return a 400 BadRequest
+ # response.
+ access_token_mock.return_value = None
+ with self.app.test_request_context():
+ with self.assertRaises(BadRequest) as error:
+ FacebookLoginView()
+ self.assertEqual(error.exception.name, 'Bad Request')
+ self.assertEqual(error.exception.code, 400)
+
+ @patch('flask_stormpath.views.get_user_from_cookie')
+ @patch('flask_stormpath.views.SocialView.get_account')
+ def test_user_logged_in_and_redirect(self, user_mock, access_token_mock):
+ # Access token is retrieved on the front end of our applications, so
+ # we have to mock it.
+ access_token_mock.return_value = {
+ 'access_token': 'mocked access token'}
+ user_mock.return_value = self.user
+
+ # Setting redirect URL to something that is easy to check
+ stormpath_login_redirect_url = '/redirect_for_login'
+ self.app.config['stormpath']['web']['login'][
+ 'nextUri'] = stormpath_login_redirect_url
+
+ # Ensure that the correct access token will log our user in and
+ # redirect him to the index page.
+ with self.app.test_client() as c:
+ self.assertFalse(current_user)
+ # Log this user in.
+ resp = c.get('/facebook')
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(current_user, self.user)
+ location = resp.headers.get('location')
+ self.assertTrue(stormpath_login_redirect_url in location)
+
+ @patch('flask_stormpath.views.get_user_from_cookie')
+ def test_error_retrieving_user(self, access_token_mock):
+ # Access token is retrieved on the front end of our applications, so
+ # we have to mock it.
+ access_token_mock.return_value = {
+ 'access_token': 'mocked access token'}
+
+ # Ensure that the user will be redirected back to the login page with
+ # the proper error message rendered in case we fail to fetch the user
+ # account.
+ with self.app.test_client() as c:
+ # First we'll check the error message.
+ self.assertFalse(current_user)
+ # Try to log a user in.
+ resp = c.get('/facebook', follow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(current_user.is_anonymous)
+
+ self.assertTrue(
+ 'Oops! We encountered an unexpected error. Please contact ' +
+ 'support and explain what you were doing at the time this ' +
+ 'error occurred.' in
+ resp.data.decode('utf-8'))
+
+ # Then we'll make the same request, but this time checking the
+ # redirect status code and location.
+
+ # Setting redirect URL to something that is easy to check
+ facebook_login_redirect_url = '/redirect_for_facebook_login'
+ self.app.config['stormpath']['web']['login'][
+ 'uri'] = facebook_login_redirect_url
+
+ # Try to log a user in.
+ resp = c.get('/facebook')
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(current_user.is_anonymous)
+ location = resp.headers.get('location')
+ self.assertTrue(facebook_login_redirect_url in location)
+
+
+class TestGoogleLogin(StormpathViewTestCase):
+ """ Test our Google login view. """
+
+ def test_access_token(self):
+ # Ensure that proper access code fetching will continue processing
+ # the view.
+ with self.app.test_request_context() as req:
+ req.request.args = {'code': 'mocked access token'}
+ GoogleLoginView()
+
+ # Ensure that invalid access code fetching will return a 400 BadRequest
+ # response.
+ with self.app.test_request_context() as req:
+ req.request.args = {}
+ with self.assertRaises(BadRequest) as error:
+ GoogleLoginView()
+ self.assertEqual(error.exception.name, 'Bad Request')
+ self.assertEqual(error.exception.code, 400)
+
+ @patch('flask_stormpath.views.SocialView.get_account')
+ def test_user_logged_in_and_redirect(self, user_mock):
+ # Access token is retrieved on the front end of our applications, so
+ # we have to mock it.
+ user_mock.return_value = self.user
+
+ # Setting redirect URL to something that is easy to check
+ stormpath_login_redirect_url = '/redirect_for_login'
+ self.app.config['stormpath']['web']['login'][
+ 'nextUri'] = stormpath_login_redirect_url
+
+ # Ensure that the correct access token will log our user in and
+ # redirect him to the index page.
+ with self.app.test_client() as c:
+ self.assertFalse(current_user)
+ # Log this user in.
+ resp = c.get(
+ '/google', query_string={'code': 'mocked access token'})
+ self.assertEqual(resp.status_code, 302)
+ self.assertEqual(current_user, self.user)
+ location = resp.headers.get('location')
+ self.assertTrue(stormpath_login_redirect_url in location)
+
+ def test_error_retrieving_user(self):
+ # Ensure that the user will be redirected back to the login page with
+ # the proper error message rendered in case we fail to fetch the user
+ # account.
+ with self.app.test_client() as c:
+ # First we'll check the error message.
+ self.assertFalse(current_user)
+ # Try to log a user in.
+ resp = c.get(
+ '/google', query_string={'code': 'mocked access token'},
+ follow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+ self.assertTrue(current_user.is_anonymous)
+
+ self.assertTrue(
+ 'Oops! We encountered an unexpected error. Please contact ' +
+ 'support and explain what you were doing at the time this ' +
+ 'error occurred.' in
+ resp.data.decode('utf-8'))
+
+ # Then we'll make the same request, but this time checking the
+ # redirect status code and location.
+
+ # Setting redirect URL to something that is easy to check
+ facebook_login_redirect_url = '/redirect_for_facebook_login'
+ self.app.config['stormpath']['web']['login'][
+ 'uri'] = facebook_login_redirect_url
+
+ # Try to log a user in.
+ resp = c.get(
+ '/google', query_string={'code': 'mocked access token'})
+ self.assertEqual(resp.status_code, 302)
+ self.assertTrue(current_user.is_anonymous)
+ location = resp.headers.get('location')
+ self.assertTrue(facebook_login_redirect_url in location)