598 lines
21 KiB
Python
598 lines
21 KiB
Python
# authentic2 - versatile identity manager
|
|
# Copyright (C) 2010-2019 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program 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 Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from unittest import mock
|
|
|
|
import responses
|
|
|
|
from authentic2.models import Attribute, SMSCode, Token
|
|
|
|
from .utils import login
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
Attribute.objects.get_or_create(
|
|
name='another_phone',
|
|
kind='phone_number',
|
|
defaults={'label': 'Another phone'},
|
|
)
|
|
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.attributes.another_phone = '+33122444444'
|
|
nomail_user.phone = ''
|
|
nomail_user.save()
|
|
|
|
assert nomail_user.phone_verified_on is None
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
assert 'Your current phone number is +33122446688.' in resp.text
|
|
assert 'Phone Change' in resp.pyquery('title')[0].text
|
|
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow()
|
|
assert not Token.objects.count()
|
|
nomail_user.refresh_from_db()
|
|
assert nomail_user.attributes.phone == '+33122446666'
|
|
assert nomail_user.attributes.another_phone == '+33122444444' # unchanged
|
|
assert nomail_user.phone_verified_on is not None
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_no_password(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
Attribute.objects.get_or_create(
|
|
name='another_phone',
|
|
kind='phone_number',
|
|
defaults={'label': 'Another phone'},
|
|
)
|
|
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.attributes.another_phone = '+33122444444'
|
|
nomail_user.phone = ''
|
|
nomail_user.save()
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
with mock.patch(
|
|
'authentic2.views.IdentifierChangeMixin.can_validate_with_password'
|
|
) as mocked_can_validate:
|
|
with mock.patch(
|
|
'authentic2.views.IdentifierChangeMixin.has_recent_authentication'
|
|
) as mocked_has_recent_authn:
|
|
mocked_can_validate.return_value = False
|
|
mocked_has_recent_authn.return_value = True
|
|
resp = app.get('/accounts/change-phone/')
|
|
assert 'Your current phone number is +33122446688.' in resp.text
|
|
resp.form.set('phone_1', '122446666')
|
|
assert 'password' not in resp.form.fields
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow()
|
|
assert not Token.objects.count()
|
|
nomail_user.refresh_from_db()
|
|
assert nomail_user.attributes.phone == '+33122446666'
|
|
assert nomail_user.attributes.another_phone == '+33122444444' # unchanged
|
|
assert nomail_user.phone_verified_on is not None
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_no_password_no_recent_authn(
|
|
app, nomail_user, user_ou1, phone_activated_authn, settings
|
|
):
|
|
Attribute.objects.get_or_create(
|
|
name='another_phone',
|
|
kind='phone_number',
|
|
defaults={'label': 'Another phone'},
|
|
)
|
|
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.attributes.another_phone = '+33122444444'
|
|
nomail_user.phone = ''
|
|
nomail_user.save()
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
with mock.patch(
|
|
'authentic2.views.IdentifierChangeMixin.can_validate_with_password'
|
|
) as mocked_can_validate:
|
|
with mock.patch(
|
|
'authentic2.views.IdentifierChangeMixin.has_recent_authentication'
|
|
) as mocked_has_recent_authn:
|
|
mocked_can_validate.return_value = False
|
|
mocked_has_recent_authn.return_value = False
|
|
resp = app.get('/accounts/change-phone/')
|
|
resp = resp.follow()
|
|
assert resp.pyquery('li.info')[0].text == 'You must re-authenticate to change your phone number.'
|
|
resp.form.set('username', nomail_user.phone_identifier)
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit(name='login-password-submit')
|
|
mocked_has_recent_authn.return_value = True
|
|
resp = resp.follow().maybe_follow()
|
|
resp.form.set('phone_1', '122446666')
|
|
assert 'Your current phone number is +33122446688.' in resp.text
|
|
assert 'password' not in resp.form.fields
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow()
|
|
assert not Token.objects.count()
|
|
nomail_user.refresh_from_db()
|
|
assert nomail_user.attributes.phone == '+33122446666'
|
|
assert nomail_user.attributes.another_phone == '+33122444444' # unchanged
|
|
assert nomail_user.phone_verified_on is not None
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_nondefault_attribute(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
phone, dummy = Attribute.objects.get_or_create(
|
|
name='another_phone',
|
|
kind='phone_number',
|
|
user_editable=True,
|
|
defaults={'label': 'Another phone'},
|
|
)
|
|
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.attributes.another_phone = '+33122444444'
|
|
nomail_user.phone = ''
|
|
nomail_user.save()
|
|
|
|
assert nomail_user.phone_verified_on is None
|
|
|
|
phone_activated_authn.phone_identifier_field = phone
|
|
phone_activated_authn.save()
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.another_phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
assert 'Your current phone number is +33122444444.' in resp.text
|
|
|
|
resp.form.set('phone_1', '122666666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow()
|
|
assert not Token.objects.count()
|
|
nomail_user.refresh_from_db()
|
|
assert nomail_user.attributes.phone == '+33122446688' # unchanged
|
|
assert nomail_user.attributes.another_phone == '+33122666666'
|
|
assert nomail_user.phone_verified_on is not None
|
|
|
|
|
|
def test_change_phone_wrong_input(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '12244666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit()
|
|
assert (
|
|
'Invalid phone number. Phone number from Metropolitan France must respect local format (e.g. 06 39 98 01 23).'
|
|
) == resp.pyquery('.error p')[0].text_content().strip()
|
|
|
|
resp.form.set('phone_0', '32')
|
|
resp.form.set('phone_1', '12244')
|
|
resp = resp.form.submit()
|
|
assert (
|
|
'Invalid phone number. Phone number from Belgium must respect local format (e.g. 042 11 22 33).'
|
|
) == resp.pyquery('.error p')[0].text_content().strip()
|
|
|
|
assert not SMSCode.objects.count()
|
|
assert not Token.objects.count()
|
|
resp.form.set('phone_1', 'abc')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit()
|
|
assert (
|
|
'Invalid phone number. Phone number from Belgium must respect local format (e.g. 042 11 22 33).'
|
|
) == resp.pyquery('.error p')[0].text_content().strip()
|
|
assert not SMSCode.objects.count()
|
|
assert not Token.objects.count()
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_expired_code(app, nomail_user, user_ou1, phone_activated_authn, settings, freezer):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
freezer.tick(3600) # user did not immediately submit code
|
|
resp = resp.form.submit('')
|
|
assert resp.pyquery('ul.errorlist li')[0].text == 'The code has expired.'
|
|
assert not Token.objects.count()
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_code_modified(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit()
|
|
location = resp.location[:-5] + 'wxyz/' # oops, something went wrong with the url token
|
|
app.get(location, status=404)
|
|
assert not Token.objects.count()
|
|
|
|
location = (
|
|
resp.location[:-5] + 'abcd/'
|
|
) # oops, something went wrong again although it's a valid uuid format
|
|
app.get(location, status=404)
|
|
assert not Token.objects.count()
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_token_modified(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('')
|
|
resp.location = resp.location.split('?')[0]
|
|
resp.location = resp.location[:-5] + 'abcd/' # oops, something went wrong with the url token
|
|
resp = resp.follow().maybe_follow()
|
|
assert resp.pyquery('.error')[0].text == 'Your phone number update request is invalid, try again'
|
|
nomail_user.refresh_from_db()
|
|
assert nomail_user.attributes.phone == '+33122446688'
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_identifier_attribute_changed(
|
|
app, nomail_user, user_ou1, phone_activated_authn, settings
|
|
):
|
|
phone, dummy = Attribute.objects.get_or_create(
|
|
name='another_phone',
|
|
kind='phone_number',
|
|
defaults={'label': 'Another phone'},
|
|
)
|
|
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.attributes.another_phone = '+33122444444'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
assert 'Your current phone number is +33122446688.' in resp.text
|
|
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
phone_activated_authn.phone_identifier_field = phone
|
|
phone_activated_authn.save()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow()
|
|
assert not Token.objects.count()
|
|
nomail_user.refresh_from_db()
|
|
# phone fields have been swapped, nothing really worth doing about this
|
|
# we just check that the phone change request does not crash
|
|
assert nomail_user.attributes.phone == '+33122446688' # unchanged
|
|
assert nomail_user.attributes.another_phone == '+33122446666'
|
|
|
|
|
|
@responses.activate
|
|
def test_change_phone_authn_deactivated(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
|
|
phone_activated_authn.accept_phone_authentication = False
|
|
phone_activated_authn.save()
|
|
|
|
resp = app.get('/accounts/change-phone/')
|
|
assert 'Your current phone number is +33122446688.' in resp.text
|
|
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow()
|
|
assert not Token.objects.count()
|
|
# assert not SMSCode.objects.count() # avoid multiple uses
|
|
nomail_user.refresh_from_db()
|
|
|
|
|
|
def test_change_phone_identifier_field_unknown(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
|
|
login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
|
|
phone_activated_authn.phone_identifier_field = None
|
|
phone_activated_authn.save()
|
|
|
|
app.get('/accounts/change-phone/', status=404)
|
|
|
|
|
|
def test_change_phone_identifier_field_not_user_editable(
|
|
app, nomail_user, user_ou1, phone_activated_authn, settings
|
|
):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
|
|
login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
|
|
phone_activated_authn.phone_identifier_field.user_editable = False
|
|
phone_activated_authn.phone_identifier_field.save()
|
|
|
|
app.get('/accounts/change-phone/', status=404)
|
|
|
|
|
|
def test_change_phone_identifier_field_disabled(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
|
|
login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
|
|
phone_activated_authn.phone_identifier_field.disabled = True
|
|
phone_activated_authn.phone_identifier_field.save()
|
|
|
|
app.get('/accounts/change-phone/', status=404)
|
|
|
|
|
|
@responses.activate
|
|
def test_phone_change_already_existing(
|
|
app, nomail_user, user_ou1, phone_activated_authn, settings, simple_user
|
|
):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
simple_user.attributes.phone = '+33122446666'
|
|
simple_user.save()
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('').follow().maybe_follow()
|
|
assert resp.pyquery('li.error')[0].text == 'This phone number is already used by another account.'
|
|
|
|
|
|
@responses.activate
|
|
def test_phone_change_preempted_during_request(
|
|
app, nomail_user, user_ou1, phone_activated_authn, settings, simple_user
|
|
):
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
resp.form.set('sms_code', code.value)
|
|
# oops, some other user took this number during the change request
|
|
simple_user.attributes.phone = '+33122446666'
|
|
simple_user.save()
|
|
resp = resp.form.submit('').follow().maybe_follow()
|
|
assert resp.pyquery('li.error')[0].text == 'This phone number is already used by another account.'
|
|
|
|
|
|
@responses.activate
|
|
def test_phone_change_lock_identifier_error_token_use(
|
|
app, nomail_user, user_ou1, phone_activated_authn, settings, monkeypatch
|
|
):
|
|
from authentic2.models import Lock
|
|
|
|
nomail_user.attributes.phone = '+33122446688'
|
|
nomail_user.save()
|
|
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
|
|
def erroneous_lock_identifier(identifier, nowait=False):
|
|
raise Lock.Error
|
|
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
|
|
resp.form.set('sms_code', code.value)
|
|
resp = resp.form.submit('')
|
|
monkeypatch.setattr(Lock, 'lock_identifier', erroneous_lock_identifier)
|
|
resp = resp.follow().maybe_follow()
|
|
assert 'Something went wrong while updating' in resp.pyquery('li.error')[0].text
|
|
assert nomail_user.attributes.phone == '+33122446688'
|
|
|
|
|
|
@responses.activate
|
|
def test_phone_change_no_existing_number(app, nomail_user, user_ou1, phone_activated_authn, settings):
|
|
settings.SMS_URL = 'https://foo.whatever.none/'
|
|
responses.post('https://foo.whatever.none/', status=200)
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/change-phone/',
|
|
password=nomail_user.username,
|
|
)
|
|
assert 'Your account does not declare a phone number yet.' in resp.text
|
|
assert 'Your phone number is' not in resp.text
|
|
resp.form.set('phone_1', '122446666')
|
|
resp.form.set('password', nomail_user.username)
|
|
resp = resp.form.submit().follow()
|
|
code = SMSCode.objects.get()
|
|
|
|
resp.form.set('sms_code', code.value)
|
|
resp.form.submit('').follow()
|
|
nomail_user.refresh_from_db()
|
|
assert nomail_user.attributes.phone == '+33122446666'
|
|
assert nomail_user.phone_verified_on is not None
|
|
|
|
|
|
def test_phone_change_no_existing_number_accounts_action_label_variation(
|
|
app,
|
|
nomail_user,
|
|
phone_activated_authn,
|
|
):
|
|
resp = login(
|
|
app,
|
|
nomail_user,
|
|
login=nomail_user.attributes.phone,
|
|
path='/accounts/',
|
|
password=nomail_user.username,
|
|
)
|
|
assert resp.pyquery("[href='/accounts/change-phone/']")[0].text == 'Declare your phone number'
|
|
nomail_user.attributes.phone = '+33122446666'
|
|
nomail_user.save()
|
|
|
|
resp = app.get('/accounts/')
|
|
assert resp.pyquery("[href='/accounts/change-phone/']")[0].text == 'Change phone'
|