Skip to content

Commit

Permalink
Fix Espresso tests to handle new FIrebase Auth/login and Refactor Pac…
Browse files Browse the repository at this point in the history
…kages (#111)

* Finished UI mockups for login page, register page, and user profile

* Pushing this commit because Owen is a soy boy

* Changed main startup activity

* switch to 1 XML (light mode) for login/register (#82)

* Very rough draft of user profile/login/logout

* Added unique username check

* Updated to use authentication and encryption

* moving off branch

* Added user authentication with unique usernames

* Finished

* Added javadocs

* Added bug fixes from Lukas

* Added Owen's changes

* clean up imports/lambdas

* @ldbonkowski and I fixed Espresso tests to handle new login screen.

Also refactored most files into their own packages.

* merge main and resolve conflicts

* fix conflicting R.string issues

* fix failing Espresso tests

* fix flaky tests

---------

Co-authored-by: antonio2uofa <antonio2@ualberta.ca>
Co-authored-by: Antonio Martin-O <90714463+antonio2uofa@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 4, 2023
1 parent e747a57 commit f95f336
Show file tree
Hide file tree
Showing 68 changed files with 756 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@ public void testSubmitWithRequiredEmpty() {
onView(withId(R.id.add_item_date_layout)).check(matches(hasDescendant(withText(defaultErrorMsg))));
}

@Test
public void testBackButtonGoesToHomePage() {
onView(withId(R.id.add_item_back_button)).perform(click());
onView(withId(R.id.item_list)).check(matches(isDisplayed()));
}

@Test
public void testRoundCostTo2Decimals() {
enterText(R.id.add_item_cost, "99.9852323324");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ public void navigateToEditItemFragment() throws Exception {
// Create mock initial item in DB
database.addTestItem(mockData);
// Click to view the item page
waitFor(() -> onData(anything())
.inAdapterView(withId(R.id.item_list))
.atPosition(0)
.perform(click()));
// Click on edit button
waitFor(() -> onView(withId(R.id.edit_button)).perform(scrollTo()));
onView(withId(R.id.edit_button)).perform(click());
waitFor(() -> {
onData(anything())
.inAdapterView(withId(R.id.item_list))
.atPosition(0)
.perform(click());
onView(withId(R.id.edit_button)).perform(scrollTo(), click());
});
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import androidx.test.core.app.ActivityScenario;

import com.example.househomey.signin.SignInActivity;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,20 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.pressImeActionButton;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasErrorText;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

import static com.example.househomey.testUtils.TestHelpers.enterText;
import static com.example.househomey.testUtils.TestHelpers.hasListLength;
import static com.example.househomey.testUtils.TestHelpers.waitFor;

import android.util.Log;

import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.Espresso;

import com.google.android.gms.tasks.OnSuccessListener;
import com.example.househomey.signin.SignInActivity;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.FirebaseFirestore;
Expand All @@ -32,9 +28,9 @@
import java.util.Objects;

public class SignUpFragmentTest {
private final FirebaseAuth auth = FirebaseAuth.getInstance();
private final FirebaseFirestore db = FirebaseFirestore.getInstance();
private ActivityScenario<SignInActivity> activityScenario;
FirebaseAuth auth = FirebaseAuth.getInstance();
FirebaseFirestore db = FirebaseFirestore.getInstance();

@Before
public void setUp() {
Expand All @@ -45,52 +41,50 @@ public void setUp() {

@Test
public void testUsername() {
onView(withId(R.id.signup_username)).perform(typeText("antonio2"));
enterText(R.id.signup_username, "antonio2");
onView(withId(R.id.signup_username)).perform(clearText());
onView(withId(R.id.signup_username)).check(matches(hasErrorText("username cannot be empty")));
onView(withId(R.id.signup_username)).perform(typeText("antonio2$"));
enterText(R.id.signup_username, "antonio2$");
onView(withId(R.id.signup_username)).check(matches(hasErrorText("only alphanumeric, underscore, or period")));
}

@Test
public void testEmail() {
onView(withId(R.id.signup_email)).perform(typeText("antonio2"));
enterText(R.id.signup_email, "antonio2");
onView(withId(R.id.signup_email)).check(matches(hasErrorText("not a valid email")));
onView(withId(R.id.signup_email)).perform(clearText());
onView(withId(R.id.signup_email)).check(matches(hasErrorText("email cannot be empty")));
}

@Test
public void testConfirmedPassword() {
onView(withId(R.id.signup_password)).perform(typeText("123456"));
onView(withId(R.id.signup_confirm_password)).perform(typeText("12345"));
enterText(R.id.signup_password, "123456");
enterText(R.id.signup_confirm_password, "12345");
onView(withId(R.id.signup_confirm_password)).check(matches(hasErrorText("passwords do not match")));
onView(withId(R.id.signup_confirm_password)).perform(clearText());
onView(withId(R.id.signup_confirm_password)).check(matches(hasErrorText("password cannot be empty")));
}

@Test
public void testRegister() {
onView(withId(R.id.signup_username)).perform(typeText("ESPRESSO_GENERAL_USER"));
onView(withId(R.id.signup_email)).perform(typeText("espresso_general_user@example.com"));
enterText(R.id.signup_username, "ESPRESSO_GENERAL_USER");
enterText(R.id.signup_email, "espresso_general_user@example.com");
enterText(R.id.signup_password, "12345");
enterText(R.id.signup_confirm_password, "12345");
onView(withId(R.id.signup_button)).perform(click());
onView(withId(R.id.signup_button)).perform(scrollTo(), click());
waitFor(()->onView(withId(R.id.signup_username)).check(matches(hasErrorText("username already exists"))));
enterText(R.id.signup_username, "a");
onView(withId(R.id.signup_button)).perform(click());
onView(withId(R.id.signup_button)).perform(scrollTo(), click());
waitFor(() -> onView(withId(R.id.signup_password)).check(matches(hasErrorText("The given password is invalid. [ Password should be at least 6 characters ]"))));
onView(withId(R.id.signup_confirm_password)).check(matches(hasErrorText("The given password is invalid. [ Password should be at least 6 characters ]")));
enterText(R.id.signup_password, "123456");
enterText(R.id.signup_confirm_password, "123456");
onView(withId(R.id.signup_button)).perform(click());
onView(withId(R.id.signup_button)).perform(scrollTo(), click());
waitFor(() -> onView(withId(R.id.signup_email)).check(matches(hasErrorText("The email address is already in use by another account."))));
enterText(R.id.signup_email, "a@example.com");
onView(withId(R.id.signup_button)).perform(click());
onView(withId(R.id.signup_button)).perform(scrollTo(), click());
waitFor(() -> onView(withId(R.id.signin_username)).check(matches(isDisplayed())));
enterText(R.id.signin_username, "a");
enterText(R.id.signin_password, "123456");
onView(withId(R.id.signin_button)).perform(click());
onView(withId(R.id.signin_button)).perform(scrollTo(), click());
waitFor(()->hasListLength(0));
DocumentReference docRef = db.collection("user").document("a");
docRef.delete().addOnSuccessListener(a -> Objects.requireNonNull(auth.getCurrentUser()).delete().addOnSuccessListener(aVoid -> Log.d("ESP-TEST", "Firebase Auth user deleted successfully"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.test.core.app.ActivityScenario;

import com.example.househomey.Item;
import com.example.househomey.MainActivity;
import com.example.househomey.item.Item;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.UserProfileChangeRequest;
import com.google.firebase.firestore.CollectionReference;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.FirebaseFirestore;
Expand All @@ -21,6 +22,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

Expand All @@ -34,10 +36,12 @@
public class DatabaseSetupRule<T extends Activity> implements TestRule {
private final String methodMatcher = "WithNewUser";
private final Class<T> activityClass;
private final int dbTimeoutInSeconds = 30;
private final FirebaseAuth auth = FirebaseAuth.getInstance();
private DocumentReference userDoc;
private boolean isDatabaseTest;
private final int dbTimeoutInSeconds = 30;
private T activity;
private final String password = "123456";

public DatabaseSetupRule(Class<T> activityClass) {
this.activityClass = activityClass;
Expand All @@ -50,12 +54,9 @@ public DatabaseSetupRule(Class<T> activityClass) {
* ex) proper page navigation, field validation, etc.
*/
public T setupActivity() {
Bundle userData = new Bundle();
userData.putString("username", isDatabaseTest ? userDoc.getId() : "ESPRESSO_GENERAL_USER");
ActivityScenario.launch(activityClass).onActivity(activity -> {
this.activity = activity;
Intent intent = new Intent(activity, MainActivity.class);
intent.putExtra("userData", userData);
activity.startActivity(intent);
});
return activity;
Expand All @@ -74,7 +75,8 @@ public void addTestItem(Map<String, Object> itemDetails) {
// Ensure that mock data can be used to create a valid Item
Item item;
try {
item = new Item("", itemDetails, userDoc.collection("tag"), item1 -> {});
item = new Item("", itemDetails, userDoc.collection("tag"), item1 -> {
});
} catch (NullPointerException e) {
throw new RuntimeException("Mock data cannot create a valid Item: " + e.getMessage());
}
Expand Down Expand Up @@ -105,6 +107,8 @@ public Statement apply(@NonNull Statement base, @NonNull Description description
public void evaluate() throws Throwable {
if (isDatabaseTest) {
createTestUser();
} else {
loginToGeneralTestUser();
}
try {
// Run the actual @Test method body
Expand All @@ -118,53 +122,84 @@ public void evaluate() throws Throwable {
};
}

public void loginToGeneralTestUser() {
CountDownLatch latch = new CountDownLatch(1);
auth.signInWithEmailAndPassword(getEmail(), password)
.addOnSuccessListener(authResult -> {
Log.d("ESP-TEST", "ESPRESSO_GENERAL_USER logged in successfully");
latch.countDown();
})
.addOnFailureListener(e -> {
Log.e("ESP-TEST", "Login failed for ESPRESSO_GENERAL_USER: " + e.getMessage());
latch.countDown();
});

// Wait for the login to finish
try {
if (!latch.await(dbTimeoutInSeconds, TimeUnit.SECONDS)) {
throw new RuntimeException("Timeout waiting for user login.");
}
} catch (InterruptedException e) {
throw new RuntimeException("Timeout waiting for user login.");
}
}


private void createTestUser() throws Exception {
FirebaseFirestore db = FirebaseFirestore.getInstance();
CountDownLatch latch = new CountDownLatch(1);
db.collection("user").add(new HashMap<String, Object>()).addOnCompleteListener(task -> {
if (task.isSuccessful()) {
userDoc = task.getResult();
latch.countDown();
} else {
throw new RuntimeException("Could not generate a unique test user.");
}
});
// Wait for user creation to finish

// Create a unique Firebase Auth test user and document
String email = getEmail();
auth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener(authResult -> auth.signInWithEmailAndPassword(email, password).addOnSuccessListener(aVoid -> {
UserProfileChangeRequest profileChangeRequest = new UserProfileChangeRequest.Builder().setDisplayName(email).build();
auth.getCurrentUser().updateProfile(profileChangeRequest);
db.collection("user").
document(email).set(new HashMap<>()).addOnSuccessListener(a -> {
userDoc = db.collection("user").document(email);
latch.countDown();
});
}).addOnFailureListener(e -> {
throw new RuntimeException("Could not create Firestore document for the test user: " + e.getMessage());
})).addOnFailureListener(e -> {
throw new RuntimeException("Could not create Firebase Auth user for the test: " + e.getMessage());
});

// Wait for user creation and Firestore document creation to finish
if (!latch.await(dbTimeoutInSeconds, TimeUnit.SECONDS)) {
throw new RuntimeException("Timeout waiting for test user creation.");
}
}

private void deleteTestUser() throws Exception {
private void deleteTestUser() {
if (userDoc != null) {
CountDownLatch latch = new CountDownLatch(1);
// Need to delete all nested collections before user doc can be deleted
deleteNestedDocuments(userDoc.collection("item"));
userDoc.delete()
.addOnSuccessListener(aVoid -> {
Log.d("ESP-TEST", "Unique test user document deleted successfully");
latch.countDown();
})
.addOnFailureListener(e -> {
Log.e("ESP-TEST", "Could not delete the unique test user: " + e.getMessage());
latch.countDown();
});
// Wait for all deletions to finish
if (!latch.await(dbTimeoutInSeconds, TimeUnit.SECONDS)) {
throw new RuntimeException("Timeout waiting for test user deletion.");
}
deleteNestedDocuments(userDoc.collection("tag"));

// Delete Firestore document
userDoc.delete().addOnSuccessListener(aVoid -> Log.d("ESP-TEST", "Unique test user document deleted successfully"))
.addOnFailureListener(e -> Log.e("ESP-TEST", "Could not delete the unique test user document: " + e.getMessage()));

// Delete Firebase Auth test user
auth.getCurrentUser().delete().addOnSuccessListener(aVoid -> Log.d("ESP-TEST", "Firebase Auth user deleted successfully"))
.addOnFailureListener(e -> Log.e("ESP-TEST", "Could not delete Firebase Auth user: " + e.getMessage()));
}
}

private void deleteNestedDocuments(CollectionReference collection) {
collection.get().addOnSuccessListener(queryDocumentSnapshots -> {
for (QueryDocumentSnapshot document : queryDocumentSnapshots) {
DocumentReference docRef = document.getReference();
docRef.delete()
.addOnSuccessListener(aVoid -> Log.d("ESP-TEST", "Nested document deleted successfully"))
.addOnFailureListener(e -> Log.e("ESP-TEST", "Could not delete nested document: " + e.getMessage()));
docRef.delete().addOnSuccessListener(aVoid -> Log.d("ESP-TEST", "Nested document deleted successfully")).addOnFailureListener(e -> Log.e("ESP-TEST", "Could not delete nested document: " + e.getMessage()));
}
}).addOnFailureListener(e -> Log.e("ESP-TEST", "Could not retrieve nested documents: " + e.getMessage()));
}

private String getEmail() {
if (isDatabaseTest) return UUID.randomUUID() + "@example.com";
return "espresso_general_user@example.com";
}
}

2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
android:windowSoftInputMode="adjustPan"
android:exported="true">
</activity>
<activity android:name=".SignInActivity"
<activity android:name=".signin.SignInActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/example/househomey/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import androidx.fragment.app.FragmentManager;

import com.example.househomey.form.AddItemFragment;
import com.example.househomey.home.HomeFragment;
import com.example.househomey.signin.SignInActivity;
import com.example.househomey.user.User;
import com.example.househomey.user.UserProfileFragment;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
Expand Down Expand Up @@ -106,5 +110,4 @@ public StorageReference getImageRef(String imageId) {
.child(user.getUsername())
.child(imageId);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.househomey.filter.model;

import com.example.househomey.Item;
import com.example.househomey.item.Item;

import java.util.ArrayList;
import java.util.Date;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.househomey.filter.model;

import com.example.househomey.Item;
import com.example.househomey.item.Item;

import java.io.Serializable;
import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.example.househomey.filter.model;


import com.example.househomey.home.HomeFragment;

import java.io.Serializable;

/**
* Callback interface for handling filter changes between fragments.
* @see com.example.househomey.HomeFragment
* @see HomeFragment
*/
public interface FilterCallback extends Serializable {
void onFilterApplied(Filter filter);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.example.househomey.filter.model;

import com.example.househomey.Item;
import com.google.android.material.chip.Chip;
import com.example.househomey.item.Item;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
Expand Down
Loading

0 comments on commit f95f336

Please sign in to comment.