Browse Source

add first implementation of unit / functionnal tests

Damien ACCORSI 10 years ago
parent
commit
0d2a4a8741

+ 1 - 1
tracim/test.ini View File

@@ -16,7 +16,7 @@ host = 127.0.0.1
16 16
 port = 8080
17 17
 
18 18
 [app:main]
19
-sqlalchemy.url = sqlite:///:memory:
19
+sqlalchemy.url = postgresql://postgres:dummy@127.0.0.1:5432/tracim_test?client_encoding=utf8
20 20
 use = config:development.ini
21 21
 
22 22
 [app:main_without_authn]

+ 4 - 4
tracim/tracim/config/app_cfg.py View File

@@ -205,11 +205,11 @@ class CFG(object):
205 205
         self.WEBSITE_HOME_TITLE_COLOR = tg.config.get('website.title.color', '#555')
206 206
         self.WEBSITE_HOME_IMAGE_URL = tg.lurl('/assets/img/home_illustration.jpg')
207 207
         self.WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl('/assets/img/bg.jpg')
208
-        self.WEBSITE_BASE_URL = tg.config.get('website.base_url')
208
+        self.WEBSITE_BASE_URL = tg.config.get('website.base_url', '')
209 209
 
210
-        self.WEBSITE_HOME_TAG_LINE = tg.config.get('website.home.tag_line')
211
-        self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle')
212
-        self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get('website.home.below_login_form')
210
+        self.WEBSITE_HOME_TAG_LINE = tg.config.get('website.home.tag_line', '')
211
+        self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle', '')
212
+        self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get('website.home.below_login_form', '')
213 213
 
214 214
 
215 215
         self.EMAIL_NOTIFICATION_FROM = tg.config.get('email.notification.from')

+ 1 - 1
tracim/tracim/controllers/root.py View File

@@ -90,7 +90,7 @@ class RootController(StandardController):
90 90
 
91 91
 
92 92
     @expose()
93
-    def post_login(self, came_from=lurl('/')):
93
+    def post_login(self, came_from=lurl('/dashboard')):
94 94
         """
95 95
         Redirect the user to the initially requested page on successful
96 96
         authentication or redirect her back to the login page if login failed.

+ 18 - 9
tracim/tracim/lib/content.py View File

@@ -5,8 +5,9 @@ __author__ = 'damien'
5 5
 import tg
6 6
 
7 7
 from sqlalchemy.orm.attributes import get_history
8
+from sqlalchemy import not_
8 9
 from tracim.lib import cmp_to_key
9
-from tracim.lib.notifications import Notifier
10
+from tracim.lib.notifications import NotifierFactory
10 11
 from tracim.model import DBSession
11 12
 from tracim.model.auth import User
12 13
 from tracim.model.data import ContentStatus, ContentRevisionRO, ActionDescription
@@ -73,6 +74,7 @@ class ContentApi(object):
73 74
 
74 75
     def _base_query(self, workspace: Workspace=None):
75 76
         result = DBSession.query(Content)
77
+
76 78
         if workspace:
77 79
             result = result.filter(Content.workspace_id==workspace.workspace_id)
78 80
 
@@ -130,6 +132,7 @@ class ContentApi(object):
130 132
         content.revision_type = ActionDescription.CREATION
131 133
 
132 134
         if do_save:
135
+            DBSession.add(content)
133 136
             self.save(content, ActionDescription.CREATION)
134 137
         return content
135 138
 
@@ -190,16 +193,18 @@ class ContentApi(object):
190 193
 
191 194
     def get_all(self, parent_id: int, content_type: str, workspace: Workspace=None) -> Content:
192 195
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
193
-        assert content_type is not None # DYN_REMOVE
196
+        assert content_type is not None# DYN_REMOVE
194 197
         assert isinstance(content_type, str) # DYN_REMOVE
195 198
 
196
-        if not parent_id:
197
-            return
199
+        resultset = self._base_query(workspace)
198 200
 
199
-        return self._base_query(workspace).\
200
-            filter(Content.parent_id==parent_id).\
201
-            filter(Content.type==content_type).\
202
-            all()
201
+        if content_type!=ContentType.Any:
202
+            resultset.filter(Content.type==content_type)
203
+
204
+        if parent_id:
205
+            resultset.filter(Content.parent_id==parent_id)
206
+
207
+        return resultset.all()
203 208
 
204 209
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
205 210
         """
@@ -269,6 +274,9 @@ class ContentApi(object):
269 274
         content.is_deleted = False
270 275
         content.revision_type = ActionDescription.UNDELETION
271 276
 
277
+    def flush(self):
278
+        DBSession.flush()
279
+
272 280
     def save(self, content: Content, action_description: str=None, do_flush=True, do_notify=True):
273 281
         """
274 282
         Save an object, flush the session and set the revision_type property
@@ -290,4 +298,5 @@ class ContentApi(object):
290 298
             DBSession.flush()
291 299
 
292 300
         if do_notify:
293
-            Notifier(self._user).notify_content_update(content)
301
+            NotifierFactory.create(self._user).notify_content_update(content)
302
+

+ 33 - 3
tracim/tracim/lib/notifications.py View File

@@ -27,14 +27,44 @@ from tracim.model.auth import User
27 27
 
28 28
 from tgext.asyncjob import asyncjob_perform
29 29
 
30
-class Notifier(object):
30
+class INotifier(object):
31
+    """
32
+    Interface for Notifier instances
33
+    """
34
+    def __init__(self, current_user: User=None):
35
+        raise NotImplementedError
36
+
37
+    def notify_content_update(self, content: Content):
38
+        raise NotImplementedError
39
+
40
+
41
+class NotifierFactory(object):
42
+
43
+    @classmethod
44
+    def create(cls, current_user: User=None) -> INotifier:
45
+        cfg = CFG.get_instance()
46
+        if cfg.EMAIL_NOTIFICATION_ACTIVATED:
47
+            return DummyNotifier(current_user)
31 48
 
49
+        return RealNotifier(current_user)
50
+
51
+
52
+class DummyNotifier(INotifier):
32 53
     def __init__(self, current_user: User=None):
33
-        """
54
+        logger.info(self, 'Instantiating Dummy Notifier')
55
+
56
+    def notify_content_update(self, content: Content):
57
+        logger.info(self, 'Fake notifier, do not send email-notification for update of content {} by user {}'.format(content.content_id, self._user.user_id))
34 58
 
