sunscrapers / djoser

REST implementation of Django authentication system.
MIT License
2.53k stars 458 forks source link

How to write unit test (APITestCase) include activation email ! #678

Open kev26 opened 2 years ago

kev26 commented 2 years ago

My registeration follow : register --> get uid & token via email --> use ui & token for activation --> login--> accsess_token.

So if easily if I disable the feature SEND_ACTIVATION_EMAIL, but if I want to write a test with this feature, how to do it? And this is necessary for the test?

chapimenge3 commented 2 years ago

@kev26

My approach for this is i separate my settings file inside a folder like below image inside my setting file i put the setting file for production, development and test file. image

To explain the above file in the. Content of __init__.py image

i read the Environment file and decide which file to load.

The base.py contains the common setting configuration.

So here is the part where you can put your email activation class in the test.py like below.

image

soon we will write the class i mentioned in the above screenshot authentications.email.ActivationEmail

Now we are done with the setting part let's move to the email activation class where i put the class inside my authentications app and in email.py file but you can put it anywhere

So approach is to put all the email activations code in a dictionary at the run time and get the code from that dictionary.

FYI: this might be the dumbest approach but untill you get a workaround this might help.

let's continue

So in the authentications app email.py file create a class name called ActivationEmail

from django.contrib.auth.tokens import default_token_generator

# djoser imports
from templated_mail.mail import BaseEmailMessage
from djoser import utils
from djoser.conf import settings

EMAILS = {}

class ActivationEmail(BaseEmailMessage):
    """Email Activation Token Generator
    """
    template_name = "email/activation.html"

    def get_context_data(self):
        # ActivationEmail can be deleted
        context = super().get_context_data()
        user = context.get("user")
        context["uid"] = utils.encode_uid(user.pk)
        context["token"] = default_token_generator.make_token(user)
        context["url"] = settings.ACTIVATION_URL.format(**context)
        uid, token = context['uid'], context['token']
        EMAILS[user.email] = {'uid': uid, 'token': token}
        return context

So any in the testing file you can test the user creation like this

   def test_create_user(self):
        """
        Test for creating users using API.
        """
        url = reverse("user-list")
        response = self.client.post(
            url,
            self.user_info,
        )
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        user = User.objects.get(id=response.data['id'])
        self.assertEqual(user.email, self.user_info["email"])
        self.assertEqual(user.username, self.user_info["username"])
        # self.assertEqual(user.ssn, self.user_info["ssn"])
        self.assertTrue(user.password is not self.user_info["password"])
        self.assertTrue(user.is_deleted is not True)
        self.assertTrue(user.father_first_name is None)
        self.assertTrue(user.mother_first_name is None)
        self.assertTrue(user.password is not None)
        self.assertTrue(user.birth_date is not None)

    def test_get_token(self):
        """
        This test is used to test the login API. getting token and testing the token.
        """
        # Create a new user to login
        user_info = generate_user_info()
        new_user = self.client.post(
            reverse("user-list"),
            user_info,
        )
        self.assertEqual(new_user.status_code, status.HTTP_201_CREATED)

        # Activation of User
        from authentications.email import EMAILS

        activation_url = "http://127.0.0.1:8000/auth/users/activation/"
        activation_data = EMAILS[user_info["email"]]
        self.client.post(activation_url, activation_data)

        url = reverse("jwt-create")
        data = {
            "username": user_info["username"],
            "password": user_info["password"],
        }
        response = self.client.post(url, data)

        self.assertTrue(response.status_code, status.HTTP_200_OK)
        self.assertTrue(response.data["access"] is not None)

hope this helps but if anyone finds a better approach i would love to see it.

Enjoy.

saaz181 commented 2 years ago

Actually when testing to create account (user sign-up) it does NOT send activation email so it doesn't save any uid and token in EMAIL dict in email.py file

chapimenge3 commented 2 years ago

So when does the email is sent ? @saaz181

saaz181 commented 2 years ago

So when does the email is sent ? @saaz181

I set up my settings like you explained but it doesn't send any email it throws keyError on test_get_token function on: activation_data = EMAILS[self.user_info["email"]] so I printed out the EMAILS dict and it was an empty dict actually my user_info is like:

self.user_info = {
            "username": self.username,
            "phone": self.phone,
            "email": self.email,
            "password": self.password,
            "re_password": self.password,
        }
chapimenge3 commented 2 years ago

Perhaps you forget to change the environment variable.

Or you can do like this if you want

ENVIROMENT=test python manage.py test

Make you sure you run on the test enviroment becausethe setting wont be able to load the test setting config. you can check by adding print value on the settings/test.py if your print actually prints when you run the test it might be other problem.

@saaz181

skonik commented 2 years ago

I don't disable SEND_ACTIVATION_EMAIL. Just use regex to pull uid and token out of email and do POST request.

Here's my example using pytest:


@pytest.fixture
def email_activation_url():
    return reverse('user-activation')

@pytest.mark.django_db
def test_email_activation(
        api_client: APIClient,
        registration_url: str,
        email_activation_url: str,

):
    expected_user = dict(
        email="tester@gmail.com",
        username="tester",
        password="superTester1sPass.",
    )

    response: Response = api_client.post(
        path=registration_url,
        data=expected_user,
    )
    assert response.status_code == status.HTTP_201_CREATED, response.data

    user = UserModel.objects.filter(email=expected_user['email']).first()
    assert user.is_active is False

    assert len(mail.outbox) == 1

    first_email = mail.outbox[0]

    # Check that we received activation email 
    activation_email_re = re.compile(
        r'.*Please go to the following page to activate account:.*',
        re.I + re.M,
    )
    activation_re_result = activation_email_re.search(first_email.body)
    assert activation_re_result is not None

    # regex template is based on string '#/activate/{uid}/{token}'. Check Djoser Settings
    activation_url_template = r'/activate/(?P<uid>.*)/(?P<token>.*)[\"\n>]'

    activation_url_re = re.search(
        activation_url_template,
        first_email.body,
        re.U + re.I + re.M,
    )
    assert activation_url_re is not None

    uid = activation_url_re.groupdict().get('uid')
    assert uid is not None

    token = activation_url_re.groupdict().get('token')
    assert token is not None

    response = api_client.post(
        path=email_activation_url,
        data={
            'uid': uid,
            'token': token
        }
    )

    assert response.status_code == status.HTTP_204_NO_CONTENT, response.data

    user.refresh_from_db()
    assert user.is_active is True