Skip to content

Commit

Permalink
Started setting up the SQLAlchemy tests and mixins
Browse files Browse the repository at this point in the history
  • Loading branch information
sontek committed Apr 15, 2014
1 parent 4b21fa9 commit a730a72
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 24 deletions.
13 changes: 13 additions & 0 deletions README.rst
Expand Up @@ -43,7 +43,20 @@ connecting to a data store (postgres, zodb, mongodb) and returning the result se

Mapping functions from database rows to model classes should be done here.

Flows
------------------------------------
Flows represent a type of authentication that will include a specific set of services.

*local*
The local flow represents that standard form workflow where you present a username/password
form that authenticates the user from a database.

The local flow includes things like registration and e-mail verification.

*ldap*
The ldap flow will authenticate against an LDAP server, no registration or activation is required.


Example Configuration
=====================

Expand Down
1 change: 1 addition & 0 deletions horus/backends/sqla/__init__.py
Expand Up @@ -4,3 +4,4 @@ def includeme(config):

class SQLABackend(object):
pass

63 changes: 51 additions & 12 deletions horus/backends/sqla/mixins.py
@@ -1,27 +1,66 @@
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from horus.utils.text import generate_random_string
from horus.interfaces import IUser
from zope.interface import implements
from cryptacular import bcrypt
import sqlalchemy as sa

crypt = bcrypt.BCRYPTPasswordManager()


class BaseMixin(object):
pass
@declared_attr
def id(self):
return sa.Column(sa.Integer, autoincrement=True, primary_key=True)


class UserMixin(BaseMixin):
implements(IUser)

__tablename__ = 'user'

class UserMixin(object):
@declared_attr
def principals(self):
pass
def username(self):
""" Unique username """
return sa.Column(sa.Unicode(30), nullable=False, unique=True)

@declared_attr
def date_registered(self):
""" Date of user's registration """
return sa.Column(
sa.TIMESTAMP(timezone=False),
default=sa.sql.func.now(),
server_default=sa.func.now(),
nullable=False,
)

@declared_attr
def salt(self):
""" Password salt for user """
return sa.Column(sa.Unicode(256), nullable=True)

class UserGroupLinkMixin(object):
pass
@declared_attr
def _password(self):
""" Password hash for user object """
return sa.Column('password', sa.Unicode(256), nullable=True)

@hybrid_property
def password(self):
return self._password

class GroupMixin(object):
pass
@password.setter
def password(self, value):
self._set_password(value)

def _get_password(self):
return self._password

class UserRoleLinkMixin(object):
pass
def _set_password(self, raw_password):
self._password = self._hash_password(raw_password)

def _hash_password(self, password):
if not self.salt:
self.salt = generate_random_string(24)

class RoleMixin(object):
pass
return unicode(crypt.encode(password + self.salt))
2 changes: 0 additions & 2 deletions horus/flows/ldap/__init__.py

This file was deleted.

7 changes: 0 additions & 7 deletions horus/flows/local/services.py
Expand Up @@ -28,13 +28,6 @@ def login(self, login, password):

return user

def logout(self, login):
"""
There is nothing to do for logout on the service side other than log
that the user did leave.
"""
pass


class RegisterService(object):
def __init__(self, backend):
Expand Down
2 changes: 0 additions & 2 deletions horus/flows/velruse/__init__.py

This file was deleted.

10 changes: 10 additions & 0 deletions horus/interfaces.py
Expand Up @@ -7,6 +7,8 @@
class IUser(Interface):
login = Attribute('The value used to do authentication')
password = Attribute('The password for verifying the user')
date_registered = Attribute('')
salt = Attribute('')


class ILoginService(Interface):
Expand All @@ -23,3 +25,11 @@ class IRegistrationService(Interface):

class IMailer(Interface):
pass


class IDataBackend(Interface):
def get_user(self, login):
pass

def create_user(self, login, password=None, email=None):
pass
File renamed without changes.
14 changes: 14 additions & 0 deletions horus/utils/text.py
@@ -0,0 +1,14 @@
import hashlib
import random
import string
from pyramid.compat import text_type