59
+
60
+class RealNotifier(object):
61
+
62
+    def __init__(self, current_user: User=None):
63
+        """
35 64
         :param current_user: the user that has triggered the notification
36 65
         :return:
37 66
         """
67
+        logger.info(self, 'Instantiating Real Notifier')
38 68
         cfg = CFG.get_instance()
39 69
 
40 70
         self._user = current_user
@@ -44,7 +74,7 @@ class Notifier(object):
44 74
                                        cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
45 75
 
46 76
     def notify_content_update(self, content: Content):
47
-        logger.info(self, 'About to email-notify update of content {} by user {}'.format(content.content_id, self._user.user_id))
77
+        logger.info(self, 'About to email-notify update of content {} by user {}'.format(content.content_id, self._user.user_id if self._user else 0)) # 0 means "no user"
48 78
 
49 79
         global_config = CFG.get_instance()
50 80
 

+ 17 - 7
tracim/tracim/lib/user.py View File

@@ -33,7 +33,7 @@ class UserApi(object):
33 33
         if do_save:
34 34
             self.save(user)
35 35
 
36
-        if user.user_id==self._user.user_id:
36
+        if self._user and user.user_id==self._user.user_id:
37 37
             # this is required for the session to keep on being up-to-date
38 38
             tg.request.identity['repoze.who.userid'] = email
39 39
 
@@ -58,10 +58,20 @@ class UserApi(object):
58 58
 
59 59
 class UserStaticApi(object):
60 60
 
61
-  @classmethod
62
-  def get_current_user(cls) -> User:
63
-    identity = tg.request.identity
61
+    @classmethod
62
+    def get_current_user(cls) -> User:
63
+        identity = tg.request.identity
64 64
 
65
-    if tg.request.identity:
66
-        return pbma.User.by_email_address(tg.request.identity['repoze.who.userid'])
67
-    return None
65
+        if tg.request.identity:
66
+            return cls._get_user(tg.request.identity['repoze.who.userid'])
67
+
68
+        return None
69
+
70
+    @classmethod
71
+    def _get_user(cls, email) -> User:
72
+        """
73
+        Do not use directly in your code.
74
+        :param email:
75
+        :return:
76
+        """
77
+        return pbma.User.by_email_address(email)

+ 12 - 5
tracim/tracim/model/auth.py View File

@@ -16,8 +16,15 @@ from tg.i18n import lazy_ugettext as l_
16 16
 
17 17
 __all__ = ['User', 'Group', 'Permission']
18 18
 
19
-from sqlalchemy import Table, ForeignKey, Column
20
-from sqlalchemy.types import Unicode, Integer, DateTime, Boolean
19
+from sqlalchemy import Column
20
+from sqlalchemy import ForeignKey
21
+from sqlalchemy import Sequence
22
+from sqlalchemy import Table
23
+
24
+from sqlalchemy.types import Unicode
25
+from sqlalchemy.types import Integer
26
+from sqlalchemy.types import DateTime
27
+from sqlalchemy.types import Boolean
21 28
 from sqlalchemy.orm import relation, relationship, synonym
22 29
 
23 30
 from tracim.model import DeclarativeBase, metadata, DBSession
@@ -54,7 +61,7 @@ class Group(DeclarativeBase):
54 61
 
55 62
     __tablename__ = 'groups'
56 63
 
57
-    group_id = Column(Integer, autoincrement=True, primary_key=True)
64
+    group_id = Column(Integer, Sequence('seq__groups__group_id'), autoincrement=True, primary_key=True)
58 65
     group_name = Column(Unicode(16), unique=True, nullable=False)
59 66
     display_name = Column(Unicode(255))
60 67
     created = Column(DateTime, default=datetime.now)
