package org.commcare; import android.content.Context; import android.content.Intent; import android.util.Log; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.mocks.ModernHttpRequesterMock; import org.commcare.android.util.TestUtils; import org.commcare.core.encryption.CryptUtil; import org.commcare.core.network.ModernHttpRequester; import org.commcare.dalvik.BuildConfig; import org.commcare.models.AndroidPrototypeFactory; import org.commcare.models.database.AndroidPrototypeFactorySetup; import org.commcare.models.database.HybridFileBackedSqlStorage; import org.commcare.models.database.HybridFileBackedSqlStorageMock; import org.commcare.models.encryption.ByteEncrypter; import org.commcare.modern.util.Pair; import org.commcare.network.DataPullRequester; import org.commcare.network.HttpUtils; import org.commcare.network.LocalReferencePullResponseFactory; import org.commcare.services.CommCareSessionService; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.model.User; import org.javarosa.core.reference.ReferenceManager; import org.javarosa.core.reference.ResourceReferenceFactory; import org.javarosa.core.services.storage.Persistable; import org.javarosa.core.util.externalizable.PrototypeFactory; import org.junit.Assert; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.TestLifecycleApplication; import org.robolectric.util.ServiceController; import java.io.File; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import static junit.framework.Assert.fail; /** * @author Phillip Mates (pmates@dimagi.com). */ public class CommCareTestApplication extends CommCareApplication implements TestLifecycleApplication { private static final String TAG = CommCareTestApplication.class.getSimpleName(); private static PrototypeFactory testPrototypeFactory; private static final ArrayList<String> factoryClassNames = new ArrayList<>(); private String cachedUserPassword; private final ArrayList<Throwable> asyncExceptions = new ArrayList<>(); @Override public void onCreate() { super.onCreate(); // allow "jr://resource" references ReferenceManager.instance().addReferenceFactory(new ResourceReferenceFactory()); Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { asyncExceptions.add(ex); Assert.fail(ex.getMessage()); } }); } @Override public <T extends Persistable> HybridFileBackedSqlStorage<T> getFileBackedAppStorage(String name, Class<T> c) { return getCurrentApp().getFileBackedStorage(name, c); } @Override public <T extends Persistable> HybridFileBackedSqlStorage<T> getFileBackedUserStorage(String storage, Class<T> c) { return new HybridFileBackedSqlStorageMock<>(storage, c, buildUserDbHandle(), getUserKeyRecordId()); } @Override public CommCareApp getCurrentApp() { CommCareApp superApp = super.getCurrentApp(); if (superApp == null) { return null; } else { return new CommCareTestApp(superApp); } } @Override public PrototypeFactory getPrototypeFactory(Context c) { TestUtils.disableSqlOptimizations(); if (testPrototypeFactory != null) { return testPrototypeFactory; } // Sort of hack-y way to get the classfile dirs initFactoryClassList(); try { testPrototypeFactory = new AndroidPrototypeFactory(new HashSet<>(factoryClassNames)); } catch (Exception e) { throw new RuntimeException(e); } return testPrototypeFactory; } /** * Get externalizable classes from *.class files in build dirs. Used to * build PrototypeFactory that mirrors a prod environment */ private static void initFactoryClassList() { if (factoryClassNames.isEmpty()) { String baseODK = BuildConfig.BUILD_DIR + "/intermediates/classes/commcare/debug/"; String baseCC = BuildConfig.PROJECT_DIR + "/../../commcare-core/build/classes/main/"; addExternalizableClassesFromDir(baseODK.replace("/", File.separator), factoryClassNames); addExternalizableClassesFromDir(baseCC.replace("/", File.separator), factoryClassNames); } } private static void addExternalizableClassesFromDir(String baseClassPath, List<String> externClasses) { try { File f = new File(baseClassPath); ArrayList<File> files = new ArrayList<>(); getFilesInDir(f, files); for (File file : files) { String className = file.getAbsolutePath() .replace(baseClassPath, "") .replace(File.separator, ".") .replace(".class", "") .replace(".class", ""); AndroidPrototypeFactorySetup.loadClass(className, externClasses); } } catch (Exception e) { Log.w(TAG, e.getMessage()); } } /** * @return Names of externalizable classes loaded from *.class files in the build dir */ public static List<String> getTestPrototypeFactoryClasses() { initFactoryClassList(); return factoryClassNames; } private static void getFilesInDir(File currentFile, ArrayList<File> acc) { for (File f : currentFile.listFiles()) { if (f.isFile()) { acc.add(f); } else { getFilesInDir(f, acc); } } } @Override public void startUserSession(byte[] symetricKey, UserKeyRecord record, boolean restoreSession) { // manually create/setup session service because robolectric doesn't // really support services CommCareSessionService ccService = startRoboCommCareService(); ccService.createCipherPool(); ccService.prepareStorage(symetricKey, record); User user = getUserFromDb(ccService, record); if (user == null && cachedUserPassword != null) { Log.d(TAG, "No user instance found, creating one"); user = new User(record.getUsername(), cachedUserPassword, "some_unique_id"); CommCareApplication.instance().getRawStorage("USER", User.class, ccService.getUserDbHandle()).write(user); } if (user != null) { user.setCachedPwd(cachedUserPassword); user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateSemiRandomKey().getEncoded(), cachedUserPassword)); } ccService.startSession(user, record); CommCareApplication.instance().setTestingService(ccService); } private static CommCareSessionService startRoboCommCareService() { Intent startIntent = new Intent(RuntimeEnvironment.application, CommCareSessionService.class); ServiceController<CommCareSessionService> serviceController = Robolectric.buildService(CommCareSessionService.class, startIntent); serviceController.attach() .create() .startCommand(0, 1); return serviceController.get(); } private static User getUserFromDb(CommCareSessionService ccService, UserKeyRecord keyRecord) { for (User u : CommCareApplication.instance().getRawStorage("USER", User.class, ccService.getUserDbHandle())) { if (keyRecord.getUsername().equals(u.getUsername())) { return u; } } return null; } public void setCachedUserPassword(String password) { cachedUserPassword = password; } @Override public ModernHttpRequester buildHttpRequesterForLoggedInUser(Context context, URL url, HashMap<String, String> params, boolean isAuthenticatedRequest, boolean isPostRequest) { Pair<User, String> userAndDomain = HttpUtils.getUserAndDomain(isAuthenticatedRequest); return new ModernHttpRequesterMock(new AndroidCacheDirSetup(context), url, params, userAndDomain.first, userAndDomain.second, isAuthenticatedRequest, isPostRequest); } @Override public DataPullRequester getDataPullRequester() { return LocalReferencePullResponseFactory.INSTANCE; } @Override public void afterTest(Method method) { Robolectric.flushBackgroundThreadScheduler(); if (!asyncExceptions.isEmpty()) { for(Throwable throwable: asyncExceptions) { throwable.printStackTrace(); fail("Test failed due to " + asyncExceptions.size() + " threads crashing off the main thread. See error log for more details"); } } } @Override public void beforeTest(Method method) { } @Override public void prepareTest(Object test) { } }