Python Django - Passing in multiple parameters to Factory during testing - python

I'm currently working on a practice social media app. In this app, current users can invite their friends by email to join the app (specifically, joining a 'channel' of the app, like Discord). For this project, I'm working on functionality where a user will get an error if they try to invite someone who is already in the app (meaning people who are already in the app's database). I'm working on unit tests that ensures the error messages pop up when users are detected as already existing.
I managed to get my first scenario working, but I'm a bit stumped for the second one.
Here is a file that is central to both tests.
factories.py
class ChannelFactory(factory.django.DjangoModelFactory)
class Meta:
model = Channel
id = int
name = str
class CurrentUserFactory(factory.django.DjangoModelFactory)
class Meta:
model = CurrentUser
user_email = user_email
channel = models.ForeignKey(Channel)
Scenario #1 (currently working) - one new user is invited to join the app but already exists in the app's database
test_forms.py
from tests.factories import ChannelFactory, CurrentUserFactory
#pytest.mark.django_db
def test_that_current_user_cannot_be_reinvited(email, error_message):
"""user that is already in the specific channel cannot be reinvited"""
email = "user#test.com"
error_message = "user#test.com already exists in this channel"
# I am not specifying the channel name because the factory object is supposed to generate it automatically
current_user = CurrentUserFactory(user_email='user#test.com')
invite_form = forms.UserRequestForm({"email":email, channel=current_user.channel)
assert not invite_form is valid()
assert invite_form.errors["email"][0] = error_message
Result: Test passes!
However, the test passes mainly because there's only one user being tested.
So now, my task is to create a test to see what happens if several people are invited at once. The app allows for a comma separated string to be entered, so theoretically up to ten emails can be invited at a time.
Scenario #2 - two new users are invited to the same channel and both exist in the app's database. Here's where I'm running into issues because I need to somehow make sure the CurrentUsers are generated into the same channel.
from tests.factories import CurrentUserFactory
#pytest.mark.django_db
def tests_that_multiple_current_users_cannot_be_reinvited(emails, error_message):
"""users that are already in the specific channel cannot be reinvited"""
emails = ["user#test.com", "user2#test.com"]
error_message = "The following users already exist in this channel: user#test.com, user2#test.com"
#here, I attempt to "force" a Channel instance
channel = Channel(id="5", name="Hometown Friends")
current_users = [
(CurrentUserFactory(user_email='user#test.com', channel=channel)),
(CurrentUserFactory(user_email='user2#test.com', channel=channel)),
]
invite_form = forms.UserRequestForm({"email":emails, "channel":current_users.channel})
assert not invite_form is valid()
assert invite_form.errors["email"][0] = error_message
When I try running the test:
E AttributeError: 'tuple' object has no attribute 'channel'
I'm wondering if I can easily solve this by trying to work with the tuple, or if there's a much easier method that I'm somehow not seeing. Help would be greatly appreciated!

Related

How to access the actor of a django notification in a usable format?

I am using django-notifications (here is the code) to create notifications for my web app.
I have the issue of whenever I try to access the actor object e.g. via Notification.object.get(id=1).actor I get the following exception:
ValueError: Field 'id' expected a number but got '<property object at 0x7fc171cc5400>'.
which then causes then exception:
ValueError: invalid literal for int() with base 10: '<property object at 0x7fc171cc5400>'
Here is the code for my signal:
#receiver(post_save, sender=Request)
def notify_owner_of_request(sender, instance, created, **kwargs):
if created:
notify.send(
sender=sender,
actor = instance.user,
verb = "requested to join ",
recipient = instance.owner,
action_object = instance,
target = instance.club,
)
No matter what kind of object or value I make the actor it always has the same error.
To note, I can access action_object, target, verb, and recipient perfectly fine, and the notification does work (there is one made and correctly).
The Notification model from notification-hq has the following attributes:
actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor', on_delete=models.CASCADE)
actor_object_id = models.CharField(max_length=255)
actor = GenericForeignKey('actor_content_type', 'actor_object_id')
accessing actor_content_type gives me this:
<ContentType: clubs | request>
even though it should be of type User
and accessing actor_object_id gives me this:
'<property object at 0x7fc171cc5400>'
I need to access the actor so I can test it is the correct user and so that it is displayed in the front-end part of the notifications correctly.
Any help would be much appreciated!

Django TestCase client.logout() is not logging out user correctly

I'm writing some tests for Django. I'm having trouble with the client.logout() method, which doesn't seem to be logging the test client out.
Here's my setUp():
def setUp(self):
# Set up a user to use with all of the tests and log in the user.
User.objects.create_user(username='temporary', password='temporary')
self.client.login(username='temporary', password='temporary')
# Create second user for later use.
User.objects.create_user(username='temporary2', password='temporary2')
And here's my test method:
def test_user_can_only_access_own_recipes(self):
"""
A user should only be able to access recipes they have created.
"""
# temporary1 user already logged in via setUp().
user_1_recipe = self.create_recipe()
self.client.logout()
# Login second user and create recipe.
self.client.login(username='temporary2', password='temporary2')
user_2_recipe = self.create_recipe()
# Response_owned is a recipe made by user 2, who is still logged in, so the recipe should
be viewable.
# Response_not_owned is recipe made by user 1 and should not be viewable as they are
logged out.
response_owned = self.client.get(
reverse('recipes:display_recipe', args=(user_2_recipe.id,)))
response_not_owned = self.client.get(
reverse('recipes:display_recipe', args=(user_1_recipe.id,)))
self.assertEqual(response_not_owned.status_code, 403)
# Convert to str, otherwise Django compares 'temporary' with just temporary no quotes.
self.assertEqual(str(user_1_recipe.user), 'temporary')
self.assertEqual(user_2_recipe.user, 'temporary2')
self.assertEqual(response_owned.status_code, 200)
This fails with the assertion error:
self.assertEqual(user_2_recipe.user, 'temporary2')
AssertionError: <User: temporary> != 'temporary2'
So my test should create a recipe owned by user 1. User 1 should be logged out, and user 2 is then logged in, and a recipe is created owned by user 2. The test should check whether the second user can access their own recipe, and can't access the first user's recipe. Trouble is, only the first user seems to be logged in, as the assertion error states that the second recipe is still owned by the first user. I don't think logout() is logging out the first user at all. Any help with this would be greatly appreciated.
EDIT:
Please see the create_recipe() method below. After comments from Iain Shelvington it's become apparent that I had been adding the same user 'temporary' to every recipe created by the create_recipe method, by including this line:
user = User.objects.get(username='temporary'),
The amended method taking a username as an argument looks as follows, and the test can now be modified to allow the post to be from any user you like.
def create_recipe(self, recipe_name='Test Recipe', username='temporary'):
"""
Creates a recipe with default values and a modifiable name to be
used in tests.
"""
recipe = RecipeCard.objects.create(
user = User.objects.get(username=username),
# REMAINDER OF CODE
ORIGINAL CODE:
def create_recipe(self, recipe_name='Test Recipe'):
"""
Creates a recipe with default values and a modifiable name to be
used in tests.
"""
# Object is receiving default values that can be changed when method
# is used
recipe = RecipeCard.objects.create(
user = User.objects.get(username='temporary'),
recipe_name=recipe_name,
source = 'Test Source',
servings = 3,
active_time_hours = 2,
active_time_minutes = 15,
total_time_hours = 2,
total_time_minutes = 15,
recipe_description = 'Test Description')
recipe.save()
ingredient = Ingredient.objects.create(
ingredient_name = 'Test Ingredient',
quantity = 1,
recipe = recipe)
ingredient.save()
method = MethodStep.objects.create(
step = 'Test Step',
recipe = recipe)
method.save()
# Make sure you return the created recipe object for use when method
# is called in tests.
return recipe

sqlalchemy different children must exist

I'm trying to build a query which returns all objects which have children matching specified criteria. The trick is that there are multiple criteria which are mutually exclusive, so there must be multiple children. I'm not sure how to express this.
Model:
class Message(Base):
__tablename__ = 'Message'
class MessageRecipient(Base):
__tablename__ = 'MessageRecipient'
recipient_id = Column(Integer, ForeignKey('User.uid'))
message_id = Column(Integer, ForeignKey('Message.uid'))
user = relationship('User', backref="messages_received")
message = relationship('Message', backref="recipients")
I want to get all messages which are being sent to a defined set of users. For example, I want to return all messages which were sent to users 1 and 2, but not messages only sent to user 1 or messages only sent to user 2. It must have been sent to both users!
I was trying a query like the following:
query = Message.query.filter(Message.recipients.any(MessageRecipient.recipient_id.in_([1,2])))
The above doesn't work because in_ is disjunctive. It does return the messages I want, but it also returns messages I don't want.
Does anyone have an idea of how I can build a query which requires that a Message have MessageRecipients with an arbitrary set of ids?
I solved this by iterating over all objects in the collection and creating a new subquery for each using exists(). Not sure if this is the most efficient way, but it works.
for recipient in [1,2]:
query = query.filter(MessageRecipient.query.filter(and_(MessageRecipient.recipient_id== recipient,
MessageRecipient.message_id == Message.uid)).exists())

How to control field name for related model fetching in Peewee?

I have two models: User and Message. Each Message has two references to User (as sender and as receiver). Also I have defined an other_user hybrid method for Message which returns "user other than specific one" - see below:
from peewee import *
from playhouse.hybrid import hybrid_method
from playhouse.shortcuts import case
class User(Model):
name = CharField()
class Message(Model):
sender = ForeignKeyField(User, related_name='messages_sent')
receiver = ForeignKeyField(User, related_name='messages_received')
text = TextField()
#hybrid_method
def other_user(self, user):
if user == self.sender:
return self.receiver
elif user == self.receiver:
return self.sender
else:
raise ValueError
#other_user.expression
def other_user(cls, user):
return case(user.id, (
(cls.sender, cls.receiver),
(cls.receiver, cls.sender)))
Now I want to make a composite query which will retrieve all messages for current user and also retrieve information about "other" user than current. Here is how I do it:
current_user = request.user # don't matter how I retrieve it
query = (Message.select(Message, User)
.where(
(Message.sender == current_user) |
(Message.receiver == current_user))
.join(User, on=(User.id == Message.other_user(current_user))))
This query works well - i.e. it retrieves the exact information I need.
But here is the problem: "other user" information is always saved as sender field.
If I use this with models which have no direct ForeignKey reference then peewee creates a new field (in this case it would be named user) for additional requested model. But if there is at least one ForeignKey relationship from primary model to secondary requested model then it uses first such relationship.
Is it possible to somehow override this behaviour?
I tried Model.alias() method, but it (unlike Node.alias) doesn't allow to specify name.
I'm not completely sure what you want, so I'll provide a snippet that will likely work for your specific scenario, and will hopefully also allow you to learn how to do what you want, if this isn't it:
SenderUser = User.alias()
ReceiverUser = User.alias()
OtherUser = User.alias()
query = (Message.select(Message, SenderUser, ReceiverUser)
.where(
(Message.sender == current_user) |
(Message.receiver == current_user))
.join(SenderUser, on = (Message.sender == SenderUser.id).alias('sender'))
.switch(Message)
.join(ReceiverUser, on = (Message.receiver == ReceiverUser.id).alias('receiver'))
.switch(Message)
.join(OtherUser, on = (Message.other_user(current_user) == OtherUser.id).alias('other_user'))
Notes:
You don't really need to create all those aliases (SenderUser/ReceiverUser/OtherUser), just two, and use User for the other. I just find that the query becomes more readable like this.
When you define an alias in the on clause, you basically tell peewee in which variable to store the joined table. I'm sending them directly to the already existing properties (sender/receiver). Also, I'm creating an extra property in the model with the value of the other user, which you can access as usual with self.other_user.
That switch method switches the current context to Message, so you can join a new table to Message instead of the SenderUser/ReceiverUser contexts where you end up after the two first joins.
If for some reason you're joining something that might be undefined (which doesn't seem to be the case here as both users are likely mandatory), you would probably want to add that you want a left outer join, like this:
.join(ReceiverUser, JOIN.LEFT_OUTER, on = (Message.receiver == ReceiverUser.id).alias('receiver'))
Don't forget to from peewee import JOIN
Something else I just noticed, is that you likely want to change that other_user method you have to compare ids instead of the model variables. If self.sender is not filled when you access it, peewee will trigger a database select to get it, so your other_user method possibly triggers 2 select queries. I would do it like:
#hybrid_method
def other_user_id(self, user):
if user.id == self.sender_id:
return self.receiver_id
elif user.id == self.receiver_id:
return self.sender_id
else:
raise ValueError
You can see that I use sender_id instead of sender.id. That uses the ids for each foreign key that are already set in the message model. If you did self.receiver.id you would likely trigger that select anyway, to then access the id property (I'm not 100% sure here though).

Generating fixture data with Python's fixture module

I'm working with the fixture module for the first time, trying to get a better set of fixture data so I can make our functional tests more complete.
I'm finding the fixture module a bit clunky, and I'm hoping there's a better way to do what I'm doing. This is a Flask/SQLAlchemy app in Python 2.7, and we're using nose as a test runner.
So I have a set of employees. Employees have roles. There are a few pages with rather complex permissions, and I'd like to make sure those are tested.
I created a DataSet that has each type of role (there are about 15 roles in our app):
class EmployeeData(DataSet):
class Meta:
storable = Employee
class engineer:
username = "engineer"
role = ROLE_ENGINEER
class manager:
username = "manager"
role = ROLE_MANAGER
class admin:
username = "admin"
role = ROLE_ADMIN
and what I'd like to do is write a functional test that checks only the right people can access a page. (The actual permissions are way more complicated, I just wanted a toy example to show you.)
Something like this:
def test_only_admin_can_see_this_page():
for employee in Employee.query.all():
login(employee)
with self.app.test_request_context('/'):
response = self.test_client.get(ADMIN_PAGE)
if employee.role == ROLE_ADMIN
eq_(200, response.status_code)
else:
eq_(401, response.status_code)
logout(employee)
Is there a way to generate the fixture data so my devs don't have to remember to add a line to the fixtures every time we add a role? We have the canonical list of all roles as configuration elsewhere in the app, so I have that.
I'm not wedded to any of this or the fixture module, so I'm happy to hear suggestions!
An option would be to use factory_boy to create your test data.
Assuming that you keep and update accordingly a list of roles (that will be used later on) like this one:
roles = [ROLE_ENGINEER, ROLE_ADMIN, ROLE_MANAGER, ...]
Let's create a factory for the Employee table:
import factory
from somewhere.in.the.app import roles
class EmployeeFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Employee
sqlalchemy_session = session
username = factory.Sequence(lambda n: u'User %d' % n)
# Other attributes
...
# Now the role choice
role = factory.fuzzy.FuzzyChoice(roles)
The FuzzyChoice method takes a list of choices and makes a random choice from this list.
Now this will be able to create any amount of Employee objects on demand.
Using the factory:
from factory.location import EmployeeFactory
def test_only_admin_can_see_this_page():
EmployeeFactory.create_batch(size=100)
for employee in session.query(Employee).all():
login(employee)
with self.app.test_request_context('/'):
response = self.test_client.get(ADMIN_PAGE)
if employee.role == ROLE_ADMIN
eq_(200, response.status_code)
else:
eq_(401, response.status_code)
logout(employee)
Breakdown:
EmployeeFactory.create_batch(size=100) Creates 100 Employee objects in the test session.
We can access those objects from the factory session.
More information about using factory_boy with SQLAlchemy: https://factoryboy.readthedocs.io/en/latest/orms.html?highlight=sqlalchemy#sqlalchemy.
Be careful with session management especially: https://factoryboy.readthedocs.io/en/latest/orms.html?highlight=sqlalchemy#managing-sessions

Categories