diff --git a/src/postorius/forms.py b/src/postorius/forms.py index b4328d6..b25458e 100644 --- a/src/postorius/forms.py +++ b/src/postorius/forms.py @@ -20,7 +20,16 @@ from django.core.validators import validate_email from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User + from postorius.fieldset_forms import FieldsetForm +from postorius.models import AddressConfirmationProfile +from postorius import utils + +try: + from urllib2 import HTTPError +except ImportError: + from urllib.error import HTTPError ACTION_CHOICES = ( @@ -754,16 +763,24 @@ class AddressActivationForm(forms.Form): email = forms.EmailField() - user_email = forms.EmailField(widget=forms.HiddenInput) - def clean(self): - cleaned_data = super(AddressActivationForm, self).clean() - email = cleaned_data.get('email') - user_email = cleaned_data.get('user_email') - if email == user_email: - raise forms.ValidationError(_('Please provide a different email ' - 'address than your own.')) - return cleaned_data + def clean_email(self): + email = self.cleaned_data.get('email') + + # Check if the address belongs to someone else + if User.objects.filter(email=email).exists(): + raise forms.ValidationError(_('This email is in use.' + 'Please choose another or contact the administrator'), + 'error') + + # Check if the email is attached to a user in Mailman + try: + utils.get_client().get_user(email) + raise forms.ValidationError(_('This email already belongs to a user'), 'error') + except HTTPError: + pass + return email + class ChangeSubscriptionForm(forms.Form): email = forms.ChoiceField() diff --git a/src/postorius/migrations/0002_auto_20160209_0635.py b/src/postorius/migrations/0002_auto_20160209_0635.py new file mode 100644 index 0000000..34af0cc --- /dev/null +++ b/src/postorius/migrations/0002_auto_20160209_0635.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('postorius', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='addressconfirmationprofile', + name='created', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/src/postorius/models.py b/src/postorius/models.py index 043c2bc..a974e43 100644 --- a/src/postorius/models.py +++ b/src/postorius/models.py @@ -257,7 +257,7 @@ """ email = models.EmailField() activation_key = models.CharField(max_length=40) - created = models.DateTimeField() + created = models.DateTimeField(auto_now=True) user = models.ForeignKey(User) objects = AddressConfirmationProfileManager() diff --git a/src/postorius/templates/postorius/menu/user_nav.html b/src/postorius/templates/postorius/menu/user_nav.html index 94054ac..c139547 100644 --- a/src/postorius/templates/postorius/menu/user_nav.html +++ b/src/postorius/templates/postorius/menu/user_nav.html @@ -5,7 +5,6 @@
{% trans "A confirmation link has been sent to the email address you submitted. Please check your email account and click on the confirmation link to add this address for your account." %}
-{% endblock main %} diff --git a/src/postorius/templates/postorius/user/profile.html b/src/postorius/templates/postorius/user/profile.html index c6e99b3..667f323 100644 --- a/src/postorius/templates/postorius/user/profile.html +++ b/src/postorius/templates/postorius/user/profile.html @@ -1,5 +1,6 @@ {% extends postorius_base_template %} {% load i18n %} +{% load bootstrap_tags %} {% load nav_helpers %} {% block subtitle %} @@ -34,5 +35,14 @@ - ++ {% blocktrans %} + You can add other addresses to your profile, + to use different addresses for your subscriptions. + {% endblocktrans %} +
+ {% endblock main %} diff --git a/src/postorius/tests/fixtures/vcr_cassettes/test_address_activation.yaml b/src/postorius/tests/fixtures/vcr_cassettes/test_address_activation.yaml new file mode 100644 index 0000000..2746eb2 --- /dev/null +++ b/src/postorius/tests/fixtures/vcr_cassettes/test_address_activation.yaml @@ -0,0 +1,161 @@ +interactions: +- request: + body: email=subscribed%40example.org&password=password + headers: + accept-encoding: ['gzip, deflate'] + !!python/unicode content-type: [!!python/unicode application/x-www-form-urlencoded] + method: !!python/unicode POST + uri: http://localhost:9001/3.0/users + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + location: ['http://localhost:9001/3.0/users/1881'] + status: {code: 201, message: Created} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode DELETE + uri: http://localhost:9001/3.0/users/1881 + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + status: {code: 204, message: No Content} +- request: + body: email=subscribed%40example.org&password=password + headers: + accept-encoding: ['gzip, deflate'] + !!python/unicode content-type: [!!python/unicode application/x-www-form-urlencoded] + method: !!python/unicode POST + uri: http://localhost:9001/3.0/users + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + location: ['http://localhost:9001/3.0/users/1882'] + status: {code: 201, message: Created} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/expired@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode DELETE + uri: http://localhost:9001/3.0/users/1882 + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + status: {code: 204, message: No Content} +- request: + body: email=subscribed%40example.org&password=password + headers: + accept-encoding: ['gzip, deflate'] + !!python/unicode content-type: [!!python/unicode application/x-www-form-urlencoded] + method: !!python/unicode POST + uri: http://localhost:9001/3.0/users + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + location: ['http://localhost:9001/3.0/users/1883'] + status: {code: 201, message: Created} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/subscribed@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"aef2be6373d039f3c26769f3eb033b21185c8817\"", "is_server_owner": false, + "password": "$6$rounds=631015$2Z59WghziNfKCrWC$JuV8ayS7XVdOnPzYOKn4qTxm8XlpL.aDYHhcHDo3.mvkC473gjLEpEObyzt4g51oPp0M/BiZ2k.vyY8DWX9U01", + "self_link": "http://localhost:9001/3.0/users/1883", "user_id": 1883}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode DELETE + uri: http://localhost:9001/3.0/users/1883 + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + status: {code: 204, message: No Content} +- request: + body: email=subscribed%40example.org&password=password + headers: + accept-encoding: ['gzip, deflate'] + !!python/unicode content-type: [!!python/unicode application/x-www-form-urlencoded] + method: !!python/unicode POST + uri: http://localhost:9001/3.0/users + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + location: ['http://localhost:9001/3.0/users/1884'] + status: {code: 201, message: Created} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode DELETE + uri: http://localhost:9001/3.0/users/1884 + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + status: {code: 204, message: No Content} +- request: + body: email=subscribed%40example.org&password=password + headers: + accept-encoding: ['gzip, deflate'] + !!python/unicode content-type: [!!python/unicode application/x-www-form-urlencoded] + method: !!python/unicode POST + uri: http://localhost:9001/3.0/users + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + location: ['http://localhost:9001/3.0/users/1885'] + status: {code: 201, message: Created} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/very_new_email@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode DELETE + uri: http://localhost:9001/3.0/users/1885 + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + status: {code: 204, message: No Content} +version: 1 diff --git a/src/postorius/tests/fixtures/vcr_cassettes/test_user_profile.yaml b/src/postorius/tests/fixtures/vcr_cassettes/test_user_profile.yaml new file mode 100644 index 0000000..7afa15b --- /dev/null +++ b/src/postorius/tests/fixtures/vcr_cassettes/test_user_profile.yaml @@ -0,0 +1,366 @@ +interactions: +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: email=les%40example.org + headers: + accept-encoding: ['gzip, deflate'] + !!python/unicode content-type: [!!python/unicode application/x-www-form-urlencoded] + method: !!python/unicode POST + uri: http://localhost:9001/3.0/users + response: + body: {string: !!python/unicode ''} + headers: + content-length: ['0'] + location: ['http://localhost:9001/3.0/users/1526'] + status: {code: 201, message: Created} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/new_address@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526 + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526/addresses + response: + body: {string: !!python/unicode '{"entries": [{"email": "les@example.org", "http_etag": + "\"bc85297a34f76fa60759c2b4f15035084262016c\"", "original_email": "les@example.org", + "registered_on": "2005-08-01T07:49:23", "self_link": "http://localhost:9001/3.0/addresses/les@example.org", + "user": "http://localhost:9001/3.0/users/1526"}], "http_etag": "\"a1cb0979c0e9dbb5d2eff3b5fba580b0c1811388\"", + "start": 0, "total_size": 1}'} + headers: + content-length: ['387'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526/addresses + response: + body: {string: !!python/unicode '{"entries": [{"email": "les@example.org", "http_etag": + "\"bc85297a34f76fa60759c2b4f15035084262016c\"", "original_email": "les@example.org", + "registered_on": "2005-08-01T07:49:23", "self_link": "http://localhost:9001/3.0/addresses/les@example.org", + "user": "http://localhost:9001/3.0/users/1526"}], "http_etag": "\"a1cb0979c0e9dbb5d2eff3b5fba580b0c1811388\"", + "start": 0, "total_size": 1}'} + headers: + content-length: ['387'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/new_address@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526/addresses + response: + body: {string: !!python/unicode '{"entries": [{"email": "les@example.org", "http_etag": + "\"bc85297a34f76fa60759c2b4f15035084262016c\"", "original_email": "les@example.org", + "registered_on": "2005-08-01T07:49:23", "self_link": "http://localhost:9001/3.0/addresses/les@example.org", + "user": "http://localhost:9001/3.0/users/1526"}], "http_etag": "\"a1cb0979c0e9dbb5d2eff3b5fba580b0c1811388\"", + "start": 0, "total_size": 1}'} + headers: + content-length: ['387'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526/addresses + response: + body: {string: !!python/unicode '{"entries": [{"email": "les@example.org", "http_etag": + "\"bc85297a34f76fa60759c2b4f15035084262016c\"", "original_email": "les@example.org", + "registered_on": "2005-08-01T07:49:23", "self_link": "http://localhost:9001/3.0/addresses/les@example.org", + "user": "http://localhost:9001/3.0/users/1526"}], "http_etag": "\"a1cb0979c0e9dbb5d2eff3b5fba580b0c1811388\"", + "start": 0, "total_size": 1}'} + headers: + content-length: ['387'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/new_address@example.org + response: + body: {string: !!python/unicode 404 Not Found} + headers: + content-length: ['13'] + content-type: [application/json; charset=utf-8] + status: {code: 404, message: Not Found} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526/addresses + response: + body: {string: !!python/unicode '{"entries": [{"email": "les@example.org", "http_etag": + "\"bc85297a34f76fa60759c2b4f15035084262016c\"", "original_email": "les@example.org", + "registered_on": "2005-08-01T07:49:23", "self_link": "http://localhost:9001/3.0/addresses/les@example.org", + "user": "http://localhost:9001/3.0/users/1526"}], "http_etag": "\"a1cb0979c0e9dbb5d2eff3b5fba580b0c1811388\"", + "start": 0, "total_size": 1}'} + headers: + content-length: ['387'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/1526/addresses + response: + body: {string: !!python/unicode '{"entries": [{"email": "les@example.org", "http_etag": + "\"bc85297a34f76fa60759c2b4f15035084262016c\"", "original_email": "les@example.org", + "registered_on": "2005-08-01T07:49:23", "self_link": "http://localhost:9001/3.0/addresses/les@example.org", + "user": "http://localhost:9001/3.0/users/1526"}], "http_etag": "\"a1cb0979c0e9dbb5d2eff3b5fba580b0c1811388\"", + "start": 0, "total_size": 1}'} + headers: + content-length: ['387'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +- request: + body: null + headers: + accept-encoding: ['gzip, deflate'] + method: !!python/unicode GET + uri: http://localhost:9001/3.0/users/les@example.org + response: + body: {string: !!python/unicode '{"created_on": "2005-08-01T07:49:23", "http_etag": + "\"ce66943f3314f292f4fb48ae1e5cbe7004c6c1a3\"", "is_server_owner": false, + "password": "$6$rounds=627500$sP7o2jMJBoFzc9qN$hgtxLB1aByTHjY5RvLo3xAWLB2U0GoSEN6Ig9ucya/O35fxyVY3U53WoauYuexoaq4L35pqqCVz4EJkQKXK9F1", + "self_link": "http://localhost:9001/3.0/users/1526", "user_id": 1526}'} + headers: + content-length: ['330'] + content-type: [application/json; charset=utf-8] + status: {code: 200, message: OK} +version: 1 diff --git a/src/postorius/tests/mailman_api_tests/test_address_activation.py b/src/postorius/tests/mailman_api_tests/test_address_activation.py new file mode 100644 index 0000000..d67ae6e --- /dev/null +++ b/src/postorius/tests/mailman_api_tests/test_address_activation.py @@ -0,0 +1,196 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import logging +from datetime import datetime, timedelta +from django.contrib.auth.models import User +from django.contrib.messages.storage.fallback import FallbackStorage +from django.core.urlresolvers import reverse +from django.core import mail +from django.test.client import Client, RequestFactory +from django.test.utils import override_settings +from django.test import TestCase +from mock import patch, call, Mock + +from postorius.forms import AddressActivationForm +from postorius.models import AddressConfirmationProfile +from postorius import views +from postorius.views.user import address_activation_link +from postorius.tests import MM_VCR +from postorius.utils import get_client + + +vcr_log = logging.getLogger('vcr') +vcr_log.setLevel(logging.WARNING) + +class TestAddressActivationForm(TestCase): + """ + Test the activation form. + """ + + @MM_VCR.use_cassette('test_address_activation.yaml') + def setUp(self): + # Create a user and profile. + self.user = User.objects.create_user('testuser', 'les@example.org', 'testpass') + self.profile = AddressConfirmationProfile.objects.create_profile('les2@example.org', + self.user) + self.expired = AddressConfirmationProfile.objects.create_profile('expired@example.org', + self.user) + self.expired.created -= timedelta(weeks=100) + self.expired.save() + self.mm_user = get_client().create_user('subscribed@example.org', 'password') + + @MM_VCR.use_cassette('test_address_activation.yaml') + def tearDown(self): + self.profile.delete() + self.expired.delete() + self.user.delete() + self.mm_user.delete() + + @MM_VCR.use_cassette('test_address_activation.yaml') + def test_valid_email_is_valid(self): + form = AddressActivationForm({'email': 'very_new_email@example.org',}) + self.assertTrue(form.is_valid()) + + def test_email_used_by_django_auth_is_invalid(self): + # No need for cassette becuase we should check mailman last since it's the most expensive + form = AddressActivationForm({'email': 'les@example.org',}) + self.assertFalse(form.is_valid()) + + def test_invalid_email_is_not_valid(self): + # No need for cassette becuase we should check mailman last since it's the most expensive + form = AddressActivationForm({'email': 'les@example',}) + self.assertFalse(form.is_valid()) + + @MM_VCR.use_cassette('test_address_activation.yaml') + def test_email_used_by_expired_confirmation_profile_is_valid(self): + form = AddressActivationForm({'email': 'expired@example.org',}) + self.assertTrue(form.is_valid()) + + @MM_VCR.use_cassette('test_address_activation.yaml') + def test_email_used_by_mailman_is_invalid(self): + form = AddressActivationForm({'email': 'subscribed@example.org',}) + self.assertFalse(form.is_valid()) + + +class TestAddressConfirmationProfile(TestCase): + """ + Test the confirmation of an email address activation (validating token, + expiration, Mailman API calls etc.). + """ + + def setUp(self): + # Create a user and profile. + self.user = User.objects.create_user( + username=u'ler_mm', email=u'ler@mailman.mostdesirable.org', + password=u'pwd') + self.profile = AddressConfirmationProfile.objects.create_profile( + u'les@example.org', self.user) + # Create a test request object + self.request = RequestFactory().get('/') + + def tearDown(self): + self.profile.delete() + self.user.delete() + mail.outbox = [] + + def test_profile_creation(self): + # Profile is created and has all necessary properties. + self.assertEqual(self.profile.email, u'les@example.org') + self.assertEqual(len(self.profile.activation_key), 40) + self.assertTrue(type(self.profile.created), datetime) + + def test_no_duplicate_profiles(self): + # Creating a new profile returns an existing updated record + # (if one exists), instead of creating a new one. + new_profile = AddressConfirmationProfile.objects.create_profile( + u'les@example.org', + User.objects.create(email=u'ler@mailman.mostdesirable.org')) + self.assertEqual(new_profile.user, self.profile.user) + self.assertEqual(new_profile.email, self.profile.email) + self.assertNotEqual(new_profile.created, self.profile.created) + self.assertNotEqual(new_profile.activation_key, self.profile.activation_key) + + def test_unicode_representation(self): + # Correct unicode representation? + self.assertEqual(unicode(self.profile), + 'Address Confirmation Profile for les@example.org') + + def test_profile_not_expired_default_setting(self): + # A profile created less then a day ago is not expired by default. + delta = timedelta(hours=23) + now = datetime.now() + self.profile.created = now - delta + self.assertFalse(self.profile.is_expired) + + def test_profile_is_expired_default_setting(self): + # A profile older than 1 day is expired by default. + delta = timedelta(days=1, hours=1) + now = datetime.now() + self.profile.created = now - delta + self.assertTrue(self.profile.is_expired) + + @override_settings( + EMAIL_CONFIRMATION_EXPIRATION_DELTA=timedelta(hours=5)) + def test_profile_not_expired(self): + # A profile older than the timedelta set in the settings is + # expired. + delta = timedelta(hours=6) + now = datetime.now() + self.profile.created = now - delta + self.assertTrue(self.profile.is_expired) + + @override_settings( + EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend', + EMAIL_CONFIRMATION_FROM='mailman@mostdesirable.org') + def test_confirmation_link(self): + # The profile obj can send out a confirmation email. + # set the activation key to a fixed string for testing + self.profile.activation_key = \ + '6323fba0097781fdb887cfc37a1122ee7c8bb0b0' + # Simulate a VirtualHost with a different name + self.request.META["HTTP_HOST"] = "another-virtualhost" + # Now send the email + self.profile.send_confirmation_link(self.request) + self.assertEqual(mail.outbox[0].to[0], u'les@example.org') + self.assertEqual(mail.outbox[0].subject, u'Confirmation needed') + self.assertIn(self.profile.activation_key, mail.outbox[0].body) + self.assertIn("another-virtualhost", mail.outbox[0].body) + + +class TestAddressActivationLinkSuccess(TestCase): + """ + This tests the activation link view if the key is valid and the profile is + not expired. + """ + + def setUp(self): + # Set up a profile with a predictable key + self.user = User.objects.create_user( + username='ler', email=u'ler@example.org', + password='pwd') + self.profile = AddressConfirmationProfile.objects.create_profile( + u'les@example.org', self.user) + self.profile.activation_key = \ + u'6323fba0097781fdb887cfc37a1122ee7c8bb0b0' + self.profile.save() + + def tearDown(self): + self.profile.delete() + self.user.delete() + + @patch.object(views.user, '_add_address') + def test_mailman(self, _add_address_mock): + # An activation key pointing to a valid profile adds the address + # to the user. + request = RequestFactory().get(reverse( + 'address_activation_link', kwargs={ + 'activation_key': '6323fba0097781fdb887cfc37a1122ee7c8bb0b0'})) + setattr(request, 'session', 'session') + messages = FallbackStorage(request) + setattr(request, '_messages', messages) + address_activation_link( + request, '6323fba0097781fdb887cfc37a1122ee7c8bb0b0') + expected_calls = [call(request, u'ler@example.org', + u'les@example.org')] + self.assertEqual(_add_address_mock.mock_calls, expected_calls) diff --git a/src/postorius/tests/mailman_api_tests/test_profile.py b/src/postorius/tests/mailman_api_tests/test_profile.py new file mode 100644 index 0000000..cd79a80 --- /dev/null +++ b/src/postorius/tests/mailman_api_tests/test_profile.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2012-2015 by the Free Software Foundation, Inc. +# +# This file is part of Postorius. +# +# Postorius is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# Postorius is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Postorius. If not, see