@@ -106,7 +113,7 @@ class User(DeclarativeBase):
106 113
     """
107 114
     __tablename__ = 'users'
108 115
 
109
-    user_id = Column(Integer, autoincrement=True, primary_key=True)
116
+    user_id = Column(Integer, Sequence('seq__users__user_id'), autoincrement=True, primary_key=True)
110 117
     email = Column(Unicode(255), unique=True, nullable=False)
111 118
     display_name = Column(Unicode(255))
112 119
     _password = Column('password', Unicode(128))
@@ -214,7 +221,7 @@ class Permission(DeclarativeBase):
214 221
     __tablename__ = 'permissions'
215 222
 
216 223
 
217
-    permission_id = Column(Integer, autoincrement=True, primary_key=True)
224
+    permission_id = Column(Integer, Sequence('seq__permissions__permission_id'), autoincrement=True, primary_key=True)
218 225
     permission_name = Column(Unicode(63), unique=True, nullable=False)
219 226
     description = Column(Unicode(255))
220 227
 

+ 4 - 3
tracim/tracim/model/data.py View File

@@ -6,6 +6,7 @@ import json
6 6
 
7 7
 from sqlalchemy import Column
8 8
 from sqlalchemy import ForeignKey
9
+from sqlalchemy import Sequence
9 10
 
10 11
 from sqlalchemy.ext.hybrid import hybrid_property
11 12
 
@@ -40,7 +41,7 @@ class Workspace(DeclarativeBase):
40 41
 
41 42
     __tablename__ = 'workspaces'
42 43
 
43
-    workspace_id = Column(Integer, autoincrement=True, primary_key=True)
44
+    workspace_id = Column(Integer, Sequence('seq__workspaces__workspace_id'), autoincrement=True, primary_key=True)
44 45
 
45 46
     label   = Column(Unicode(1024), unique=False, nullable=False, default='')
46 47
     description = Column(Text(), unique=False, nullable=False, default='')
@@ -335,7 +336,7 @@ class Content(DeclarativeBase):
335 336
 
336 337
     revision_to_serialize = -0  # This flag allow to serialize a given revision if required by the user
337 338
 
338
-    content_id = Column(Integer, autoincrement=True, primary_key=True)
339
+    content_id = Column(Integer, Sequence('seq__contents__content_id'), autoincrement=True, primary_key=True)
339 340
     parent_id = Column(Integer, ForeignKey('contents.content_id'), nullable=True, default=None)
340 341
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True, default=None)
341 342
 
@@ -475,7 +476,7 @@ class ContentRevisionRO(DeclarativeBase):
475 476
 
476 477
     __tablename__ = 'content_revisions'
477 478
 
478
-    revision_id = Column(Integer, primary_key=True)
479
+    revision_id = Column(Integer, Sequence('seq__content_revisions__revision_id'), primary_key=True)
479 480
     content_id = Column(Integer, ForeignKey('contents.content_id'))
480 481
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
481 482
     label = Column(Unicode(1024), unique=False, nullable=False)

+ 3 - 1
tracim/tracim/templates/index.mak View File

@@ -15,6 +15,7 @@
15 15
                     <p class="text-center" style="color: ${h.CFG.WEBSITE_HOME_TITLE_COLOR};">${h.CFG.WEBSITE_SUBTITLE|n}</p>
16 16
                 </div>
17 17
             </div>
18
+
18 19
             <div class="row">
19 20
                 <div class="col-sm-offset-3 col-sm-2">
20 21
                     <a class="thumbnail">
@@ -27,7 +28,7 @@
27 28
                 <div class="col-sm-4">
28 29
                     <div class="well">
29 30
                         <h2 style="margin-top: 0;">${TIM.ICO(32, 'status/status-locked')} ${_('Login')}</h2>
30
-                        <form role="form" method="POST" action="${tg.url('/login_handler', params=dict(came_from=came_from, __logins=login_counter))}">
31
+                        <form id='w-login-form' role="form" method="POST" action="${tg.url('/login_handler', params=dict(came_from=came_from, __logins=login_counter))}">
31 32
                             <div class="form-group">
32 33
                                 <div class="input-group">
33 34
                                     <div class="input-group-addon"><i class="fa fa-envelope-o"></i></div>
@@ -63,6 +64,7 @@
63 64
                 <div class="col-sm-offset-3 col-sm-6 text-center">${h.CFG.WEBSITE_HOME_BELOW_LOGIN_FORM|n}</div>
64 65
             </div>
65 66
 
67
+
66 68
         </div>
67 69
     </div>
68 70
 </div>

+ 0 - 1
tracim/tracim/templates/master_anonymous.mak View File

@@ -23,7 +23,6 @@
23 23
     background-size: cover;
24 24
     -o-background-size: cover;">
25 25
         <script src="${tg.url('/assets/js/jquery.min.js')}"></script>
26
-## FIXME - REMOVE        <script src="${tg.url('/javascript/tracim.js')}"></script>
27 26
 
28 27
         <div class="container-fluid">
29 28
             ${self.main_menu()}

+ 122 - 5
tracim/tracim/tests/__init__.py View File

@@ -10,7 +10,19 @@ from gearbox.commands.setup_app import SetupAppCommand
10 10
 from tg import config
11 11
 from tg.util import Bunch
12 12
 
13
-from tracim import model
13
+from sqlalchemy.engine import reflection
14
+
15
+from sqlalchemy.schema import DropConstraint
16
+from sqlalchemy.schema import DropSequence
17
+from sqlalchemy.schema import DropTable
18
+from sqlalchemy.schema import ForeignKeyConstraint
19
+from sqlalchemy.schema import MetaData
20
+from sqlalchemy.schema import Sequence
21
+from sqlalchemy.schema import Table
22
+
23
+import transaction
24
+
25
+from tracim.lib.base import logger
14 26
 
15 27
 __all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController']
16 28
 
@@ -24,21 +36,118 @@ def load_app(name=application_name):
24 36
 
25 37
 def setup_app():
26 38
     """Setup the application."""
39
+
40
+    engine = config['tg.app_globals'].sa_engine
41
+    inspector = reflection.Inspector.from_engine(engine)
42
+    metadata = MetaData()
43
+
44
+    logger.debug(setup_app, 'Before setup...')
45
+
27 46
     cmd = SetupAppCommand(Bunch(options=Bunch(verbose_level=1)), Bunch())
47
+    logger.debug(setup_app, 'After setup, before run...')
48
+
28 49
     cmd.run(Bunch(config_file='config:test.ini', section_name=None))
50
+    logger.debug(setup_app, 'After run...')
51
+
52
+
29 53
 
30 54
 def setup_db():
31 55
     """Create the database schema (not needed when you run setup_app)."""
56
+
32 57
     engine = config['tg.app_globals'].sa_engine
33
-    model.init_model(engine)
34
-    model.metadata.create_all(engine)
58
+    # model.init_model(engine)
59
+    # model.metadata.create_all(engine)
60
+
35 61
 
36 62
 
37 63
 def teardown_db():
38 64
     """Destroy the database schema."""
39 65
     engine = config['tg.app_globals'].sa_engine
40
-    model.metadata.drop_all(engine)
66
+    connection = engine.connect()
67
+
68
+    # INFO - D.A. - 2014-12-04
69
+    # Recipe taken from bitbucket:
70
+    # https://bitbucket.org/zzzeek/sqlalchemy/wiki/UsageRecipes/DropEverything
71
+
72
+    inspector = reflection.Inspector.from_engine(engine)
73
+    metadata = MetaData()
74
+
75
+    tbs = []
76
+    all_fks = []
77
+    views = []
78
+
79
+    # INFO - D.A. - 2014-12-04
80
+    # Sequences are hard defined here because SQLA does not allow to reflect them from existing schema
81
+    seqs = [
82
+        Sequence('seq__groups__group_id'),
83
+        Sequence('seq__contents__content_id'),
84
+        Sequence('seq__content_revisions__revision_id'),
85
+        Sequence('seq__permissions__permission_id'),
86
+        Sequence('seq__users__user_id'),
87
+        Sequence('seq__workspaces__workspace_id')
88
+    ]
89
+
90
+    for view_name in inspector.get_view_names():
91
+        v = Table(view_name,metadata)
92
+        views.append(v)
93
+
94
+    for table_name in inspector.get_table_names():
95
+
96
+        fks = []
97
+        for fk in inspector.get_foreign_keys(table_name):
98
+            if not fk['name']:
99
+                continue
100
+            fks.append(
101
+                ForeignKeyConstraint((),(),name=fk['name'])
102
+                )
103
+        t = Table(table_name,metadata,*fks)
104
+        tbs.append(t)
105
+        all_fks.extend(fks)
41 106
 
107
+    for fkc in all_fks:
108
+        connection.execute(DropConstraint(fkc))
109
+
110
+    for view in views:
111
+        drop_statement = 'DROP VIEW {}'.format(view.name)
112
+        # engine.execute(drop_statement)
113
+        connection.execute(drop_statement)
114
+
115
+    for table in tbs:
116
+        connection.execute(DropTable(table))
117
+
118
+
119
+    for sequence in seqs:
120
+        try:
121
+            connection.execute(DropSequence(sequence))
122
+        except Exception as e:
123
+            logger.debug(teardown_db, 'Exception while trying to remove sequence {}'.format(sequence.name))
124
+
125
+    transaction.commit()
126
+    engine.dispose()
127
+
128
+
129
+class TestStandard(object):
130
+
131
+    def setUp(self):
132
+        self.app = load_app('main')
133
+
134
+        logger.debug(self, 'Start setUp() by trying to clean database...')
135
+        try:
136
+            teardown_db()
137
+        except Exception as e:
138
+            logger.debug(self, 'teardown() throwed an exception {}'.format(e.__str__()))
139
+        logger.debug(self, 'Start setUp() by trying to clean database... -> done')
140
+
141
+        logger.debug(self, 'Start Application Setup...')
142
+        setup_app()
143
+        logger.debug(self, 'Start Application Setup... -> done')
144
+
145
+        logger.debug(self, 'Start Database Setup...')
146
+        setup_db()
147
+        logger.debug(self, 'Start Database Setup... -> done')
148
+
149
+    def tearDown(self):
150
+        transaction.commit()
42 151
 
43 152
 class TestController(object):
44 153
     """Base functional test case for the controllers.
