summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorthaissiqueira <thais.siqueira@thoughtworks.com>2017-03-31 18:00:43 -0300
committerGitHub <noreply@github.com>2017-03-31 18:00:43 -0300
commitf40808a147d1135e8bbee6d78306a598cf5ca647 (patch)
tree58d15414134ef5554e947348179e92e248df433b
parentdadf39c1574d53612410371b8b639a159841eaf5 (diff)
parent304215fe36666c8401bd9fa01a50cab61ddc10ca (diff)
Merge pull request #1040 from pixelated/email-recovery-code
[#927] Implements sending recovery code by email.
-rw-r--r--service/pixelated/account_recovery.py35
-rw-r--r--service/pixelated/resources/backup_account_resource.py9
-rw-r--r--service/test/unit/resources/test_backup_account_resource.py16
-rw-r--r--service/test/unit/test_account_recovery.py53
-rw-r--r--web-ui/src/backup_account/backup_email/backup_email.js12
-rw-r--r--web-ui/src/backup_account/backup_email/backup_email.spec.js20
6 files changed, 124 insertions, 21 deletions
diff --git a/service/pixelated/account_recovery.py b/service/pixelated/account_recovery.py
index 234bb1fe..c0a18792 100644
--- a/service/pixelated/account_recovery.py
+++ b/service/pixelated/account_recovery.py
@@ -13,23 +13,52 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.logger import Logger
+from twisted.mail import smtp
+
+from email.mime.text import MIMEText
+
log = Logger()
class AccountRecovery(object):
- def __init__(self, session, soledad):
+ def __init__(self, session, soledad, smtp_config, backup_email):
self._bonafide_session = session
self._soledad = soledad
+ self._smtp_config = smtp_config
+ self._backup_email = backup_email
@inlineCallbacks
def update_recovery_code(self):
try:
code = self._soledad.create_recovery_code()
response = yield self._bonafide_session.update_recovery_code(code)
+ yield self._send_mail(code, self._backup_email)
+
returnValue(response)
+
+ except Exception as e:
+ log.error('Something went wrong when trying to save the recovery code')
+ log.error(e)
+ raise e
+
+ @inlineCallbacks
+ def _send_mail(self, code, backup_email):
+ msg = MIMEText('Your code %s' % code)
+ msg['Subject'] = 'Recovery Code'
+ msg['From'] = 'team@pixelated-project.org'
+ msg['To'] = backup_email
+
+ try:
+ send_mail_result = yield smtp.sendmail(
+ str(self._smtp_config.remote_smtp_host),
+ 'team@pixelated-project.org',
+ [backup_email],
+ msg.as_string())
+ returnValue(send_mail_result)
except Exception as e:
- log.warn('Something went wrong when trying to save the recovery code')
- raise
+ log.error('Failed trying to send the email with the recovery code')
+ raise e
diff --git a/service/pixelated/resources/backup_account_resource.py b/service/pixelated/resources/backup_account_resource.py
index b752b4c7..f51ac2ec 100644
--- a/service/pixelated/resources/backup_account_resource.py
+++ b/service/pixelated/resources/backup_account_resource.py
@@ -15,6 +15,8 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import os
+import json
+
from xml.sax import SAXParseException
from pixelated.resources import BaseResource
@@ -51,7 +53,9 @@ class BackupAccountResource(BaseResource):
def render_POST(self, request):
account_recovery = AccountRecovery(
self._authenticator.bonafide_session,
- self.soledad(request))
+ self.soledad(request),
+ self._service(request, '_leap_session').smtp_config,
+ self._get_backup_email(request))
def update_response(response):
request.setResponseCode(NO_CONTENT)
@@ -64,3 +68,6 @@ class BackupAccountResource(BaseResource):
d = account_recovery.update_recovery_code()
d.addCallbacks(update_response, error_response)
return NOT_DONE_YET
+
+ def _get_backup_email(self, request):
+ return json.loads(request.content.getvalue()).get('backupEmail')
diff --git a/service/test/unit/resources/test_backup_account_resource.py b/service/test/unit/resources/test_backup_account_resource.py
index 2b68dd1b..e5e2793a 100644
--- a/service/test/unit/resources/test_backup_account_resource.py
+++ b/service/test/unit/resources/test_backup_account_resource.py
@@ -50,12 +50,16 @@ class TestBackupAccountResource(unittest.TestCase):
mock_account_recovery.update_recovery_code.return_value = defer.succeed("Success")
request = DummyRequest(['/backup-account'])
request.method = 'POST'
+ request.content = MagicMock()
+ request.content.getvalue.return_value = '{"email": "test@test.com"}'
d = self.web.get(request)
def assert_update_recovery_code_called(_):
mock_account_recovery_init.assert_called_with(
self.resource._authenticator.bonafide_session,
- self.resource.soledad(request))
+ self.resource.soledad(request),
+ self.resource._service(request, '_leap_session').smtp_config,
+ self.resource._get_backup_email(request))
mock_account_recovery.update_recovery_code.assert_called()
d.addCallback(assert_update_recovery_code_called)
@@ -66,6 +70,8 @@ class TestBackupAccountResource(unittest.TestCase):
mock_update_recovery_code.return_value = defer.succeed("Success")
request = DummyRequest(['/backup-account'])
request.method = 'POST'
+ request.content = MagicMock()
+ request.content.getvalue.return_value = '{"email": "test@test.com"}'
d = self.web.get(request)
def assert_successful_response(_):
@@ -79,6 +85,8 @@ class TestBackupAccountResource(unittest.TestCase):
mock_update_recovery_code.return_value = defer.fail(Exception)
request = DummyRequest(['/backup-account'])
request.method = 'POST'
+ request.content = MagicMock()
+ request.content.getvalue.return_value = '{"email": "test@test.com"}'
d = self.web.get(request)
def assert_successful_response(_):
@@ -86,3 +94,9 @@ class TestBackupAccountResource(unittest.TestCase):
d.addCallback(assert_successful_response)
return d
+
+ def test_get_backup_email_from_request(self):
+ request = MagicMock()
+ request.content.getvalue.return_value = '{"backupEmail": "test@test.com"}'
+
+ self.assertEqual(self.resource._get_backup_email(request), 'test@test.com')
diff --git a/service/test/unit/test_account_recovery.py b/service/test/unit/test_account_recovery.py
index af14814a..f113169a 100644
--- a/service/test/unit/test_account_recovery.py
+++ b/service/test/unit/test_account_recovery.py
@@ -13,24 +13,55 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from email.mime.text import MIMEText
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet import defer
from twisted.trial import unittest
+from twisted.mail import smtp
-from mock import patch, Mock
+from mock import patch, Mock, MagicMock
+from mockito import mock, when, any as ANY
from pixelated.account_recovery import AccountRecovery
class AccountRecoveryTest(unittest.TestCase):
+ def setUp(self):
+ self.generated_code = '4645a2f8997e5d0d'
+ self.mock_bonafide_session = Mock()
+ self.mock_soledad = Mock()
+ self.mock_smtp_config = Mock()
+ self.keymanager = Mock()
+ self.mock_smtp_config.remote_smtp_host = 'test.com'
+ self.mock_soledad.create_recovery_code.return_value = self.generated_code
+ self.backup_email = 'test@test.com'
+ self.account_recovery = AccountRecovery(
+ self.mock_bonafide_session,
+ self.mock_soledad,
+ self.mock_smtp_config,
+ self.backup_email)
+ self.mock_smtp = Mock()
- @inlineCallbacks
+ @defer.inlineCallbacks
def test_update_recovery_code(self):
- generated_code = '4645a2f8997e5d0d'
- mock_bonafide_session = Mock()
- mock_soledad = Mock()
- mock_soledad.create_recovery_code.return_value = generated_code
- account_recovery = AccountRecovery(mock_bonafide_session, mock_soledad)
-
- yield account_recovery.update_recovery_code()
- mock_bonafide_session.update_recovery_code.assert_called_once_with(generated_code)
+ when(self.account_recovery)._send_mail(ANY).thenReturn(defer.succeed(None))
+ response = yield self.account_recovery.update_recovery_code()
+ self.mock_bonafide_session.update_recovery_code.assert_called_once_with(self.generated_code)
+
+ @defer.inlineCallbacks
+ def test_send_recovery_code_by_email(self):
+ msg = MIMEText('Your code %s' % self.generated_code)
+ msg['Subject'] = 'Recovery Code'
+ msg['From'] = 'team@pixelated-project.org'
+ msg['To'] = self.backup_email
+
+ result = MagicMock()
+ deferred_sendmail = defer.succeed(result)
+ with patch.object(smtp, 'sendmail', return_value=deferred_sendmail) as mock_sendmail:
+ response = yield self.account_recovery._send_mail(self.generated_code, self.backup_email)
+
+ mock_sendmail.assert_called_with(
+ 'test.com',
+ 'team@pixelated-project.org',
+ [self.backup_email],
+ msg.as_string())
diff --git a/web-ui/src/backup_account/backup_email/backup_email.js b/web-ui/src/backup_account/backup_email/backup_email.js
index 9d622d8d..8fa71191 100644
--- a/web-ui/src/backup_account/backup_email/backup_email.js
+++ b/web-ui/src/backup_account/backup_email/backup_email.js
@@ -31,7 +31,7 @@ export class BackupEmail extends React.Component {
constructor(props) {
super(props);
- this.state = { error: '', submitButtonDisabled: true };
+ this.state = { error: '', submitButtonDisabled: true, backupEmail: '' };
}
validateEmail = (event) => {
@@ -54,7 +54,8 @@ export class BackupEmail extends React.Component {
'Content-Type': 'application/json'
},
body: JSON.stringify({
- csrftoken: [browser.getCookie('XSRF-TOKEN')]
+ csrftoken: [browser.getCookie('XSRF-TOKEN')],
+ backupEmail: this.state.backupEmail
})
}).then((response) => {
if (response.ok) {
@@ -65,6 +66,11 @@ export class BackupEmail extends React.Component {
});
};
+ handleChange = (event) => {
+ this.setState({ backupEmail: event.target.value });
+ this.validateEmail(event);
+ }
+
render() {
const t = this.props.t;
return (
@@ -78,7 +84,7 @@ export class BackupEmail extends React.Component {
<h1>{t('backup-account.backup-email.title')}</h1>
<p>{t('backup-account.backup-email.paragraph1')}</p>
<p>{t('backup-account.backup-email.paragraph2')}</p>
- <InputField name='email' label={t('backup-account.backup-email.input-label')} errorText={this.state.error} onChange={this.validateEmail} />
+ <InputField name='email' value={this.state.backupEmail} label={t('backup-account.backup-email.input-label')} errorText={this.state.error} onChange={this.handleChange} />
<SubmitButton buttonText={t('backup-account.backup-email.button')} disabled={this.state.submitButtonDisabled} />
<BackLink
href='/'
diff --git a/web-ui/src/backup_account/backup_email/backup_email.spec.js b/web-ui/src/backup_account/backup_email/backup_email.spec.js
index 65fad608..b23aadaa 100644
--- a/web-ui/src/backup_account/backup_email/backup_email.spec.js
+++ b/web-ui/src/backup_account/backup_email/backup_email.spec.js
@@ -9,6 +9,7 @@ describe('BackupEmail', () => {
let backupEmail;
let mockOnSubmit;
let mockTranslations;
+ let backupEmailInstance;
beforeEach(() => {
mockOnSubmit = expect.createSpy();
@@ -30,8 +31,6 @@ describe('BackupEmail', () => {
});
describe('Email validation', () => {
- let backupEmailInstance;
-
beforeEach(() => {
backupEmailInstance = backupEmail.instance();
});
@@ -84,6 +83,17 @@ describe('BackupEmail', () => {
});
});
+ describe('Email changing handler', () => {
+ beforeEach(() => {
+ backupEmailInstance = backupEmail.instance();
+ });
+
+ it('sets user backup email in the state', () => {
+ backupEmailInstance.handleChange({ target: { value: 'test@test.com' } });
+ expect(backupEmailInstance.state.backupEmail).toEqual('test@test.com');
+ });
+ });
+
describe('Submit', () => {
let preventDefaultSpy;
@@ -98,6 +108,8 @@ describe('BackupEmail', () => {
fetchMock.post('/backup-account', 204);
backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
+
+ backupEmail.find('InputField').simulate('change', { target: { value: 'test@test.com' } });
backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
});
@@ -109,6 +121,10 @@ describe('BackupEmail', () => {
expect(fetchMock.lastOptions('/backup-account').body).toContain('"csrftoken":["abc123"]');
});
+ it('sends user email as content', () => {
+ expect(fetchMock.lastOptions('/backup-account').body).toContain('"backupEmail":"test@test.com"');
+ });
+
it('sends content-type header', () => {
expect(fetchMock.lastOptions('/backup-account').headers['Content-Type']).toEqual('application/json');
});