mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Dependency injection is cool - it should however not be confined to the top-layer of the application. Inversion of control can help every layer and is a great way to de-couple various parts of the app.
This code brings in a framework (Lagom) to build a dependency injection framework and then adds a small function (``depends``) that adapts it to FastAPI's dependency injection mechanism ("Depends").
The advantages to this approach are numerous.
We don't need to write these little adapters for each component of the backend to adapt it FastAPI. For example look at the change to the roles API controller:
```diff
-def get_role_manager(app: UniverseApplication = Depends(get_app)) -> RoleManager:
- return app.role_manager
-
-
@cbv(router)
class FastAPIRoles:
- role_manager: RoleManager = Depends(get_role_manager)
+ role_manager: RoleManager = depends(RoleManager)
```
This is much less boilerplate. We don't need to implement & type that function get_role_manager and we don't need to bring in the import on UniverseApplication.
Additionally, we've got a clean abstraction that shields us from ``fastapi`` imports in all of our controllers. It should make it more possible to switch to new frameworks and such as the Python ecosystem matures.
Also, the same DI that is used to inject ``RoleManager`` into this contoller is used to inject app into RoleManager when it is constructed during application initialization. Any component being managed by UniverseApplication can now rely on its constructor arguments to be injected if it wants. It is easy to see the cool examples on FastAPI and think it is just a technology for controllers, but it totally is not.
I don't think there is really a way to use FastAPI's dependency injection outside the context of that framework, but even if one could Lagom is superior. All the auto-wiring is by type and requires zero framework and zero configuration (https://github.com/meadsteve/lagom#auto-wiring-with-zero-configuraton).
Having a web framework provide these framework-bound extension points for injecting stuff into controllers was the state of the art of Java like 15 years ago. Skipping that whole learning process and using the type system and auto-wiring that isn't dependent on framework annotation really jumps out to Java circa 8 years ago!
Why Lagom is an interesting question. When researching DI frameworks, I couldn't find a clear winner but Lagom focus on type annotations versus annotating by name makes it clearly more modern than a lot frameworks by much bigger names (https://github.com/meadsteve/lagom/blob/master/docs/comparison.md). The other type-centric framework that had even a nice interface that I found was punq (https://punq.readthedocs.io/en/latest/). The development just doesn't seem as active on punq. While I didn't land up using the integration Lagom seems to have async frameworks in mind (https://github.com/meadsteve/lagom/blob/master/lagom/integrations/fast_api.py), so that is another plus. Ultimately though I think I can swap between these two with like 10 lines of code switch, they do cool things with very simple interfaces and neither requires a bunch of investment in annotation on your components.
359 lines
19 KiB
Python
359 lines
19 KiB
Python
"""
|
|
User Manager testing.
|
|
|
|
Executable directly using: python -m test.unit.managers.test_UserManager
|
|
"""
|
|
import json
|
|
import unittest
|
|
from datetime import datetime, timedelta
|
|
|
|
from sqlalchemy import desc
|
|
|
|
from galaxy import exceptions, model
|
|
from galaxy.managers import base as base_manager
|
|
from galaxy.managers import histories, users
|
|
from galaxy.security.passwords import check_password
|
|
from galaxy.webapps.galaxy.controllers.user import User
|
|
from .base import BaseTestCase
|
|
|
|
|
|
# =============================================================================
|
|
default_password = '123456'
|
|
changed_password = '654321'
|
|
user2_data = dict(email='user2@user2.user2', username='user2', password=default_password)
|
|
user3_data = dict(email='user3@user3.user3', username='user3', password=default_password)
|
|
user4_data = dict(email='user4@user4.user4', username='user4', password=default_password)
|
|
uppercase_email_user = dict(email='USER5@USER5.USER5', username='USER5', password=default_password)
|
|
lowercase_email_user = dict(email='user5@user5.user5', username='user5', password=default_password)
|
|
|
|
|
|
# =============================================================================
|
|
class UserManagerTestCase(BaseTestCase):
|
|
|
|
def test_framework(self):
|
|
self.log("(for testing) should have admin_user, and admin_user is current")
|
|
self.assertEqual(self.trans.user, self.admin_user)
|
|
|
|
def test_base(self):
|
|
self.log("should be able to create a user")
|
|
user2 = self.user_manager.create(**user2_data)
|
|
self.assertIsInstance(user2, model.User)
|
|
self.assertIsNotNone(user2.id)
|
|
self.assertEqual(user2.email, user2_data['email'])
|
|
self.assertTrue(check_password(default_password, user2.password))
|
|
|
|
user3 = self.user_manager.create(**user3_data)
|
|
|
|
self.log("should be able to query")
|
|
users = self.trans.sa_session.query(model.User).all()
|
|
self.assertEqual(self.user_manager.list(), users)
|
|
|
|
self.assertEqual(self.user_manager.by_id(user2.id), user2)
|
|
self.assertEqual(self.user_manager.by_ids([user3.id, user2.id]), [user3, user2])
|
|
|
|
self.log("should be able to limit and offset")
|
|
self.assertEqual(self.user_manager.list(limit=1), users[0:1])
|
|
self.assertEqual(self.user_manager.list(offset=1), users[1:])
|
|
self.assertEqual(self.user_manager.list(limit=1, offset=1), users[1:2])
|
|
|
|
self.assertEqual(self.user_manager.list(limit=0), [])
|
|
self.assertEqual(self.user_manager.list(offset=3), [])
|
|
|
|
self.log("should be able to order")
|
|
self.assertEqual(self.user_manager.list(order_by=(desc(model.User.create_time))),
|
|
[user3, user2, self.admin_user])
|
|
|
|
def test_invalid_create(self):
|
|
self.user_manager.create(**user2_data)
|
|
|
|
self.log("emails must be unique")
|
|
self.assertRaises(exceptions.Conflict, self.user_manager.create,
|
|
**dict(email='user2@user2.user2', username='user2a', password=default_password))
|
|
self.log("usernames must be unique")
|
|
self.assertRaises(exceptions.Conflict, self.user_manager.create,
|
|
**dict(email='user2a@user2.user2', username='user2', password=default_password))
|
|
|
|
def test_email_queries(self):
|
|
user2 = self.user_manager.create(**user2_data)
|
|
|
|
self.log("should be able to query by email")
|
|
self.assertEqual(self.user_manager.by_email(user2_data['email']), user2)
|
|
|
|
def test_admin(self):
|
|
user2 = self.user_manager.create(**user2_data)
|
|
|
|
self.log("should be able to test whether admin")
|
|
self.assertTrue(self.user_manager.is_admin(self.admin_user))
|
|
self.assertFalse(self.user_manager.is_admin(user2))
|
|
self.assertEqual(self.user_manager.admins(), [self.admin_user])
|
|
self.assertRaises(exceptions.AdminRequiredException, self.user_manager.error_unless_admin, user2)
|
|
self.assertEqual(self.user_manager.error_unless_admin(self.admin_user), self.admin_user)
|
|
|
|
def test_anonymous(self):
|
|
anon = None
|
|
user2 = self.user_manager.create(**user2_data)
|
|
|
|
self.log("should be able to tell if a user is anonymous")
|
|
self.assertRaises(exceptions.AuthenticationFailed, self.user_manager.error_if_anonymous, anon)
|
|
self.assertEqual(self.user_manager.error_if_anonymous(user2), user2)
|
|
|
|
def test_current(self):
|
|
user2 = self.user_manager.create(**user2_data)
|
|
|
|
self.log("should be able to tell if a user is the current (trans) user")
|
|
self.assertEqual(self.user_manager.current_user(self.trans), self.admin_user)
|
|
self.assertNotEqual(self.user_manager.current_user(self.trans), user2)
|
|
|
|
def test_change_password(self):
|
|
self.log("should be able to change password")
|
|
user2 = self.user_manager.create(**user2_data)
|
|
encoded_id = self.app.security.encode_id(user2.id)
|
|
self.assertIsInstance(user2, model.User)
|
|
self.assertIsNotNone(user2.id)
|
|
self.assertEqual(user2.email, user2_data["email"])
|
|
self.assertTrue(check_password(default_password, user2.password))
|
|
user, message = self.user_manager.change_password(self.trans)
|
|
self.assertEqual(message, "Please provide a token or a user and password.")
|
|
user, message = self.user_manager.change_password(self.trans, id=encoded_id, current=changed_password)
|
|
self.assertEqual(message, "Invalid current password.")
|
|
user, message = self.user_manager.change_password(self.trans, id=encoded_id, current=default_password, password=changed_password, confirm=default_password)
|
|
self.assertEqual(message, "Passwords do not match.")
|
|
user, message = self.user_manager.change_password(self.trans, id=encoded_id, current=default_password, password=default_password, confirm=changed_password)
|
|
self.assertEqual(message, "Passwords do not match.")
|
|
user, message = self.user_manager.change_password(self.trans, id=encoded_id, current=default_password, password=changed_password, confirm=changed_password)
|
|
self.assertFalse(check_password(default_password, user2.password))
|
|
self.assertTrue(check_password(changed_password, user2.password))
|
|
reset_user, prt = self.user_manager.get_reset_token(self.trans, user2.email)
|
|
user, message = self.user_manager.change_password(self.trans, token=prt.token, password=default_password, confirm=default_password)
|
|
self.assertTrue(check_password(default_password, user2.password))
|
|
self.assertFalse(check_password(changed_password, user2.password))
|
|
prt.expiration_time = datetime.utcnow()
|
|
user, message = self.user_manager.change_password(self.trans, token=prt.token, password=default_password, confirm=default_password)
|
|
self.assertEqual(message, "Invalid or expired password reset token, please request a new one.")
|
|
|
|
def test_login(self):
|
|
self.log("should be able to validate user credentials")
|
|
user2 = self.user_manager.create(**user2_data)
|
|
self.app.security.encode_id(user2.id)
|
|
self.assertIsInstance(user2, model.User)
|
|
self.assertIsNotNone(user2.id)
|
|
self.assertEqual(user2.email, user2_data["email"])
|
|
self.assertTrue(check_password(default_password, user2.password))
|
|
controller = User(self.app)
|
|
response = json.loads(controller.login(self.trans))
|
|
self.assertEqual(response["err_msg"], "Please specify a username and password.")
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.email, "password": changed_password}))
|
|
self.assertEqual(response["err_msg"], "Invalid password.")
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.username, "password": changed_password}))
|
|
self.assertEqual(response["err_msg"], "Invalid password.")
|
|
user2.deleted = True
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.username, "password": default_password}))
|
|
self.assertEqual(response["err_msg"], "This account has been marked deleted, contact your local Galaxy administrator to restore the account. Contact: admin@email.to.")
|
|
user2.deleted = False
|
|
user2.external = True
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.username, "password": default_password}))
|
|
self.assertEqual(response["err_msg"], "This account was created for use with an external authentication method, contact your local Galaxy administrator to activate it. Contact: admin@email.to.")
|
|
user2.external = False
|
|
self.trans.app.config.password_expiration_period = timedelta(days=1)
|
|
user2.last_password_change = datetime.today() - timedelta(days=1)
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.username, "password": default_password}))
|
|
self.assertEqual(response["message"], "Your password has expired. Please reset or change it to access Galaxy.")
|
|
self.assertEqual(response["expired_user"], self.trans.security.encode_id(user2.id))
|
|
self.trans.app.config.password_expiration_period = timedelta(days=10)
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.username, "password": default_password}))
|
|
self.assertEqual(response["message"], "Your password will expire in 11 day(s).")
|
|
self.trans.app.config.password_expiration_period = timedelta(days=100)
|
|
response = json.loads(controller.login(self.trans, payload={"login": user2.username, "password": default_password}))
|
|
self.assertEqual(response["message"], "Success.")
|
|
|
|
def test_empty_password(self):
|
|
self.log("should be able to create a user with no password")
|
|
user = self.user_manager.create(email='user@nopassword.com', username='nopassword')
|
|
self.assertIsNotNone(user.id)
|
|
self.assertIsNotNone(user.password)
|
|
# should not be able to login with a null or empty password
|
|
self.assertFalse(check_password("", user.password))
|
|
self.assertFalse(check_password(None, user.password))
|
|
|
|
def test_get_user_by_identity(self):
|
|
# return None if username/email not found
|
|
assert self.user_manager.get_user_by_identity('xyz') is None
|
|
uppercase_user = self.user_manager.create(**uppercase_email_user)
|
|
assert uppercase_user.email == uppercase_email_user['email']
|
|
assert uppercase_user.username == uppercase_email_user['username']
|
|
assert self.user_manager.get_user_by_identity(uppercase_user.email) == uppercase_user
|
|
assert self.user_manager.get_user_by_identity(uppercase_user.username) == uppercase_user
|
|
# Create another user with the same email just differently capitalized.
|
|
# This is not normally allowed now, since registration goes through user_manager.register(),
|
|
# which checks for that, but was possible in earlier releases of Galaxy
|
|
lowercase_user = self.user_manager.create(**lowercase_email_user)
|
|
assert lowercase_user.email == lowercase_email_user['email']
|
|
assert lowercase_user.username == lowercase_email_user['username']
|
|
assert self.user_manager.get_user_by_identity(lowercase_user.email) == lowercase_user
|
|
assert self.user_manager.get_user_by_identity(lowercase_user.username) == lowercase_user
|
|
# assert uppercase user can still be retrieved
|
|
assert self.user_manager.get_user_by_identity(uppercase_user.email) == uppercase_user
|
|
assert self.user_manager.get_user_by_identity(uppercase_user.username) == uppercase_user
|
|
# username matches need to be exact
|
|
assert self.user_manager.get_user_by_identity(uppercase_user.username.capitalize()) is None
|
|
# email matches can ignore capitalization
|
|
ignore_email_capitalization_user = self.user_manager.create(email='user123@nopassword.com', username='someusername123')
|
|
assert self.user_manager.get_user_by_identity(ignore_email_capitalization_user.email.capitalize()) == ignore_email_capitalization_user
|
|
|
|
|
|
# =============================================================================
|
|
class UserSerializerTestCase(BaseTestCase):
|
|
|
|
def set_up_managers(self):
|
|
super().set_up_managers()
|
|
self.user_serializer = users.UserSerializer(self.app)
|
|
|
|
def test_views(self):
|
|
user = self.user_manager.create(**user2_data)
|
|
|
|
self.log('should have a summary view')
|
|
summary_view = self.user_serializer.serialize_to_view(user, view='summary')
|
|
self.assertKeys(summary_view, self.user_serializer.views['summary'])
|
|
|
|
self.log('should have the summary view as default view')
|
|
default_view = self.user_serializer.serialize_to_view(user, default_view='summary')
|
|
self.assertKeys(default_view, self.user_serializer.views['summary'])
|
|
|
|
self.log('should have a serializer for all serializable keys')
|
|
for key in self.user_serializer.serializable_keyset:
|
|
instantiated_attribute = getattr(user, key, None)
|
|
if not ((key in self.user_serializer.serializers)
|
|
or (isinstance(instantiated_attribute, self.TYPES_NEEDING_NO_SERIALIZERS))):
|
|
self.fail(f'no serializer for: {key} ({instantiated_attribute})')
|
|
else:
|
|
self.assertTrue(True, 'all serializable keys have a serializer')
|
|
|
|
def test_views_and_keys(self):
|
|
user = self.user_manager.create(**user2_data)
|
|
|
|
self.log('should be able to use keys with views')
|
|
serialized = self.user_serializer.serialize_to_view(user,
|
|
view='summary', keys=['create_time'])
|
|
self.assertKeys(serialized,
|
|
self.user_serializer.views['summary'] + ['create_time'])
|
|
|
|
self.log('should be able to use keys on their own')
|
|
serialized = self.user_serializer.serialize_to_view(user,
|
|
keys=['tags_used', 'is_admin'])
|
|
self.assertKeys(serialized, ['tags_used', 'is_admin'])
|
|
|
|
def test_serializers(self):
|
|
user = self.user_manager.create(**user2_data)
|
|
all_keys = list(self.user_serializer.serializable_keyset)
|
|
serialized = self.user_serializer.serialize(user, all_keys, trans=self.trans)
|
|
# pprint.pprint( serialized )
|
|
|
|
self.log('everything serialized should be of the proper type')
|
|
self.assertEncodedId(serialized['id'])
|
|
self.assertDate(serialized['create_time'])
|
|
self.assertDate(serialized['update_time'])
|
|
self.assertIsInstance(serialized['deleted'], bool)
|
|
self.assertIsInstance(serialized['purged'], bool)
|
|
|
|
# self.assertIsInstance( serialized[ 'active' ], bool )
|
|
self.assertIsInstance(serialized['is_admin'], bool)
|
|
self.assertIsInstance(serialized['total_disk_usage'], float)
|
|
self.assertIsInstance(serialized['nice_total_disk_usage'], str)
|
|
self.assertIsInstance(serialized['quota_percent'], (type(None), float))
|
|
self.assertIsInstance(serialized['tags_used'], list)
|
|
|
|
self.log('serialized should jsonify well')
|
|
self.assertIsJsonifyable(serialized)
|
|
|
|
|
|
class CurrentUserSerializerTestCase(BaseTestCase):
|
|
|
|
def set_up_managers(self):
|
|
super().set_up_managers()
|
|
self.history_manager = self.app[histories.HistoryManager]
|
|
self.user_serializer = users.CurrentUserSerializer(self.app)
|
|
|
|
def test_anonymous(self):
|
|
anonym = None
|
|
# need a history here for total_disk_usage
|
|
self.trans.set_history(self.history_manager.create())
|
|
|
|
self.log('should be able to serialize anonymous user')
|
|
serialized = self.user_serializer.serialize_to_view(anonym, view='detailed', trans=self.trans)
|
|
self.assertKeys(serialized,
|
|
['id', 'total_disk_usage', 'nice_total_disk_usage', 'quota_percent'])
|
|
|
|
self.log('anonymous\'s id should be None')
|
|
self.assertEqual(serialized['id'], None)
|
|
self.log('everything serialized should be of the proper type')
|
|
self.assertIsInstance(serialized['total_disk_usage'], float)
|
|
self.assertIsInstance(serialized['nice_total_disk_usage'], str)
|
|
self.assertIsInstance(serialized['quota_percent'], (type(None), float))
|
|
|
|
self.log('serialized should jsonify well')
|
|
self.assertIsJsonifyable(serialized)
|
|
|
|
|
|
# =============================================================================
|
|
class UserDeserializerTestCase(BaseTestCase):
|
|
|
|
def set_up_managers(self):
|
|
super().set_up_managers()
|
|
self.deserializer = users.UserDeserializer(self.app)
|
|
|
|
def _assertRaises_and_return_raised(self, exception_class, fn, *args, **kwargs):
|
|
try:
|
|
fn(*args, **kwargs)
|
|
except exception_class as exception:
|
|
self.assertTrue(True)
|
|
return exception
|
|
raise AssertionError(f'{exception_class.__name__} not raised')
|
|
|
|
def test_username_validation(self):
|
|
user = self.user_manager.create(**user2_data)
|
|
|
|
# self.log( "usernames can be unicode" ) #TODO: nope they can't
|
|
# self.deserializer.deserialize( user, { 'username': 'Σίσυφος' }, trans=self.trans )
|
|
|
|
self.log("usernames must be long enough and with no non-hyphen punctuation")
|
|
exception = self._assertRaises_and_return_raised(base_manager.ModelDeserializingError,
|
|
self.deserializer.deserialize, user, {'username': 'ed'}, trans=self.trans)
|
|
self.assertTrue('Public name must be at least' in str(exception))
|
|
self.assertRaises(base_manager.ModelDeserializingError, self.deserializer.deserialize,
|
|
user, {'username': 'f,d,r,'}, trans=self.trans)
|
|
|
|
self.log("usernames must be unique")
|
|
self.user_manager.create(**user3_data)
|
|
self.assertRaises(base_manager.ModelDeserializingError, self.deserializer.deserialize,
|
|
user, {'username': 'user3'}, trans=self.trans)
|
|
|
|
self.log("username should be updatable")
|
|
new_name = 'double-plus-good'
|
|
self.deserializer.deserialize(user, {'username': new_name}, trans=self.trans)
|
|
self.assertEqual(self.user_manager.by_id(user.id).username, new_name)
|
|
|
|
|
|
# =============================================================================
|
|
class AdminUserFilterParserTestCase(BaseTestCase):
|
|
|
|
def set_up_managers(self):
|
|
super().set_up_managers()
|
|
self.filter_parser = users.AdminUserFilterParser(self.app)
|
|
|
|
def test_parsable(self):
|
|
self.log('the following filters should be parsable')
|
|
self.assertORMFilter(self.filter_parser.parse_filter('email', 'eq', 'wot'))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('email', 'contains', 'wot'))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('email', 'like', 'wot'))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('username', 'eq', 'wot'))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('username', 'contains', 'wot'))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('username', 'like', 'wot'))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('active', 'eq', True))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('disk_usage', 'le', 500000.00))
|
|
self.assertORMFilter(self.filter_parser.parse_filter('disk_usage', 'ge', 500000.00))
|
|
|
|
|
|
# =============================================================================
|
|
if __name__ == '__main__':
|
|
unittest.main()
|