@@ -61,9 +170,17 @@ class TestController(object):
61 170
     def setUp(self):
62 171
         """Setup test fixture for each functional test method."""
63 172
         self.app = load_app(self.application_under_test)
173
+
174
+        try:
175
+            teardown_db()
176
+        except Exception as e:
177
+            print('-> err ({})'.format(e.__str__()))
178
+
64 179
         setup_app()
180
+        setup_db()
181
+
65 182
 
66 183
     def tearDown(self):
67 184
         """Tear down test fixture for each functional test method."""
68
-        model.DBSession.remove()
185
+        # model.DBSession.remove()
69 186
         teardown_db()

+ 20 - 11
tracim/tracim/tests/functional/test_authentication.py View File

@@ -10,6 +10,7 @@ from __future__ import unicode_literals
10 10
 
11 11
 from nose.tools import eq_, ok_
12 12
 
13
+from tracim.lib.user import UserApi
13 14
 from tracim.tests import TestController
14 15
 
15 16
 
@@ -23,6 +24,9 @@ class TestAuthentication(TestController):
23 24
 
24 25
     application_under_test = 'main'
25 26
 
27
+    login = 'admin@admin.admin'
28
+    password = 'admin@admin.admin'
29
+
26 30
     def test_forced_login(self):
27 31
         """Anonymous users are forced to login
28 32
 
@@ -31,44 +35,49 @@ class TestAuthentication(TestController):
31 35
         should be redirected to the initially requested page.
32 36
 
33 37
         """
38
+
34 39
         # Requesting a protected area
35
-        resp = self.app.get('/secc/', status=302)
40
+        resp = self.app.get('/dashboard/', status=302)
36 41
         ok_( resp.location.startswith('http://localhost/login'))
37 42
         # Getting the login form:
38 43
         resp = resp.follow(status=200)
39 44
         form = resp.form
40 45
         # Submitting the login form:
41
-        form['login'] = 'manager'
42
-        form['password'] = 'managepass'
46
+        form['login'] = self.login
47
+        form['password'] = self.password
43 48
         post_login = form.submit(status=302)
44 49
         # Being redirected to the initially requested page:
45 50
         ok_(post_login.location.startswith('http://localhost/post_login'))