def generate_random_string(length=12):
"""Generate a generic hash key for the user to use."""
m = hashlib.sha256()
word = ''
for i in range(length):
word += random.choice(string.ascii_letters)
m.update(word.encode('ascii'))
return text_type(m.hexdigest()[:length])
5 changes: 4 additions & 1 deletion setup.py
Expand Up @@ -32,16 +32,18 @@
CHANGES = ''

requires = [
'cryptacular',
'pyramid',
'zope.interface',
]

testing_extras = ['pytest', 'pytest-cov', 'coverage', 'mock']
docs_extras = ['Sphinx']
sqla_extras = ['sqlalchemy']

setupkw = dict(
name='horus',
version='0.0.1',
version='2.0.0',
description='Pyramid authentication and registration system',
long_description=README + '\n\n' + CHANGES,
classifiers=[
Expand All @@ -67,6 +69,7 @@
extras_require={
'testing': testing_extras,
'docs': docs_extras,
'sqla': sqla_extras,
},
)

Expand Down
Empty file added tests/backends/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions tests/backends/test_sql.py
@@ -0,0 +1,20 @@
import pytest

from sqlalchemy.ext.declarative import declarative_base
from horus.backends.sqla.mixins import UserMixin

BaseModel = declarative_base()


class UserModel(UserMixin, BaseModel):
pass


@pytest.mark.sqla
def test_user_class(db_session):

user = UserModel(username='sontek')
db_session.add(user)
db_session.flush()
results = db_session.query(UserModel).all()
assert results[0] == user
90 changes: 90 additions & 0 deletions tests/conftest.py
@@ -0,0 +1,90 @@
import pytest
import os
import mock

from pyramid.paster import get_appsettings
from pyramid import testing
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker

here = os.path.dirname(__file__)
config_file = os.path.join(here, 'test.ini')
settings = get_appsettings(config_file)
_DBSession = None
_DBTrans = None
current_session = None


class NotAllowed(Exception):
message = 'You must mark your test as sqla'


def pytest_addoption(parser):
parser.addoption(
"--slow",
action="store_true",
help="run slow tests"
)

parser.addoption(
"--sqla",
action="store_true",
help="run sqla tests"
)


def setup_sqlalchemy():
global DBTrans
global _DBSession

testing.setUp(settings=settings)

engine = engine_from_config(settings, prefix='backend.sqla.')
connection = engine.connect()
DBTrans = connection.begin()
_DBSession = sessionmaker(bind=connection)()


def pytest_runtest_setup(item):
global current_session
current_session = None

if 'slow' in item.keywords and not item.config.getoption("--slow"):
pytest.skip("need --slow option to run")
return

if 'sqla' in item.keywords and not item.config.getoption("--sqla"):
pytest.skip("need --sqla option to run")
return
else:
current_session = _DBSession


@pytest.fixture()
def db_session(request):
"""
This will hande the db session to tests that declare session in
their arguments
"""
if 'sqla' in request.keywords:
return current_session
else:
raise NotAllowed('This test is not a sqla test')


def pytest_sessionstart():
from py.test import config

is_sqla = config.getoption('--sqla')

# Only run database setup on master (in case of xdist/multiproc mode)
if not hasattr(config, 'slaveinput') and is_sqla:
from pyramid.config import Configurator
from .backends.test_sql import BaseModel
setup_sqlalchemy()
engine = _DBSession.bind.engine
print('Creating the tables on the test database %s' % engine)
config = Configurator(settings=settings)
BaseModel.metadata.bind = _DBSession.bind
BaseModel.metadata.drop_all(engine)
BaseModel.metadata.create_all(engine)
3 changes: 3 additions & 0 deletions tests/test.ini
@@ -0,0 +1,3 @@
[app:main]
use = egg:horus_demo
backend.sqla.url = sqlite:///:memory:

0 comments on commit a730a72

Please sign in to comment.