package org.fdroid.fdroid.data;
import android.app.Application;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import org.fdroid.fdroid.Assert;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import org.fdroid.fdroid.data.Schema.RepoTable;
import org.fdroid.fdroid.mock.MockApk;
import org.fdroid.fdroid.mock.MockRepo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import static org.fdroid.fdroid.Assert.assertCantDelete;
import static org.fdroid.fdroid.Assert.assertResultCount;
import static org.fdroid.fdroid.Assert.insertApp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
// TODO: Use sdk=24 when Robolectric supports this
@Config(constants = BuildConfig.class, application = Application.class, sdk = 23)
@RunWith(RobolectricGradleTestRunner.class)
public class ApkProviderTest extends FDroidProviderTest {
private static final String[] PROJ = Cols.ALL;
@Test
public void testAppApks() {
App fdroidApp = insertApp(context, "org.fdroid.fdroid", "F-Droid");
App exampleApp = insertApp(context, "com.example", "Example");
for (int i = 1; i <= 10; i++) {
Assert.insertApk(context, fdroidApp, i);
Assert.insertApk(context, exampleApp, i);
}
assertTotalApkCount(20);
Cursor fdroidApks = contentResolver.query(
ApkProvider.getAppUri("org.fdroid.fdroid"),
PROJ,
null, null, null);
assertResultCount(10, fdroidApks);
assertBelongsToApp(fdroidApks, "org.fdroid.fdroid");
fdroidApks.close();
Cursor exampleApks = contentResolver.query(
ApkProvider.getAppUri("com.example"),
PROJ,
null, null, null);
assertResultCount(10, exampleApks);
assertBelongsToApp(exampleApks, "com.example");
exampleApks.close();
}
@Test
public void testInvalidDeleteUris() {
Apk apk = new MockApk("org.fdroid.fdroid", 10);
assertCantDelete(contentResolver, ApkProvider.getContentUri());
assertCantDelete(contentResolver, ApkProvider.getApkFromAnyRepoUri("org.fdroid.fdroid", 10));
assertCantDelete(contentResolver, ApkProvider.getApkFromAnyRepoUri(apk));
assertCantDelete(contentResolver, Uri.withAppendedPath(ApkProvider.getContentUri(), "some-random-path"));
}
private static final long REPO_KEEP = 1;
private static final long REPO_DELETE = 2;
@Test
public void testRepoApks() {
// Insert apks into two repos, one of which we will later purge the
// the apks from.
for (int i = 1; i <= 5; i++) {
insertApkForRepo("org.fdroid.fdroid", i, REPO_KEEP);
insertApkForRepo("com.example." + i, 1, REPO_DELETE);
}
for (int i = 6; i <= 10; i++) {
insertApkForRepo("org.fdroid.fdroid", i, REPO_DELETE);
insertApkForRepo("com.example." + i, 1, REPO_KEEP);
}
assertTotalApkCount(20);
Cursor cursor = contentResolver.query(
ApkProvider.getRepoUri(REPO_DELETE), PROJ, null, null, null);
assertResultCount(10, cursor);
assertBelongsToRepo(cursor, REPO_DELETE);
cursor.close();
int count = ApkProvider.Helper.deleteApksByRepo(context, new MockRepo(REPO_DELETE));
assertEquals(10, count);
assertTotalApkCount(10);
cursor = contentResolver.query(
ApkProvider.getRepoUri(REPO_DELETE), PROJ, null, null, null);
assertResultCount(0, cursor);
cursor.close();
// The only remaining apks should be those from REPO_KEEP.
assertBelongsToRepo(queryAllApks(), REPO_KEEP);
}
@Test
public void testQuery() {
Cursor cursor = queryAllApks();
assertNotNull(cursor);
cursor.close();
}
@Test
public void testInsert() {
// Start with an empty database...
Cursor cursor = queryAllApks();
assertNotNull(cursor);
assertEquals(0, cursor.getCount());
cursor.close();
Apk apk = new MockApk("org.fdroid.fdroid", 13);
// Insert a new record...
Assert.insertApk(context, apk.packageName, apk.versionCode);
cursor = queryAllApks();
assertNotNull(cursor);
assertEquals(1, cursor.getCount());
// And now we should be able to recover these values from the apk
// value object (because the queryAllApks() helper asks for VERSION_CODE and
// PACKAGE_NAME.
cursor.moveToFirst();
Apk toCheck = new Apk(cursor);
cursor.close();
assertEquals("org.fdroid.fdroid", toCheck.packageName);
assertEquals(13, toCheck.versionCode);
}
@Test(expected = IllegalArgumentException.class)
public void testCursorMustMoveToFirst() {
Assert.insertApk(context, "org.example.test", 12);
Cursor cursor = queryAllApks();
new Apk(cursor);
}
@Test
public void testCount() {
String[] projectionCount = new String[] {Cols._COUNT};
for (int i = 0; i < 13; i++) {
Assert.insertApk(context, "com.example", i);
}
Uri all = ApkProvider.getContentUri();
Cursor allWithFields = contentResolver.query(all, PROJ, null, null, null);
Cursor allWithCount = contentResolver.query(all, projectionCount, null, null, null);
assertResultCount(13, allWithFields);
allWithFields.close();
assertResultCount(1, allWithCount);
allWithCount.moveToFirst();
int countColumn = allWithCount.getColumnIndex(Cols._COUNT);
assertEquals(13, allWithCount.getInt(countColumn));
allWithCount.close();
}
@Test(expected = IllegalArgumentException.class)
public void testInsertWithInvalidExtraFieldDescription() {
assertInvalidExtraField(RepoTable.Cols.DESCRIPTION);
}
@Test(expected = IllegalArgumentException.class)
public void testInsertWithInvalidExtraFieldAddress() {
assertInvalidExtraField(RepoTable.Cols.ADDRESS);
}
@Test(expected = IllegalArgumentException.class)
public void testInsertWithInvalidExtraFieldFingerprint() {
assertInvalidExtraField(RepoTable.Cols.FINGERPRINT);
}
@Test(expected = IllegalArgumentException.class)
public void testInsertWithInvalidExtraFieldName() {
assertInvalidExtraField(RepoTable.Cols.NAME);
}
@Test(expected = IllegalArgumentException.class)
public void testInsertWithInvalidExtraFieldSigningCert() {
assertInvalidExtraField(RepoTable.Cols.SIGNING_CERT);
}
public void assertInvalidExtraField(String field) {
ContentValues invalidRepo = new ContentValues();
invalidRepo.put(field, "Test data");
Assert.insertApk(context, "org.fdroid.fdroid", 10, invalidRepo);
}
@Test
public void testInsertWithValidExtraFields() {
assertResultCount(0, queryAllApks());
ContentValues values = new ContentValues();
values.put(Cols.REPO_ID, 10);
values.put(Cols.Repo.ADDRESS, "http://example.com");
values.put(Cols.Repo.VERSION, 3);
values.put(Cols.FEATURES, "Some features");
Uri uri = Assert.insertApk(context, "com.example.com", 1, values);
assertResultCount(1, queryAllApks());
String[] projections = Cols.ALL;
Cursor cursor = contentResolver.query(uri, projections, null, null, null);
cursor.moveToFirst();
Apk apk = new Apk(cursor);
cursor.close();
// These should have quietly been dropped when we tried to save them,
// because the provider only knows how to query them (not update them).
assertEquals(null, apk.repoAddress);
assertEquals(0, apk.repoVersion);
// But this should have saved correctly...
assertEquals(1, apk.features.length);
assertEquals("Some features", apk.features[0]);
assertEquals("com.example.com", apk.packageName);
assertEquals(1, apk.versionCode);
assertEquals(10, apk.repo);
}
@Test
public void testKnownApks() {
App fdroid = Assert.ensureApp(context, "org.fdroid.fdroid");
for (int i = 0; i < 7; i++) {
Assert.insertApk(context, fdroid, i);
}
App exampleOrg = Assert.ensureApp(context, "org.example");
for (int i = 0; i < 9; i++) {
Assert.insertApk(context, exampleOrg, i);
}
App exampleCom = Assert.ensureApp(context, "com.example");
for (int i = 0; i < 3; i++) {
Assert.insertApk(context, exampleCom, i);
}
App thingo = Assert.ensureApp(context, "com.apk.thingo");
Assert.insertApk(context, thingo, 1);
Apk[] known = {
new MockApk(fdroid, 1),
new MockApk(fdroid, 3),
new MockApk(fdroid, 5),
new MockApk(exampleCom, 1),
new MockApk(exampleCom, 2),
};
Apk[] unknown = {
new MockApk(fdroid, 7),
new MockApk(fdroid, 9),
new MockApk(fdroid, 11),
new MockApk(fdroid, 13),
new MockApk(exampleCom, 3),
new MockApk(exampleCom, 4),
new MockApk(exampleCom, 5),
new MockApk(-10, 1),
new MockApk(-10, 2),
};
List<Apk> apksToCheck = new ArrayList<>(known.length + unknown.length);
Collections.addAll(apksToCheck, known);
Collections.addAll(apksToCheck, unknown);
String[] projection = {
Cols.Package.PACKAGE_NAME,
Cols.APP_ID,
Cols.VERSION_CODE,
};
List<Apk> knownApks = ApkProvider.Helper.knownApks(context, apksToCheck, projection);
assertResultCount(known.length, knownApks);
for (Apk knownApk : knownApks) {
assertContains(knownApks, knownApk);
}
}
@Test
public void testFindByApp() {
for (int i = 0; i < 7; i++) {
Assert.insertApk(context, "org.fdroid.fdroid", i);
}
for (int i = 0; i < 9; i++) {
Assert.insertApk(context, "org.example", i);
}
for (int i = 0; i < 3; i++) {
Assert.insertApk(context, "com.example", i);
}
Assert.insertApk(context, "com.apk.thingo", 1);
assertTotalApkCount(7 + 9 + 3 + 1);
List<Apk> fdroidApks = ApkProvider.Helper.findByPackageName(context, "org.fdroid.fdroid");
assertResultCount(7, fdroidApks);
assertBelongsToApp(fdroidApks, "org.fdroid.fdroid");
List<Apk> exampleApks = ApkProvider.Helper.findByPackageName(context, "org.example");
assertResultCount(9, exampleApks);
assertBelongsToApp(exampleApks, "org.example");
List<Apk> exampleApks2 = ApkProvider.Helper.findByPackageName(context, "com.example");
assertResultCount(3, exampleApks2);
assertBelongsToApp(exampleApks2, "com.example");
List<Apk> thingoApks = ApkProvider.Helper.findByPackageName(context, "com.apk.thingo");
assertResultCount(1, thingoApks);
assertBelongsToApp(thingoApks, "com.apk.thingo");
}
@Test
public void testUpdate() {
Uri apkUri = Assert.insertApk(context, "com.example", 10);
String[] allFields = Cols.ALL;
Cursor cursor = contentResolver.query(apkUri, allFields, null, null, null);
assertResultCount(1, cursor);
cursor.moveToFirst();
Apk apk = new Apk(cursor);
cursor.close();
assertEquals("com.example", apk.packageName);
assertEquals(10, apk.versionCode);
assertNull(apk.features);
assertNull(apk.added);
assertNull(apk.hashType);
apk.features = new String[] {"one", "two", "three" };
long dateTimestamp = System.currentTimeMillis();
apk.added = new Date(dateTimestamp);
apk.hashType = "i'm a hash type";
ApkProvider.Helper.update(context, apk);
// Should not have inserted anything else, just updated the already existing apk.
Cursor allCursor = contentResolver.query(ApkProvider.getContentUri(), allFields, null, null, null);
assertResultCount(1, allCursor);
allCursor.close();
Cursor updatedCursor = contentResolver.query(apkUri, allFields, null, null, null);
assertResultCount(1, updatedCursor);
updatedCursor.moveToFirst();
Apk updatedApk = new Apk(updatedCursor);
updatedCursor.close();
assertEquals("com.example", updatedApk.packageName);
assertEquals(10, updatedApk.versionCode);
assertNotNull(updatedApk.features);
assertNotNull(updatedApk.added);
assertNotNull(updatedApk.hashType);
assertEquals(3, updatedApk.features.length);
assertEquals("one", updatedApk.features[0]);
assertEquals("two", updatedApk.features[1]);
assertEquals("three", updatedApk.features[2]);
assertEquals(new Date(dateTimestamp).getYear(), updatedApk.added.getYear());
assertEquals(new Date(dateTimestamp).getMonth(), updatedApk.added.getMonth());
assertEquals(new Date(dateTimestamp).getDay(), updatedApk.added.getDay());
assertEquals("i'm a hash type", updatedApk.hashType);
}
@Test
public void testFind() {
// Insert some random apks either side of the "com.example", so that
// the Helper.find() method doesn't stumble upon the app we are interested
// in by shear dumb luck...
for (int i = 0; i < 10; i++) {
Assert.insertApk(context, "org.fdroid.apk." + i, i);
}
App exampleApp = insertApp(context, "com.example", "Example");
ContentValues values = new ContentValues();
values.put(Cols.VERSION_NAME, "v1.1");
values.put(Cols.HASH, "xxxxyyyy");
values.put(Cols.HASH_TYPE, "a hash type");
Assert.insertApk(context, exampleApp, 11, values);
// ...and a few more for good measure...
for (int i = 15; i < 20; i++) {
Assert.insertApk(context, "com.other.thing." + i, i);
}
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(context, "com.example", 11);
assertNotNull(apk);
// The find() method populates ALL fields if you don't specify any,
// so we expect to find each of the ones we inserted above...
assertEquals("com.example", apk.packageName);
assertEquals(11, apk.versionCode);
assertEquals("v1.1", apk.versionName);
assertEquals("xxxxyyyy", apk.hash);
assertEquals("a hash type", apk.hashType);
String[] projection = {
Cols.Package.PACKAGE_NAME,
Cols.HASH,
};
Apk apkLessFields = ApkProvider.Helper.findApkFromAnyRepo(context, "com.example", 11, projection);
assertNotNull(apkLessFields);
assertEquals("com.example", apkLessFields.packageName);
assertEquals("xxxxyyyy", apkLessFields.hash);
// Didn't ask for these fields, so should be their default values...
assertNull(apkLessFields.hashType);
assertNull(apkLessFields.versionName);
assertEquals(0, apkLessFields.versionCode);
Apk notFound = ApkProvider.Helper.findApkFromAnyRepo(context, "com.doesnt.exist", 1000);
assertNull(notFound);
}
protected final Cursor queryAllApks() {
return contentResolver.query(ApkProvider.getContentUri(), PROJ, null, null, null);
}
protected void assertContains(List<Apk> apks, Apk apk) {
boolean found = false;
for (Apk a : apks) {
if (a.versionCode == apk.versionCode && a.packageName.equals(apk.packageName)) {
found = true;
break;
}
}
if (!found) {
fail("Apk [" + apk + "] not found in " + Assert.listToString(apks));
}
}
protected void assertBelongsToApp(Cursor apks, String appId) {
assertBelongsToApp(ApkProvider.Helper.cursorToList(apks), appId);
}
protected void assertBelongsToApp(List<Apk> apks, String appId) {
for (Apk apk : apks) {
assertEquals(appId, apk.packageName);
}
}
protected void assertTotalApkCount(int expected) {
assertResultCount(expected, queryAllApks());
}
protected void assertBelongsToRepo(Cursor apkCursor, long repoId) {
for (Apk apk : ApkProvider.Helper.cursorToList(apkCursor)) {
assertEquals(repoId, apk.repo);
}
}
protected Apk insertApkForRepo(String id, int versionCode, long repoId) {
ContentValues additionalValues = new ContentValues();
additionalValues.put(Cols.REPO_ID, repoId);
Uri uri = Assert.insertApk(context, id, versionCode, additionalValues);
return ApkProvider.Helper.get(context, uri);
}
}