46 51
         initial_page = post_login.follow(status=302)
47 52
         ok_('authtkt' in initial_page.request.cookies,
48 53
             "Session cookie wasn't defined: %s" % initial_page.request.cookies)
49
-        ok_(initial_page.location.startswith('http://localhost/secc/'),
54
+        ok_(initial_page.location.startswith('http://localhost/dashboard/'),
50 55
             initial_page.location)
51 56
 
57
+
52 58
     def test_voluntary_login(self):
53 59
         """Voluntary logins must work correctly"""
54 60
         # Going to the login form voluntarily:
55
-        resp = self.app.get('/login', status=200)
61
+        resp = self.app.get('/', status=200)
56 62
         form = resp.form
57 63
         # Submitting the login form:
58
-        form['login'] = 'manager'
59
-        form['password'] = 'managepass'
64
+        form['login'] = self.login
65
+        form['password'] = self.password
60 66
         post_login = form.submit(status=302)
67
+
61 68
         # Being redirected to the home page:
62 69
         ok_(post_login.location.startswith('http://localhost/post_login'))
70
+        # FIXME - D.A - 2014-12-04 - Why double redirect to post_login ?!
63 71
         home_page = post_login.follow(status=302)
64
-        ok_('authtkt' in home_page.request.cookies,
65
-            'Session cookie was not defined: %s' % home_page.request.cookies)
66
-        eq_(home_page.location, 'http://localhost/')
72
+        real_home_page = home_page.follow(status=302)
73
+        ok_('authtkt' in real_home_page.request.cookies,
74
+            'Session cookie was not defined: %s' % real_home_page.request.cookies)
75
+        eq_(real_home_page.location, 'http://localhost/dashboard')
67 76
 
68 77
     def test_logout(self):
69 78
         """Logouts must work correctly"""
70 79
         # Logging in voluntarily the quick way:
71
-        resp = self.app.get('/login_handler?login=manager&password=managepass',
80
+        resp = self.app.get('/login_handler?login={}&password={}'.format(self.login, self.password),
72 81
                             status=302)
73 82
         resp = resp.follow(status=302)
74 83
         ok_('authtkt' in resp.request.cookies,

+ 11 - 48
tracim/tracim/tests/functional/test_root.py View File

@@ -11,8 +11,12 @@ Please read http://pythonpaste.org/webtest/ for more information.
11 11
 
12 12
 """
13 13
 
14
+from bs4 import BeautifulSoup
15
+
16
+from nose.tools import eq_
14 17
 from nose.tools import ok_
15 18
 
19
+from tracim.lib import helpers as h
16 20
 from tracim.tests import TestController
17 21
 
18 22
 
@@ -20,54 +24,13 @@ class TestRootController(TestController):
20 24
     """Tests for the method in the root controller."""
21 25
 
22 26
     def test_index(self):
23
-        """The front page is working properly"""
24 27
         response = self.app.get('/')
25
-        msg = 'TurboGears 2 is rapid web application development toolkit '\
26
-              'designed to make your life easier.'
27
-        # You can look for specific strings:
28
-        ok_(msg in response)
29
-
30
-        # You can also access a BeautifulSoup'ed response in your tests
31
-        # (First run $ easy_install BeautifulSoup
32
-        # and then uncomment the next two lines)
33
-
34
-        # links = response.html.findAll('a')
35
-        # print links
36
-        # ok_(links, "Mummy, there are no links here!")
37
-
38
-    def test_environ(self):
39
-        """Displaying the wsgi environ works"""
40
-        response = self.app.get('/environ.html')
41
-        ok_('The keys in the environment are: ' in response)
28
+        eq_(200, response.status_int)
42 29
 
43
-    def test_data(self):
44
-        """The data display demo works with HTML"""
45
-        response = self.app.get('/data.html?a=1&b=2')
46
-        expected1 = """<td>a</td>\n                <td>1</td>"""
47
-        expected2 = """<td>b</td>\n                <td>2</td>"""
48
-        body = '\n'.join(response.text.splitlines())
49
-        ok_(expected1 in body, response)
50
-        ok_(expected2 in body, response)
51
-
52
-    def test_data_json(self):
53
-        """The data display demo works with JSON"""
54
-        resp = self.app.get('/data.json?a=1&b=2')
55
-        ok_(dict(page='data', params={'a':'1', 'b':'2'}) == resp.json, resp.json)
56
-
57
-    def test_secc_with_manager(self):
58
-        """The manager can access the secure controller"""
59
-        # Note how authentication is forged:
60
-        environ = {'REMOTE_USER': 'manager'}
61
-        resp = self.app.get('/secc', extra_environ=environ, status=200)
62
-        ok_('Secure Controller here' in resp.text, resp.text)
63
-
64
-    def test_secc_with_editor(self):
65
-        """The editor cannot access the secure controller"""
66
-        environ = {'REMOTE_USER': 'editor'}
67
-        self.app.get('/secc', extra_environ=environ, status=403)
68
-        # It's enough to know that authorization was denied with a 403 status
30
+        msg = 'copyright &copy; 2013 - {} tracim project.'.format(h.current_year())
31
+        ok_(msg in response)
69 32
 
70
-    def test_secc_with_anonymous(self):
71
-        """Anonymous users must not access the secure controller"""
72
-        self.app.get('/secc', status=401)
73
-        # It's enough to know that authorization was denied with a 401 status
33
+        forms = BeautifulSoup(response.body).find_all('form')
34
+        print('FORMS = ',forms)
35
+        eq_(1, len(forms))
36
+        eq_('w-login-form', forms[0].get('id'))

+ 2 - 1
tracim/tracim/tests/library/__init__.py View File

@@ -1 +1,2 @@
1
-__author__ = 'damien'
1
+# -*- coding: utf-8 -*-
2
+"""Unit test suite for the models of the application."""

+ 129 - 0
tracim/tracim/tests/library/test_content_api.py View File

@@ -0,0 +1,129 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from nose.tools import eq_
4
+
5
+import transaction
6
+
7
+from tracim.lib.content import compare_content_for_sorting_by_type_and_name
8
+from tracim.lib.content import ContentApi
9
+
10
+from tracim.model.data import Content
11
+from tracim.model.data import ContentType
12
+
13
+from tracim.tests import TestStandard
14
+
15
+
16
+class TestContentApi(TestStandard):
17
+
18
+    def test_compare_content_for_sorting_by_type(self):
19
+        c1 = Content()
20
+        c1.label = ''
21
+        c1.type = 'file'
22
+
23
+        c2 = Content()
24
+        c2.label = ''
25
+        c2.type = 'folder'
26
+
27
+        c11 = c1
28
+
29
+        eq_(1, compare_content_for_sorting_by_type_and_name(c1, c2))
30
+        eq_(-1, compare_content_for_sorting_by_type_and_name(c2, c1))
31
+        eq_(0, compare_content_for_sorting_by_type_and_name(c1, c11))
32
+
33
+    def test_compare_content_for_sorting_by_label(self):
34
+        c1 = Content()
35
+        c1.label = 'bbb'
36
+        c1.type = 'file'
37
+
38
+        c2 = Content()
39
+        c2.label = 'aaa'
40
+        c2.type = 'file'
41
+
42
+        c11 = c1
43
+
44
+        eq_(1, compare_content_for_sorting_by_type_and_name(c1, c2))
45
+        eq_(-1, compare_content_for_sorting_by_type_and_name(c2, c1))
46
+        eq_(0, compare_content_for_sorting_by_type_and_name(c1, c11))
47
+
48
+    def test_sort_by_label_or_filename(self):
49
+        c1 = Content()
50
+        c1.label = 'ABCD'
51
+        c1.type = 'file'
52
+
53
+        c2 = Content()
54
+        c2.label = ''
55
+        c2.type = 'file'
56
+        c2.file_name = 'AABC'
57
+
58
+        c3 = Content()
59
+        c3.label = 'BCDE'
60
+        c3.type = 'file'
61
+
62
+        items = [c1, c2, c3]
63
+        sorteds = ContentApi.sort_content(items)
64
+
65
+        eq_(sorteds[0], c2)
66
+        eq_(sorteds[1], c1)
67
+        eq_(sorteds[2], c3)
68
+
69
+    def test_sort_by_content_type(self):
70
+        c1 = Content()
71
+        c1.label = 'AAAA'
72
+        c1.type = 'file'
73
+
74
+        c2 = Content()
75
+        c2.label = 'BBBB'
76
+        c2.type = 'folder'
77
+
78
+        items = [c1, c2]
79
+        sorteds = ContentApi.sort_content(items)
80
+
81
+        eq_(sorteds[0], c2, 'value is {} instead of {}'.format(sorteds[0].content_id, c2.content_id))
82
+        eq_(sorteds[1], c1, 'value is {} instead of {}'.format(sorteds[1].content_id, c1.content_id))
83
+
84
+class TestContentApiFilteringDeletedItem(TestStandard):
85
+
86
+    def test_delete(self):
87
+        api = ContentApi(None)
88
+        item = api.create(ContentType.Folder, None, None, 'not_deleted', True)
89
+        item2 = api.create(ContentType.Folder, None, None, 'to_delete', True)
90
+        transaction.commit()
91
+
92
+        items = api.get_all(None, ContentType.Any, None)
93
+        eq_(2, len(items))
94
+
95
+        items = api.get_all(None, ContentType.Any, None)
96
+        api.delete(items[0])
97
+        transaction.commit()
98
+
99
+        items = api.get_all(None, ContentType.Any, None)
100
+        eq_(1, len(items))
101
+        transaction.commit()
102
+
103
+        # Test that the item is still available if "show deleted" is activated
104
+        api = ContentApi(None, show_deleted=True)
105
+        items = api.get_all(None, ContentType.Any, None)
106
+        eq_(2, len(items))
107
+
108
+
109
+    def test_archive(self):
110
+        api = ContentApi(None)
111
+        item = api.create(ContentType.Folder, None, None, 'not_archived', True)
112
+        item2 = api.create(ContentType.Folder, None, None, 'to_archive', True)
113
+        transaction.commit()
114
+
115
+        items = api.get_all(None, ContentType.Any, None)
116
+        eq_(2, len(items))
117
+
118
+        items = api.get_all(None, ContentType.Any, None)
119
+        api.archive(items[0])
120
+        transaction.commit()
121
+
122
+        items = api.get_all(None, ContentType.Any, None)
123
+        eq_(1, len(items))
124
+        transaction.commit()
125
+
126
+        # Test that the item is still available if "show deleted" is activated
127
+        api = ContentApi(None, show_archived=True)
128
+        items = api.get_all(None, ContentType.Any, None)
129
+        eq_(2, len(items))

+ 91 - 0
tracim/tracim/tests/library/test_serializers.py View File

@@ -0,0 +1,91 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from nose.tools import eq_
4
+from nose.tools import ok_
5
+from nose.tools import raises
6
+
7
+from sqlalchemy.orm.exc import NoResultFound
8
+
9
+import transaction
10
+
11
+
12
+from tracim.model.data import Content
13
+
14
+from tracim.model.serializers import Context
15
+from tracim.model.serializers import ContextConverterNotFoundException
16
+from tracim.model.serializers import CTX
17
+from tracim.model.serializers import DictLikeClass
18
+
19
+from tracim.model.data import ActionDescription
20
+
21
+from tracim.lib.user import UserApi
22
+
23
+from tracim.tests import TestStandard
24
+
25
+
26
+
27
+class TestSerializers(TestStandard):
28
+
29
+    def test_DictLikeClass(self):
30
+        instance = DictLikeClass()
31
+
32
+        instance.bob = 'titi'
33
+        ok_(instance.bob==instance['bob'])
34
+
35
+        instance['titi'] = 'bob'
36
+        ok_(instance.titi==instance['titi'])
37
+
38
+        instance2 = DictLikeClass(instance)
39
+        ok_(instance2.bob==instance2['bob'])
40
+        ok_(instance2.bob==instance.bob)
41
+        ok_(instance2.titi==instance2['titi'])
42
+        ok_(instance2.titi==instance.titi)
43
+
44
+        instance3 = DictLikeClass({'bob': 'titi', 'toto': 'bib'})
45
+        ok_(instance3.bob=='titi')
46
+        ok_(instance3.bob==instance3['bob'])
47
+
48
+        ok_(instance3.toto=='bib')
49
+        ok_(instance3.toto==instance3['toto'])
50
+
51
+    def test_ContextConverterNotFoundException(self):
52
+        class DummyClass(object):
53
+            pass
54
+        context = 'some_context'
55
+        e = ContextConverterNotFoundException(context, DummyClass)
56
+        eq_('converter not found (context: some_context - model: DummyClass)', e.__str__())
57
+
58
+    def test_serialize_ActionDescription_DEFAULT(self):
59
+        obj = ActionDescription('archiving')
60
+        obj.icon = 'places/folder-remote'
61
+        obj.label = 'edit the content'
62
+
63
+        res = Context(CTX.DEFAULT).toDict(obj)
64
+        eq_(res.__class__, DictLikeClass)
65
+        eq_(obj.id, res.id)
66
+        eq_(obj.label, res.label)
67
+        eq_(obj.icon, res.icon)
68
+        eq_(3, len(res.keys()))
69
+
70
+    def test_serialize_Content_DEFAULT(self):
71
+        obj = Content()
72
+        obj.content_id = 132
73
+        obj.label = 'Some label'
74
+        obj.description = 'Some Description'
75
+
76
+        res = Context(CTX.DEFAULT).toDict(obj)
77
+        eq_(res.__class__, DictLikeClass, res)
78
+        eq_(obj.content_id, res.id, res)
79
+        eq_(obj.label, res.label, res)
80
+
81
+        ok_('folder' in res.keys())
82
+        ok_('id' in res.folder.keys())
83
+        eq_(None, res.folder.id)
84
+        eq_(1, len(res.folder.keys()))
85
+
86
+        ok_('workspace' in res.keys())
87
+        eq_(None, res.workspace, res)
88
+        eq_(4, len(res.keys()), res)
89
+
90
+
91
+

+ 66 - 0
tracim/tracim/tests/library/test_user_api.py View File

@@ -0,0 +1,66 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from nose.tools import eq_
4
+from nose.tools import ok_
5
+from nose.tools import raises
6
+
7
+from sqlalchemy.orm.exc import NoResultFound
8
+
9
+import transaction
10
+
11
+from tracim.lib.user import UserApi
12
+from tracim.tests import TestStandard
13
+
14
+
15
+
16
+class TestUserApi(TestStandard):
17
+
18
+    def test_create_and_update_user(self):
19
+        api = UserApi(None)
20
+        u = api.create_user()
21
+        api.update(u, 'bob', 'bob@bob', True)
22
+
23
+        nu = api.get_one_by_email('bob@bob')
24
+        ok_(nu!=None)
25
+        eq_('bob@bob', nu.email)
26
+        eq_('bob', nu.display_name)
27
+
28
+
29
+    def test_user_with_email_exists(self):
30
+        api = UserApi(None)
31
+        u = api.create_user()
32
+        api.update(u, 'bibi', 'bibi@bibi', True)
33
+        transaction.commit()
34
+
35
+        eq_(True, api.user_with_email_exists('bibi@bibi'))
36
+        eq_(False, api.user_with_email_exists('unknown'))
37
+
38
+
39
+    def test_get_one_by_email(self):
40
+        api = UserApi(None)
41
+        u = api.create_user()
42
+        api.update(u, 'bibi', 'bibi@bibi', True)
43
+        uid = u.user_id
44
+        transaction.commit()
45
+
46
+        eq_(uid, api.get_one_by_email('bibi@bibi').user_id)
47
+
48
+    @raises(NoResultFound)
49
+    def test_get_one_by_email_exception(self):
50
+        api = UserApi(None)
51
+        api.get_one_by_email('unknown')
52
+
53
+    def test_get_all(self):
54
+        api = UserApi(None)
55
+        # u1 = api.create_user(True)
56
+        # u2 = api.create_user(True)
57
+
58
+        # users = api.get_all()
59
+        # ok_(2==len(users))
60
+
61
+    def test_get_one(self):
62
+        api = UserApi(None)
63
+        u = api.create_user()
64
+        api.update(u, 'titi', 'titi@titi', True)
65
+        one = api.get_one(u.user_id)
66
+        eq_(u.user_id, one.user_id)

+ 0 - 62
tracim/tracim/tests/models/__init__.py View File

@@ -1,64 +1,2 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 """Unit test suite for the models of the application."""
3
-
4
-from nose.tools import eq_
5
-from tracim.model import DBSession
6
-from tracim.tests import load_app
7
-from tracim.tests import setup_db, teardown_db
8
-
9
-__all__ = ['ModelTest']
10
-
11
-
12
-def setup():
13
-    """Setup test fixture for all model tests."""
14
-    load_app()
15
-    setup_db()
16
-
17
-
18
-def teardown():
19
-    """Tear down test fixture for all model tests."""
20
-    teardown_db()
21
-
22
-
23
-class ModelTest(object):
24
-    """Base unit test case for the models."""
25
-
26
-    klass = None
27
-    attrs = {}
28
-
29
-    def setUp(self):
30
-        """Setup test fixture for each model test method."""
31
-        try:
32
-            new_attrs = {}
33
-            new_attrs.update(self.attrs)
34
-            new_attrs.update(self.do_get_dependencies())
35
-            self.obj = self.klass(**new_attrs)
36
-            DBSession.add(self.obj)
37
-            DBSession.flush()
38
-            return self.obj
39
-        except:
40
-            DBSession.rollback()
41
-            raise
42
-
43
-    def tearDown(self):
44
-        """Tear down test fixture for each model test method."""
45
-        DBSession.rollback()
46
-
47
-    def do_get_dependencies(self):
48
-        """Get model test dependencies.
49
-
50
-        Use this method to pull in other objects that need to be created
51
-        for this object to be build properly.
52
-
53
-        """
54
-        return {}
55
-
56
-    def test_create_obj(self):
57
-        """Model objects can be created"""
58
-        pass
59
-
60
-    def test_query_obj(self):
61
-        """Model objects can be queried"""
62
-        obj = DBSession.query(self.klass).one()
63
-        for key, value in self.attrs.items():
64
-            eq_(getattr(obj, key), value)

+ 0 - 52
tracim/tracim/tests/models/test_auth.py View File

@@ -1,52 +0,0 @@
1
-# -*- coding: utf-8 -*-
2
-"""Test suite for the TG app's models"""
3
-from __future__ import unicode_literals
4
-from nose.tools import eq_
5
-
6
-from tracim import model
7
-from tracim.tests.models import ModelTest
8
-
9
-class TestGroup(ModelTest):
10
-    """Unit test case for the ``Group`` model."""
11
-    klass = model.Group
12
-    attrs = dict(
13
-        group_name = "test_group",
14
-        display_name = "Test Group"
15
-        )
16
-
17
-
18
-class TestUser(ModelTest):
19
-    """Unit test case for the ``User`` model."""
20
-    
21
-    klass = model.User
22
-    attrs = dict(
23
-        user_name = "ignucius",
24
-        email = "ignucius@example.org"
25
-        )
26
-
27
-    def test_obj_creation_username(self):
28
-        """The obj constructor must set the user name right"""
29
-        eq_(self.obj.user_name, "ignucius")
30
-
31
-    def test_obj_creation_email(self):
32
-        """The obj constructor must set the email right"""
33
-        eq_(self.obj.email, "ignucius@example.org")
34
-
35
-    def test_no_permissions_by_default(self):
36
-        """User objects should have no permission by default."""
37
-        eq_(len(self.obj.permissions), 0)
38
-
39
-    def test_getting_by_email(self):
40
-        """Users should be fetcheable by their email addresses"""
41
-        him = model.User.by_email_address("ignucius@example.org")
42
-        eq_(him, self.obj)
43
-
44
-
45
-class TestPermission(ModelTest):
46
-    """Unit test case for the ``Permission`` model."""
47
-    
48
-    klass = model.Permission
49
-    attrs = dict(
50
-        permission_name = "test_permission",
51
-        description = "This is a test Description"
52
-        )

+ 34 - 0
tracim/tracim/tests/models/test_user.py View File

@@ -0,0 +1,34 @@
1
+import transaction
2
+
3
+from nose.tools import eq_
4
+from nose.tools import ok_
5
+
6
+from tracim.model import DBSession
7
+
8
+from tracim.tests import TestStandard
9
+
10
+
11
+from tracim.model.auth import User
12
+
13
+
14
+class TestUserModel(TestStandard):
15
+
16
+    def test_create(self):
17
+        DBSession.flush()
18
+        transaction.commit()
19
+        name = 'Damien'
20
+        email = 'damien@accorsi.info'
21
+
22
+        user = User()
23
+        user.display_name = name
24
+        user.email = email
25
+
26
+        DBSession.add(user)
27
+        DBSession.flush()
28
+        transaction.commit()
29
+
30
+        new_user = DBSession.query(User).filter(User.display_name==name).one()
31
+
32
+        eq_(new_user.display_name, name)
33
+        eq_(new_user.email, email)
34
+        eq_(new_user.email_address, email)

+ 4 - 3
tracim/tracim/websetup/schema.py View File

@@ -48,7 +48,8 @@ SET client_min_messages = warning;
48 48
 -- COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
49 49
 SET search_path = public, pg_catalog;
50 50
 
51
-CREATE FUNCTION update_node() RETURNS trigger
51
+
52
+CREATE OR REPLACE FUNCTION update_node() RETURNS trigger
52 53
     LANGUAGE plpgsql
53 54
     AS $$
54 55
 BEGIN
@@ -60,7 +61,7 @@ return new;
60 61
 END;
61 62
 $$;
62 63
 
63
-CREATE FUNCTION set_created() RETURNS trigger
64
+CREATE OR REPLACE FUNCTION set_created() RETURNS trigger
64 65
     LANGUAGE plpgsql
65 66
     AS $$
66 67
 BEGIN
@@ -70,7 +71,7 @@ BEGIN
70 71
 END;
71 72
 $$;
72 73
 
73
-CREATE FUNCTION set_updated() RETURNS trigger
74
+CREATE OR REPLACE FUNCTION set_updated() RETURNS trigger
74 75
     LANGUAGE plpgsql
75 76
     AS $$
76 77
 BEGIN