From c5708ee3ce3b0cd62afe582e9c78c9089d413b2a Mon Sep 17 00:00:00 2001 From: str4d <str4d@mail.i2p> Date: Fri, 10 Jun 2016 12:58:01 +0000 Subject: [PATCH] Add a UI for changing the password --- briar-android/AndroidManifest.xml | 10 + .../res/layout/activity_change_password.xml | 128 ++++++++++ briar-android/res/values/strings.xml | 6 + briar-android/res/xml/settings.xml | 14 ++ .../android/ActivityComponent.java | 2 + .../android/ChangePasswordActivity.java | 162 ++++++++++++ .../controller/PasswordController.java | 3 + .../controller/PasswordControllerImpl.java | 52 +++- .../controller/SetupControllerImpl.java | 31 +-- .../activity/ChangePasswordActivityTest.java | 234 ++++++++++++++++++ .../activity/TestChangePasswordActivity.java | 29 +++ 11 files changed, 640 insertions(+), 31 deletions(-) create mode 100644 briar-android/res/layout/activity_change_password.xml create mode 100644 briar-android/src/org/briarproject/android/ChangePasswordActivity.java create mode 100644 briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java create mode 100644 briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 43231803db..2d8dc0223d 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -205,6 +205,16 @@ </intent-filter> </activity> + <activity + android:name=".android.ChangePasswordActivity" + android:label="@string/change_password" + android:parentActivityName=".android.SettingsActivity"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".android.SettingsActivity" + /> + </activity> + <activity android:name=".android.panic.PanicPreferencesActivity" android:label="@string/panic_setting"> diff --git a/briar-android/res/layout/activity_change_password.xml b/briar-android/res/layout/activity_change_password.xml new file mode 100644 index 0000000000..6e5887f2bf --- /dev/null +++ b/briar-android/res/layout/activity_change_password.xml @@ -0,0 +1,128 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".android.ChangePasswordActivity"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="@dimen/margin_activity_vertical" + android:paddingEnd="@dimen/margin_activity_horizontal" + android:paddingLeft="@dimen/margin_activity_horizontal" + android:paddingRight="@dimen/margin_activity_horizontal" + android:paddingStart="@dimen/margin_activity_horizontal" + android:paddingTop="@dimen/margin_activity_vertical"> + + <TextView + android:id="@+id/current_password_title" + style="@style/BriarTextTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:text="@string/current_password" + android:textSize="@dimen/text_size_medium"/> + + <android.support.design.widget.TextInputLayout + android:id="@+id/current_password_entry_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/current_password_title" + android:layout_centerHorizontal="true" + app:errorEnabled="true"> + + <EditText + android:id="@+id/current_password_entry" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPassword" + android:maxLines="1"/> + </android.support.design.widget.TextInputLayout> + + <TextView + android:id="@+id/new_password_title" + style="@style/BriarTextTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/current_password_entry_wrapper" + android:layout_centerHorizontal="true" + android:text="@string/choose_new_password" + android:textSize="@dimen/text_size_medium"/> + + <android.support.design.widget.TextInputLayout + android:id="@+id/new_password_entry_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/new_password_title" + android:layout_centerHorizontal="true" + app:errorEnabled="true"> + + <EditText + android:id="@+id/new_password_entry" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPassword" + android:maxLines="1"/> + </android.support.design.widget.TextInputLayout> + + <TextView + android:id="@+id/new_password_confirm_title" + style="@style/BriarTextTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/new_password_entry_wrapper" + android:layout_centerHorizontal="true" + android:text="@string/confirm_new_password" + android:textSize="@dimen/text_size_medium"/> + + <android.support.design.widget.TextInputLayout + android:id="@+id/new_password_confirm_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/new_password_confirm_title" + android:layout_centerHorizontal="true" + app:errorEnabled="true"> + + <EditText + android:id="@+id/new_password_confirm" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:inputType="textPassword" + android:maxLines="1"/> + </android.support.design.widget.TextInputLayout> + + <org.briarproject.android.util.StrengthMeter + android:id="@+id/strength_meter" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/new_password_confirm_wrapper" + android:layout_centerHorizontal="true" + android:visibility="invisible"/> + + <Button + android:id="@+id/change_password" + style="@style/BriarButton.Default" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/strength_meter" + android:layout_centerHorizontal="true" + android:layout_marginTop="@dimen/margin_medium" + android:enabled="false" + android:text="@string/change_password"/> + + <ProgressBar + android:id="@+id/progress_wheel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@id/change_password" + android:layout_centerHorizontal="true" + android:visibility="invisible"/> + + </RelativeLayout> + +</ScrollView> \ No newline at end of file diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index d237e3260c..d68a009fd0 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -139,6 +139,8 @@ <string name="tor_mobile_setting">Connect via Tor</string> <string name="tor_mobile_setting_enabled">When using Wi-Fi or mobile data</string> <string name="tor_mobile_setting_disabled">Only when using Wi-Fi</string> + <string name="security_settings_title">Security</string> + <string name="change_password">Change password</string> <string name="panic_setting">Panic button setup</string> <string name="panic_setting_title">Panic button</string> <string name="panic_setting_hint">Configure how Briar will react when you use a panic button app</string> @@ -165,6 +167,10 @@ <string name="purge_setting_summary">Delete your Briar account if a panic button is pressed. Caution: This will permanently delete your identities, contacts and messages</string> <string name="uninstall_setting_title">Uninstall Briar</string> <string name="uninstall_setting_summary">This requires manual confirmation in a panic event</string> + <string name="current_password">Enter your current password:</string> + <string name="choose_new_password">Choose your new password:</string> + <string name="confirm_new_password">Confirm your new password:</string> + <string name="password_changed">Password has been changed.</string> <string name="feedback_settings_title">Feedback</string> <string name="send_feedback">Send feedback</string> diff --git a/briar-android/res/xml/settings.xml b/briar-android/res/xml/settings.xml index 7ee104ad2b..80c75046aa 100644 --- a/briar-android/res/xml/settings.xml +++ b/briar-android/res/xml/settings.xml @@ -25,6 +25,20 @@ </PreferenceCategory> + <PreferenceCategory + android:title="@string/security_settings_title"> + + <Preference + android:key="pref_key_change_password" + android:title="@string/change_password"> + + <intent + android:targetClass="org.briarproject.android.ChangePasswordActivity" + android:targetPackage="org.briarproject"/> + </Preference> + + </PreferenceCategory> + <PreferenceCategory android:title="@string/panic_setting_title"> diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index 09a9461eae..346c01ef8b 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -62,6 +62,8 @@ public interface ActivityComponent { void inject(SettingsActivity activity); + void inject(ChangePasswordActivity activity); + void inject(IntroductionActivity activity); @Named("ContactListFragment") diff --git a/briar-android/src/org/briarproject/android/ChangePasswordActivity.java b/briar-android/src/org/briarproject/android/ChangePasswordActivity.java new file mode 100644 index 0000000000..4c93924ea8 --- /dev/null +++ b/briar-android/src/org/briarproject/android/ChangePasswordActivity.java @@ -0,0 +1,162 @@ +package org.briarproject.android; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; + +import org.briarproject.R; +import org.briarproject.android.controller.PasswordController; +import org.briarproject.android.controller.SetupController; +import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.android.util.AndroidUtils; +import org.briarproject.android.util.StrengthMeter; + +import javax.inject.Inject; + +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK; + +public class ChangePasswordActivity extends BaseActivity + implements OnClickListener, + OnEditorActionListener { + + @Inject + protected PasswordController passwordController; + @Inject + protected SetupController setupController; + + private TextInputLayout currentPasswordEntryWrapper; + private TextInputLayout newPasswordEntryWrapper; + private TextInputLayout newPasswordConfirmationWrapper; + private EditText currentPassword; + private EditText newPassword; + private EditText newPasswordConfirmation; + private StrengthMeter strengthMeter; + private Button changePasswordButton; + private ProgressBar progress; + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + setContentView(R.layout.activity_change_password); + + currentPasswordEntryWrapper = + (TextInputLayout) findViewById( + R.id.current_password_entry_wrapper); + newPasswordEntryWrapper = + (TextInputLayout) findViewById(R.id.new_password_entry_wrapper); + newPasswordConfirmationWrapper = + (TextInputLayout) findViewById( + R.id.new_password_confirm_wrapper); + currentPassword = (EditText) findViewById(R.id.current_password_entry); + newPassword = (EditText) findViewById(R.id.new_password_entry); + newPasswordConfirmation = + (EditText) findViewById(R.id.new_password_confirm); + strengthMeter = (StrengthMeter) findViewById(R.id.strength_meter); + changePasswordButton = (Button) findViewById(R.id.change_password); + progress = (ProgressBar) findViewById(R.id.progress_wheel); + + TextWatcher tw = new TextWatcher() { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + enableOrDisableContinueButton(); + } + + @Override + public void afterTextChanged(Editable s) { + } + }; + + currentPassword.addTextChangedListener(tw); + newPassword.addTextChangedListener(tw); + newPasswordConfirmation.addTextChangedListener(tw); + newPasswordConfirmation.setOnEditorActionListener(this); + changePasswordButton.setOnClickListener(this); + } + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + } + + private void enableOrDisableContinueButton() { + if (progress == null) return; // Not created yet + if (newPassword.getText().length() > 0 && newPassword.hasFocus()) + strengthMeter.setVisibility(VISIBLE); + else strengthMeter.setVisibility(INVISIBLE); + String firstPassword = newPassword.getText().toString(); + String secondPassword = newPasswordConfirmation.getText().toString(); + boolean passwordsMatch = firstPassword.equals(secondPassword); + float strength = + setupController.estimatePasswordStrength(firstPassword); + strengthMeter.setStrength(strength); + AndroidUtils.setError(newPasswordEntryWrapper, + getString(R.string.password_too_weak), + firstPassword.length() > 0 && strength < WEAK); + AndroidUtils.setError(newPasswordConfirmationWrapper, + getString(R.string.passwords_do_not_match), + secondPassword.length() > 0 && !passwordsMatch); + changePasswordButton.setEnabled( + !currentPassword.getText().toString().isEmpty() && + passwordsMatch && strength >= WEAK); + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + hideSoftKeyboard(v); + return true; + } + + @Override + public void onClick(View view) { + // Replace the button with a progress bar + changePasswordButton.setVisibility(INVISIBLE); + progress.setVisibility(VISIBLE); + passwordController.changePassword(currentPassword.getText().toString(), + newPassword.getText().toString(), + new UiResultHandler<Boolean>(this) { + @Override + public void onResultUi(@NonNull Boolean result) { + if (result) { + Toast.makeText(ChangePasswordActivity.this, + R.string.password_changed, + Toast.LENGTH_LONG).show(); + setResult(RESULT_OK); + finish(); + } else { + tryAgain(); + } + } + }); + } + + private void tryAgain() { + AndroidUtils.setError(currentPasswordEntryWrapper, + getString(R.string.try_again), true); + changePasswordButton.setVisibility(VISIBLE); + progress.setVisibility(INVISIBLE); + currentPassword.setText(""); + + // show the keyboard again + showSoftKeyboard(currentPassword); + } +} diff --git a/briar-android/src/org/briarproject/android/controller/PasswordController.java b/briar-android/src/org/briarproject/android/controller/PasswordController.java index b49c4c3fa6..64e4d4df00 100644 --- a/briar-android/src/org/briarproject/android/controller/PasswordController.java +++ b/briar-android/src/org/briarproject/android/controller/PasswordController.java @@ -6,4 +6,7 @@ public interface PasswordController extends ConfigController { void validatePassword(String password, ResultHandler<Boolean> resultHandler); + + void changePassword(String password, String newPassword, + ResultHandler<Boolean> resultHandler); } diff --git a/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java b/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java index 22025b0a21..23527c81fc 100644 --- a/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java +++ b/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java @@ -1,28 +1,40 @@ package org.briarproject.android.controller; import android.app.Activity; +import android.content.SharedPreferences; import org.briarproject.android.controller.handler.ResultHandler; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoExecutor; import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.identity.LocalAuthor; import org.briarproject.util.StringUtils; import java.util.concurrent.Executor; +import java.util.logging.Logger; import javax.inject.Inject; +import static java.util.logging.Level.INFO; + public class PasswordControllerImpl extends ConfigControllerImpl implements PasswordController { + private static final Logger LOG = + Logger.getLogger(PasswordControllerImpl.class.getName()); + + private final static String PREF_DB_KEY = "key"; + @Inject @CryptoExecutor protected Executor cryptoExecutor; @Inject - protected CryptoComponent crypto; - @Inject protected Activity activity; + // Fields that are accessed from background threads must be volatile + @Inject + protected CryptoComponent crypto; + @Inject public PasswordControllerImpl() { @@ -46,10 +58,46 @@ public class PasswordControllerImpl extends ConfigControllerImpl }); } + @Override + public void changePassword(final String password, final String newPassword, + final ResultHandler<Boolean> resultHandler) { + final byte[] encrypted = getEncryptedKey(); + cryptoExecutor.execute(new Runnable() { + @Override + public void run() { + byte[] key = crypto.decryptWithPassword(encrypted, password); + if (key == null) { + resultHandler.onResult(false); + } else { + String hex = + encryptDatabaseKey(new SecretKey(key), newPassword); + storeEncryptedDatabaseKey(hex); + resultHandler.onResult(true); + } + } + }); + } + private byte[] getEncryptedKey() { String hex = getEncryptedDatabaseKey(); if (hex == null) throw new IllegalStateException("Encrypted database key is null"); return StringUtils.fromHexString(hex); } + + // Call inside cryptoExecutor + String encryptDatabaseKey(SecretKey key, String password) { + long now = System.currentTimeMillis(); + byte[] encrypted = crypto.encryptWithPassword(key.getBytes(), password); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Key derivation took " + duration + " ms"); + return StringUtils.toHexString(encrypted); + } + + void storeEncryptedDatabaseKey(String hex) { + SharedPreferences.Editor editor = briarPrefs.edit(); + editor.putString(PREF_DB_KEY, hex); + editor.apply(); + } } diff --git a/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java b/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java index 6500346ebe..c194b31370 100644 --- a/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java +++ b/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java @@ -22,29 +22,17 @@ import javax.inject.Inject; import static java.util.logging.Level.INFO; -public class SetupControllerImpl implements SetupController { +public class SetupControllerImpl extends PasswordControllerImpl + implements SetupController { private static final Logger LOG = Logger.getLogger(SetupControllerImpl.class.getName()); - private final static String PREF_DB_KEY = "key"; - - @Inject - @CryptoExecutor - protected Executor cryptoExecutor; @Inject protected PasswordStrengthEstimator strengthEstimator; - @Inject - protected Activity activity; - @Inject - protected SharedPreferences briarPrefs; // Fields that are accessed from background threads must be volatile @Inject - protected volatile CryptoComponent crypto; - @Inject - protected volatile DatabaseConfig databaseConfig; - @Inject protected volatile AuthorFactory authorFactory; @Inject protected volatile ReferenceManager referenceManager; @@ -54,15 +42,6 @@ public class SetupControllerImpl implements SetupController { } - private String encryptDatabaseKey(SecretKey key, String password) { - long now = System.currentTimeMillis(); - byte[] encrypted = crypto.encryptWithPassword(key.getBytes(), password); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Key derivation took " + duration + " ms"); - return StringUtils.toHexString(encrypted); - } - private LocalAuthor createLocalAuthor(String nickname) { long now = System.currentTimeMillis(); KeyPair keyPair = crypto.generateSignatureKeyPair(); @@ -98,10 +77,4 @@ public class SetupControllerImpl implements SetupController { } }); } - - private void storeEncryptedDatabaseKey(String hex) { - SharedPreferences.Editor editor = briarPrefs.edit(); - editor.putString(PREF_DB_KEY, hex); - editor.apply(); - } } diff --git a/briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java b/briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java new file mode 100644 index 0000000000..b7fb34821f --- /dev/null +++ b/briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java @@ -0,0 +1,234 @@ +package briarproject.activity; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.support.design.widget.TextInputLayout; +import android.widget.Button; +import android.widget.EditText; + +import org.briarproject.BuildConfig; +import org.briarproject.R; +import org.briarproject.android.SettingsActivity; +import org.briarproject.android.controller.PasswordController; +import org.briarproject.android.controller.SetupController; +import org.briarproject.android.controller.handler.ResultHandler; +import org.briarproject.android.util.StrengthMeter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowActivity; + +import static junit.framework.Assert.assertEquals; +import static org.briarproject.api.crypto.PasswordStrengthEstimator.NONE; +import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_STRONG; +import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; +import static org.briarproject.api.crypto.PasswordStrengthEstimator.STRONG; +import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21, + application = TestBriarApplication.class) +public class ChangePasswordActivityTest { + + private TestChangePasswordActivity changePasswordActivity; + private TextInputLayout passwordConfirmationWrapper; + private EditText currentPassword; + private EditText newPassword; + private EditText newPasswordConfirmation; + private StrengthMeter strengthMeter; + private Button changePasswordButton; + + @Mock + private PasswordController passwordController; + @Mock + private SetupController setupController; + @Captor + private ArgumentCaptor<ResultHandler<Boolean>> resultCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + changePasswordActivity = + Robolectric.setupActivity(TestChangePasswordActivity.class); + passwordConfirmationWrapper = (TextInputLayout) changePasswordActivity + .findViewById(R.id.new_password_confirm_wrapper); + currentPassword = + (EditText) changePasswordActivity + .findViewById(R.id.current_password_entry); + newPassword = + (EditText) changePasswordActivity + .findViewById(R.id.new_password_entry); + newPasswordConfirmation = + (EditText) changePasswordActivity + .findViewById(R.id.new_password_confirm); + strengthMeter = + (StrengthMeter) changePasswordActivity + .findViewById(R.id.strength_meter); + changePasswordButton = + (Button) changePasswordActivity + .findViewById(R.id.change_password); + } + + private void testStrengthMeter(String pass, float strength, int color) { + newPassword.setText(pass); + assertEquals(strengthMeter.getProgress(), + (int) (strengthMeter.getMax() * strength)); + assertEquals(color, strengthMeter.getColor()); + } + + @Test + public void testPasswordMatchUI() { + // Password mismatch + newPassword.setText("really.safe.password"); + newPasswordConfirmation.setText("really.safe.pass"); + assertEquals(changePasswordButton.isEnabled(), false); + assertEquals(passwordConfirmationWrapper.getError(), + changePasswordActivity + .getString(R.string.passwords_do_not_match)); + // Button enabled + newPassword.setText("really.safe.pass"); + newPasswordConfirmation.setText("really.safe.pass"); + // Confirm that the password mismatch error message is not visible + Assert.assertNotEquals(passwordConfirmationWrapper.getError(), + changePasswordActivity + .getString(R.string.passwords_do_not_match)); + // Nick has not been set, expect the button to be disabled + assertEquals(changePasswordButton.isEnabled(), false); + } + + @Test + public void testChangePasswordUI() { + + PasswordController mockedPasswordController = this.passwordController; + SetupController mockedSetupController = this.setupController; + changePasswordActivity.setPasswordController(mockedPasswordController); + changePasswordActivity.setSetupController(mockedSetupController); + // Mock strong password strength answer + when(mockedSetupController.estimatePasswordStrength(anyString())) + .thenReturn(STRONG); + String curPass = "old.password"; + String safePass = "really.safe.password"; + currentPassword.setText(curPass); + newPassword.setText(safePass); + newPasswordConfirmation.setText(safePass); + // Confirm that the create account button is clickable + assertEquals(changePasswordButton.isEnabled(), true); + changePasswordButton.performClick(); + // Verify that the controller's method was called with the correct + // params and get the callback + verify(mockedPasswordController, times(1)) + .changePassword(eq(curPass), eq(safePass), + resultCaptor.capture()); + // execute the callback + resultCaptor.getValue().onResult(true); + assertEquals(changePasswordActivity.isFinishing(), true); + } + + @Test + public void testPasswordChange() { + PasswordController passwordController = + changePasswordActivity.getPasswordController(); + SetupController setupController = + changePasswordActivity.getSetupController(); + // mock a resulthandler + ResultHandler<Long> resultHandler = + (ResultHandler<Long>) mock(ResultHandler.class); + setupController.createIdentity("nick", "some.old.pass", resultHandler); + // blocking verification call with timeout that waits until the mocked + // result gets called with handle 0L, the expected value + verify(resultHandler, timeout(2000).times(1)).onResult(0L); + SharedPreferences prefs = + changePasswordActivity + .getSharedPreferences("db", Context.MODE_PRIVATE); + // Confirm database key + assertTrue(prefs.contains("key")); + String oldKey = prefs.getString("key", null); + // mock a resulthandler + ResultHandler<Boolean> resultHandler2 = + (ResultHandler<Boolean>) mock(ResultHandler.class); + passwordController + .changePassword("some.old.pass", "some.strong.pass", + resultHandler2); + // blocking verification call with timeout that waits until the mocked + // result gets called with handle 0L, the expected value + verify(resultHandler2, timeout(2000).times(1)).onResult(true); + // Confirm database key + assertTrue(prefs.contains("key")); + assertNotEquals(oldKey, prefs.getString("key", null)); + // Note that Robolectric uses its own persistant storage that it + // wipes clean after each test run, no need to clean up manually. + } + + @Test + public void testStrengthMeter() { + SetupController controller = + changePasswordActivity.getSetupController(); + + String strongPass = "very.strong.password.123"; + String weakPass = "we"; + String quiteStrongPass = "quite.strong"; + + float val = controller.estimatePasswordStrength(strongPass); + assertTrue(val == STRONG); + val = controller.estimatePasswordStrength(weakPass); + assertTrue(val < WEAK && val > NONE); + val = controller.estimatePasswordStrength(quiteStrongPass); + assertTrue(val < STRONG && val > QUITE_WEAK); + } + + @Test + public void testStrengthMeterUI() { + Assert.assertNotNull(changePasswordActivity); + // replace the setup controller with our mocked copy + SetupController mockedController = this.setupController; + changePasswordActivity.setSetupController(mockedController); + // Mock answers for UI testing only + when(mockedController.estimatePasswordStrength("strong")).thenReturn( + STRONG); + when(mockedController.estimatePasswordStrength("qstring")).thenReturn( + QUITE_STRONG); + when(mockedController.estimatePasswordStrength("qweak")).thenReturn( + QUITE_WEAK); + when(mockedController.estimatePasswordStrength("weak")).thenReturn( + WEAK); + when(mockedController.estimatePasswordStrength("empty")).thenReturn( + NONE); + // Test the meters progress and color for several values + testStrengthMeter("strong", STRONG, StrengthMeter.GREEN); + Mockito.verify(mockedController, Mockito.times(1)) + .estimatePasswordStrength(eq("strong")); + testStrengthMeter("qstring", QUITE_STRONG, StrengthMeter.LIME); + Mockito.verify(mockedController, Mockito.times(1)) + .estimatePasswordStrength(eq("qstring")); + testStrengthMeter("qweak", QUITE_WEAK, StrengthMeter.YELLOW); + Mockito.verify(mockedController, Mockito.times(1)) + .estimatePasswordStrength(eq("qweak")); + testStrengthMeter("weak", WEAK, StrengthMeter.ORANGE); + Mockito.verify(mockedController, Mockito.times(1)) + .estimatePasswordStrength(eq("weak")); + // Not sure this should be the correct behaviour on an empty input ? + testStrengthMeter("empty", NONE, StrengthMeter.RED); + Mockito.verify(mockedController, Mockito.times(1)) + .estimatePasswordStrength(eq("empty")); + } +} diff --git a/briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java b/briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java new file mode 100644 index 0000000000..a5dc6669d9 --- /dev/null +++ b/briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java @@ -0,0 +1,29 @@ +package briarproject.activity; + +import org.briarproject.android.ChangePasswordActivity; +import org.briarproject.android.SetupActivity; +import org.briarproject.android.controller.PasswordController; +import org.briarproject.android.controller.SetupController; + +/** + * This class exposes the PasswordController and SetupController and offers the + * possibility to override them. + */ +public class TestChangePasswordActivity extends ChangePasswordActivity { + + public PasswordController getPasswordController() { + return passwordController; + } + + public SetupController getSetupController() { + return setupController; + } + + public void setPasswordController(PasswordController passwordController) { + this.passwordController = passwordController; + } + + public void setSetupController(SetupController setupController) { + this.setupController = setupController; + } +} -- GitLab