From 5d58a396156e4b26e5aa06c9170c78f3cd885816 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Mar 2021 13:55:02 +0000 Subject: [PATCH 01/10] Enable 3p cookies when the google auth flow requires so --- .../31.json | 872 ++++++++++++++++++ .../app/browser/BrowserTabViewModelTest.kt | 65 +- .../app/browser/BrowserWebViewClientTest.kt | 13 +- .../cookies/AppThirdPartyCookieManagerTest.kt | 183 ++++ .../db/AllowedDomainsRepositoryTest.kt | 118 +++ .../app/global/db/AppDatabaseTest.kt | 5 + .../view/ClearPersonalDataActionTest.kt | 11 +- .../app/browser/BrowserTabFragment.kt | 14 + .../app/browser/BrowserTabViewModel.kt | 10 +- .../app/browser/BrowserWebViewClient.kt | 10 +- .../cookies/ThirdPartyCookieManager.kt | 95 ++ .../browser/cookies/db/AllowedDomainEntity.kt | 26 + .../browser/cookies/db/AllowedDomainsDao.kt | 39 + .../cookies/db/AllowedDomainsRepository.kt | 62 ++ .../app/browser/di/BrowserModule.kt | 14 +- .../java/com/duckduckgo/app/di/DaoModule.kt | 3 + .../com/duckduckgo/app/di/PrivacyModule.kt | 7 +- .../duckduckgo/app/global/db/AppDatabase.kt | 17 +- .../global/view/ClearPersonalDataAction.kt | 5 +- .../app/tabs/model/TabDataRepository.kt | 12 + .../app/tabs/model/TabRepository.kt | 3 + 21 files changed, 1552 insertions(+), 32 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json create mode 100644 app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt create mode 100644 app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json new file mode 100644 index 000000000000..baf152b33277 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json @@ -0,0 +1,872 @@ +{ + "formatVersion": 1, + "database": { + "version": 31, + "identityHash": "11c0c226b52d931b47fb5abe382c33e9", + "entities": [ + { + "tableName": "tds_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `defaultAction` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `categories` TEXT NOT NULL, `rules` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultAction", + "columnName": "defaultAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `displayName` TEXT NOT NULL, `prevalence` REAL NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevalence", + "columnName": "prevalence", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_domain_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `entityName` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityName", + "columnName": "entityName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporary_tracking_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_bloom_filter_spec", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `bitCount` INTEGER NOT NULL, `errorRate` REAL NOT NULL, `totalEntries` INTEGER NOT NULL, `sha256` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bitCount", + "columnName": "bitCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorRate", + "columnName": "errorRate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalEntries", + "columnName": "totalEntries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_false_positive_domain", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`networkName`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sites_visited", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tabId` TEXT NOT NULL, `url` TEXT, `title` TEXT, `skipHome` INTEGER NOT NULL, `viewed` INTEGER NOT NULL, `position` INTEGER NOT NULL, `tabPreviewFile` TEXT, `sourceTabId` TEXT, `deletable` INTEGER NOT NULL, PRIMARY KEY(`tabId`), FOREIGN KEY(`sourceTabId`) REFERENCES `tabs`(`tabId`) ON UPDATE SET NULL ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "skipHome", + "columnName": "skipHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewed", + "columnName": "viewed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabPreviewFile", + "columnName": "tabPreviewFile", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceTabId", + "columnName": "sourceTabId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deletable", + "columnName": "deletable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "SET NULL", + "columns": [ + "sourceTabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "tab_selection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tabId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`tabId`) REFERENCES `tabs`(`tabId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tab_selection_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "tabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`surveyId` TEXT NOT NULL, `url` TEXT, `daysInstalled` INTEGER, `status` TEXT NOT NULL, PRIMARY KEY(`surveyId`))", + "fields": [ + { + "fieldPath": "surveyId", + "columnName": "surveyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "daysInstalled", + "columnName": "daysInstalled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "surveyId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dismissed_cta", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ctaId` TEXT NOT NULL, PRIMARY KEY(`ctaId`))", + "fields": [ + { + "fieldPath": "ctaId", + "columnName": "ctaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "ctaId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_days_used", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, PRIMARY KEY(`date`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_enjoyment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventType` INTEGER NOT NULL, `promptCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `primaryKey` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "promptCount", + "columnName": "promptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryKey", + "columnName": "primaryKey", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "primaryKey" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notificationId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "privacy_protection_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `blocked_tracker_count` INTEGER NOT NULL, `upgrade_count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedTrackerCount", + "columnName": "blocked_tracker_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upgradeCount", + "columnName": "upgrade_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UncaughtExceptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL, `version` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionSource", + "columnName": "exceptionSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tdsMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `eTag` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userStage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `appStage` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appStage", + "columnName": "appStage", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "fireproofWebsites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "locationPermissions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `permission` INTEGER NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permission", + "columnName": "permission", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pixel_store", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `pixelName` TEXT NOT NULL, `atb` TEXT NOT NULL, `additionalQueryParams` TEXT NOT NULL, `encodedQueryParams` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pixelName", + "columnName": "pixelName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "atb", + "columnName": "atb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "additionalQueryParams", + "columnName": "additionalQueryParams", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encodedQueryParams", + "columnName": "encodedQueryParams", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "allowed_domains", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11c0c226b52d931b47fb5abe382c33e9')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 06aca8461993..cf62583cea05 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -114,6 +114,8 @@ import io.reactivex.Observable import io.reactivex.Single import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking @@ -173,7 +175,7 @@ class BrowserTabViewModelTest { private lateinit var mockOmnibarConverter: OmnibarEntryConverter @Mock - private lateinit var mockTabsRepository: TabRepository + private lateinit var mockTabsRepositoryTabViewModel: TabRepository @Mock private lateinit var webViewSessionStorage: WebViewSessionStorage @@ -230,7 +232,7 @@ class BrowserTabViewModelTest { private lateinit var mockFileDownloader: FileDownloader @Mock - private lateinit var mockTabRepository: TabRepository + private lateinit var mockTabRepositoryCtaViewModel: TabRepository @Mock private lateinit var geoLocationPermissions: GeoLocationPermissions @@ -263,6 +265,10 @@ class BrowserTabViewModelTest { private val dismissedCtaDaoChannel = Channel>() + private val childClosedTabsSharedFlow = MutableSharedFlow() + + private val childClosedTabsFlow = childClosedTabsSharedFlow.asSharedFlow() + @Before fun before() { MockitoAnnotations.openMocks(this) @@ -276,7 +282,7 @@ class BrowserTabViewModelTest { val fireproofWebsiteRepository = FireproofWebsiteRepository(fireproofWebsiteDao, coroutineRule.testDispatcherProvider, lazyFaviconManager) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow()) - whenever(mockTabRepository.flowTabs).thenReturn(flowOf(emptyList())) + whenever(mockTabRepositoryCtaViewModel.flowTabs).thenReturn(flowOf(emptyList())) ctaViewModel = CtaViewModel( mockAppInstallStore, @@ -291,7 +297,7 @@ class BrowserTabViewModelTest { mockUserStageStore, mockUserEventsStore, UseOurAppDetector(mockUserEventsStore), - mockTabRepository, + mockTabRepositoryCtaViewModel, coroutineRule.testDispatcherProvider ) @@ -299,9 +305,10 @@ class BrowserTabViewModelTest { whenever(mockOmnibarConverter.convertQueryToUrl(any(), any())).thenReturn("duckduckgo.com") whenever(mockVariantManager.getVariant()).thenReturn(DEFAULT_VARIANT) - whenever(mockTabsRepository.liveSelectedTab).thenReturn(selectedTabLiveData) + whenever(mockTabsRepositoryTabViewModel.liveSelectedTab).thenReturn(selectedTabLiveData) whenever(mockNavigationAwareLoginDetector.loginEventLiveData).thenReturn(loginEventLiveData) - whenever(mockTabsRepository.retrieveSiteData(any())).thenReturn(MutableLiveData()) + whenever(mockTabsRepositoryTabViewModel.retrieveSiteData(any())).thenReturn(MutableLiveData()) + whenever(mockTabsRepositoryTabViewModel.childClosedTabs).thenReturn(childClosedTabsFlow) whenever(mockPrivacyPractices.privacyPracticesFor(any())).thenReturn(PrivacyPractices.UNKNOWN) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) whenever(mockUserWhitelistDao.contains(anyString())).thenReturn(false) @@ -312,7 +319,7 @@ class BrowserTabViewModelTest { queryUrlConverter = mockOmnibarConverter, duckDuckGoUrlDetector = DuckDuckGoUrlDetector(), siteFactory = siteFactory, - tabRepository = mockTabsRepository, + tabRepository = mockTabsRepositoryTabViewModel, userWhitelistDao = mockUserWhitelistDao, networkLeaderboardDao = mockNetworkLeaderboardDao, autoComplete = mockAutoCompleteApi, @@ -389,7 +396,7 @@ class BrowserTabViewModelTest { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) assertTrue(commandCaptor.lastValue is Command.OpenInNewBackgroundTab) - verify(mockTabsRepository).addNewTabAfterExistingTab(url, "abc") + verify(mockTabsRepositoryTabViewModel).addNewTabAfterExistingTab(url, "abc") } @Test @@ -553,7 +560,7 @@ class BrowserTabViewModelTest { testee.onUserSubmittedQuery("foo") coroutineRule.runBlocking { - verify(mockTabsRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } } @@ -1218,7 +1225,7 @@ class BrowserTabViewModelTest { testee.onRefreshRequested() coroutineRule.runBlocking { - verify(mockTabsRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } } @@ -1519,7 +1526,7 @@ class BrowserTabViewModelTest { showErrorWithAction.action() coroutineRule.runBlocking { - verify(mockTabsRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } } @@ -1691,7 +1698,7 @@ class BrowserTabViewModelTest { fun whenCloseCurrentTabSelectedThenTabDeletedFromRepository() = runBlocking { givenOneActiveTabSelected() testee.closeCurrentTab() - verify(mockTabsRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } @Test @@ -1723,7 +1730,7 @@ class BrowserTabViewModelTest { testee.onUserPressedBack() - verify(mockTabsRepository).deleteTabAndSelectSource("TAB_ID") + verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource("TAB_ID") } @Test @@ -2870,7 +2877,7 @@ class BrowserTabViewModelTest { testee.prefetchFavicon(url) - verify(mockTabsRepository).updateTabFavicon("TAB_ID", file.name) + verify(mockTabsRepositoryTabViewModel).updateTabFavicon("TAB_ID", file.name) } @Test @@ -2879,7 +2886,7 @@ class BrowserTabViewModelTest { testee.prefetchFavicon("url") - verify(mockTabsRepository, never()).updateTabFavicon(any(), any()) + verify(mockTabsRepositoryTabViewModel, never()).updateTabFavicon(any(), any()) } @Test @@ -2901,7 +2908,7 @@ class BrowserTabViewModelTest { testee.iconReceived(bitmap) - verify(mockTabsRepository).updateTabFavicon("TAB_ID", file.name) + verify(mockTabsRepositoryTabViewModel).updateTabFavicon("TAB_ID", file.name) } @Test @@ -2912,7 +2919,7 @@ class BrowserTabViewModelTest { testee.iconReceived(bitmap) - verify(mockTabsRepository, never()).updateTabFavicon(any(), any()) + verify(mockTabsRepositoryTabViewModel, never()).updateTabFavicon(any(), any()) } @Test @@ -3136,6 +3143,24 @@ class BrowserTabViewModelTest { } } + @Test + fun whenChildrenTabClosedIfViewModelIsParentThenChildTabClosedCommandSent() = coroutineRule.runBlocking { + givenOneActiveTabSelected() + + childClosedTabsSharedFlow.emit("TAB_ID") + + assertCommandIssued() + } + + @Test + fun whenChildrenTabClosedIfViewModelIsNotParentThenChildTabClosedCommandNotSent() = coroutineRule.runBlocking { + givenOneActiveTabSelected() + + childClosedTabsSharedFlow.emit("other_tab") + + assertCommandNotIssued() + } + private suspend fun givenFireButtonPulsing() { whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) dismissedCtaDaoChannel.send(listOf(DismissedCta(CtaId.DAX_DIALOG_TRACKERS_FOUND))) @@ -3222,7 +3247,7 @@ class BrowserTabViewModelTest { whenever(site.url).thenReturn(USE_OUR_APP_DOMAIN) val siteLiveData = MutableLiveData() siteLiveData.value = site - whenever(mockTabsRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + whenever(mockTabsRepositoryTabViewModel.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) testee.loadData("TAB_ID", USE_OUR_APP_DOMAIN, false) } @@ -3232,7 +3257,7 @@ class BrowserTabViewModelTest { whenever(site.url).thenReturn("example.com") val siteLiveData = MutableLiveData() siteLiveData.value = site - whenever(mockTabsRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + whenever(mockTabsRepositoryTabViewModel.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) testee.loadData("TAB_ID", "example.com", false) } @@ -3250,7 +3275,7 @@ class BrowserTabViewModelTest { whenever(site.uri).thenReturn(Uri.parse(domain)) val siteLiveData = MutableLiveData() siteLiveData.value = site - whenever(mockTabsRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + whenever(mockTabsRepositoryTabViewModel.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) testee.loadData("TAB_ID", domain, false) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index 96c83ff0416e..b645523882c5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -19,11 +19,13 @@ package com.duckduckgo.app.browser import android.content.Context import android.os.Build import android.webkit.* +import androidx.core.net.toUri import androidx.test.annotation.UiThreadTest import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore import com.duckduckgo.app.browser.logindetection.DOMLoginDetector import com.duckduckgo.app.browser.logindetection.WebNavigationEvent @@ -59,6 +61,7 @@ class BrowserWebViewClientTest { private val globalPrivacyControl: GlobalPrivacyControl = mock() private val trustedCertificateStore: TrustedCertificateStore = mock() private val webViewHttpAuthStore: WebViewHttpAuthStore = mock() + private val thirdPartyCookieManager: ThirdPartyCookieManager = mock() @UiThreadTest @Before @@ -75,7 +78,8 @@ class BrowserWebViewClientTest { cookieManager, loginDetector, dosDetector, - globalPrivacyControl + globalPrivacyControl, + thirdPartyCookieManager ) testee.webViewClientListener = listener } @@ -117,6 +121,13 @@ class BrowserWebViewClientTest { verify(globalPrivacyControl).injectDoNotSellToDom(webView) } + @UiThreadTest + @Test + fun whenOnPageStartedCalledThenProcessUriForThirdPartyCookiesCalled() = coroutinesTestRule.runBlocking { + testee.onPageStarted(webView, EXAMPLE_URL, null) + verify(thirdPartyCookieManager).processUriForThirdPartyCookies(webView, EXAMPLE_URL.toUri()) + } + @UiThreadTest @Test fun whenOnPageFinishedCalledThenListenerInstructedToUpdateNavigationState() { diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt new file mode 100644 index 000000000000..335344ff2650 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.cookies + +import android.content.Context +import android.webkit.CookieManager +import android.webkit.WebView +import androidx.core.net.toUri +import androidx.room.Room +import androidx.test.annotation.UiThreadTest +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.browser.cookies.db.AllowedDomainsDao +import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.runBlocking +import kotlinx.coroutines.withContext +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class AppThirdPartyCookieManagerTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private val cookieManager = CookieManager.getInstance() + private lateinit var db: AppDatabase + private lateinit var allowedDomainsDao: AllowedDomainsDao + private lateinit var allowedDomainsRepository: AllowedDomainsRepository + private lateinit var testee: AppThirdPartyCookieManager + private lateinit var webView: WebView + + @UiThreadTest + @Before + fun setup() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) + .allowMainThreadQueries() + .build() + allowedDomainsDao = db.allowedDomainsDao() + allowedDomainsRepository = AllowedDomainsRepository(allowedDomainsDao, coroutinesTestRule.testDispatcherProvider) + webView = TestWebView(InstrumentationRegistry.getInstrumentation().targetContext) + + testee = AppThirdPartyCookieManager(cookieManager, allowedDomainsRepository) + } + + @UiThreadTest + @After + fun after() { + cookieManager.removeAllCookies { } + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAndIsNotInTheListThenThirdPartyCookiesDisabled() = coroutinesTestRule.runBlocking { + testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) + + assertFalse(cookieManager.acceptThirdPartyCookies(webView)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAndIsInTheListAndHasCookieThenThirdPartyCookiesEnabled() = coroutinesTestRule.runBlocking { + givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) + givenUserIdCookieIsSet() + + testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) + + assertTrue(cookieManager.acceptThirdPartyCookies(webView)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAndIsInTheListAndDoesNotHaveCookieThenThirdPartyCookiesDisabled() = coroutinesTestRule.runBlocking { + givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) + + testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) + + assertFalse(cookieManager.acceptThirdPartyCookies(webView)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfDomainIsInTheListAndCookieIsSetThenDomainRemovedFromList() = coroutinesTestRule.runBlocking { + givenUserIdCookieIsSet() + givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) + + testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) + + assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfDomainIsInTheListAndCookieIsNotSetThenDomainRemovedFromList() = coroutinesTestRule.runBlocking { + givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) + + testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) + + assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfDomainIsInTheListAndIsFromExceptionListThenDomainNotRemovedFromList() = coroutinesTestRule.runBlocking { + givenDomainIsInTheThirdPartyCookieList(EXCLUDED_DOMAIN_URI.host!!) + + testee.processUriForThirdPartyCookies(webView, EXCLUDED_DOMAIN_URI) + + assertNotNull(allowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfUrlIsGoogleAuthAndIsTokenTypeThenDomainAddedToTheList() = coroutinesTestRule.runBlocking { + testee.processUriForThirdPartyCookies(webView, THIRD_PARTY_AUTH_URI) + + assertNotNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + } + + @UiThreadTest + @Test + fun whenProcessUriForThirdPartyCookiesIfUrlIsGoogleAuthAndIsNotTokenTypeThenDomainNotAddedToTheList() = coroutinesTestRule.runBlocking { + testee.processUriForThirdPartyCookies(webView, NON_THIRD_PARTY_AUTH_URI) + + assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + } + + @Test + fun whenClearAllDataThenDomainDeletedFromDatabase() = coroutinesTestRule.runBlocking { + givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) + + testee.clearAllData() + + assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + } + + @Test + fun whenClearAllDataIfDomainIsInExclusionListThenDomainNotDeletedFromDatabase() = coroutinesTestRule.runBlocking { + givenDomainIsInTheThirdPartyCookieList(EXCLUDED_DOMAIN_URI.host!!) + + testee.clearAllData() + + assertNotNull(allowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!)) + } + + private suspend fun givenDomainIsInTheThirdPartyCookieList(domain: String) = coroutinesTestRule.runBlocking { + withContext(coroutinesTestRule.testDispatcherProvider.io()) { + allowedDomainsRepository.addDomain(domain) + } + } + + private suspend fun givenUserIdCookieIsSet() { + withContext(coroutinesTestRule.testDispatcherProvider.main()) { + cookieManager.setCookie("https://accounts.google.com", "user_id=test") + } + } + + private class TestWebView(context: Context) : WebView(context) + + companion object { + val EXCLUDED_DOMAIN_URI = "http://home.nest.com".toUri() + val EXAMPLE_URI = "http://example.com".toUri() + val THIRD_PARTY_AUTH_URI = "https://accounts.google.com/o/oauth2/auth/identifier?response_type=permission%20id_token&ss_domain=https%3A%2F%2Fexample.com".toUri() + val NON_THIRD_PARTY_AUTH_URI = "https://accounts.google.com/o/oauth2/auth/identifier?response_type=code&ss_domain=https%3A%2F%2Fexample.com".toUri() + } +} diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt new file mode 100644 index 000000000000..fd49e86e567b --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.cookies.db + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.global.db.AppDatabase +import com.duckduckgo.app.runBlocking +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class AllowedDomainsRepositoryTest { + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() + + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + private lateinit var db: AppDatabase + private lateinit var allowedDomainsDao: AllowedDomainsDao + private lateinit var allowedDomainsRepository: AllowedDomainsRepository + + @Before + fun before() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) + .allowMainThreadQueries() + .build() + allowedDomainsDao = db.allowedDomainsDao() + allowedDomainsRepository = AllowedDomainsRepository(allowedDomainsDao, coroutineRule.testDispatcherProvider) + } + + @After + fun after() { + db.close() + } + + @Test + fun whenAddDomainIfIsEmptyThenReturnNull() = coroutineRule.runBlocking { + assertNull(allowedDomainsRepository.addDomain("")) + } + + @Test + fun whenAddDomainAndDomainNotValidThenReturnNull() = coroutineRule.runBlocking { + assertNull(allowedDomainsRepository.addDomain("https://example.com")) + } + + @Test + fun whenAddDomainWithSubDomainThenReturnNonNull() = coroutineRule.runBlocking { + assertNotNull(allowedDomainsRepository.addDomain("test.example.com")) + } + + @Test + fun whenAddDomainThenReturnNonNull() = coroutineRule.runBlocking { + assertNotNull(allowedDomainsRepository.addDomain("example.com")) + } + + @Test + fun whenGetDomainIfDomainExistsThenReturnAllowedDomainEntity() = coroutineRule.runBlocking { + givenAllowedDomain("example.com") + + val allowedDomainEntity = allowedDomainsRepository.getDomain("example.com") + assertEquals("example.com", allowedDomainEntity?.domain) + } + + @Test + fun whenGetDomainIfDomainDoesNotExistThenReturnNull() = coroutineRule.runBlocking { + val allowedDomainEntity = allowedDomainsRepository.getDomain("example.com") + assertNull(allowedDomainEntity) + } + + @Test + fun whenRemoveDomainThenDomainDeletedFromDatabase() = coroutineRule.runBlocking { + givenAllowedDomain("example.com") + val allowedDomainEntity = allowedDomainsRepository.getDomain("example.com") + + allowedDomainsRepository.removeDomain(allowedDomainEntity!!) + + val deletedEntity = allowedDomainsRepository.getDomain("example.com") + assertNull(deletedEntity) + } + + @Test + fun whenDeleteAllThenAllDomainsDeletedExceptFromTheExceptionList() = coroutineRule.runBlocking { + givenAllowedDomain("example.com", "example2.com") + + allowedDomainsRepository.deleteAll(listOf("example.com")) + + assertNull(allowedDomainsRepository.getDomain("example2.com")) + assertNotNull(allowedDomainsRepository.getDomain("example.com")) + } + + private fun givenAllowedDomain(vararg allowedDomain: String) { + allowedDomain.forEach { + allowedDomainsDao.insert(AllowedDomainEntity(domain = it)) + } + } +} diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 71cd045e6fc0..8447a8da6841 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -345,6 +345,11 @@ class AppDatabaseTest { } } + @Test + fun whenMigratingFromVersion30To31ThenValidationSucceeds() { + createDatabaseAndMigrate(30, 31, migrationsProvider.MIGRATION_30_TO_31) + } + private fun givenUserStageIs(database: SupportSQLiteDatabase, appStage: AppStage) { database.execSQL("INSERT INTO `userStage` values (1, '${appStage.name}') ") } diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt index 2608f6031ad7..ac8e5770d480 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/view/ClearPersonalDataActionTest.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.global.view import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.browser.WebDataManager +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.fire.AppCacheClearer import com.duckduckgo.app.fire.DuckDuckGoCookieManager import com.duckduckgo.app.fire.UnsentForgetAllPixelStore @@ -44,6 +45,7 @@ class ClearPersonalDataActionTest { private val mockCookieManager: DuckDuckGoCookieManager = mock() private val mockAppCacheClearer: AppCacheClearer = mock() private val mockGeoLocationPermissions: GeoLocationPermissions = mock() + private val mockThirdPartyCookieManager: ThirdPartyCookieManager = mock() @Before fun setup() { @@ -55,7 +57,8 @@ class ClearPersonalDataActionTest { mockSettingsDataStore, mockCookieManager, mockAppCacheClearer, - mockGeoLocationPermissions + mockGeoLocationPermissions, + mockThirdPartyCookieManager ) } @@ -100,4 +103,10 @@ class ClearPersonalDataActionTest { testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false) verify(mockGeoLocationPermissions).clearAllButFireproofed() } + + @Test + fun whenClearCalledThenThirdPartyCookieSitesAreCleared() = runBlocking { + testee.clearTabsAndAllDataAsync(appInForeground = false, shouldFireDataClearPixel = false) + verify(mockThirdPartyCookieManager).clearAllData() + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 3d0f3095a6b3..29f9840eb8f5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -65,6 +65,7 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.* import com.duckduckgo.app.browser.BrowserTabViewModel.Command.DownloadCommand import com.duckduckgo.app.browser.DownloadConfirmationFragment.DownloadConfirmationDialogListener import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.downloader.BlobConverterInjector import com.duckduckgo.app.browser.downloader.DownloadFailReason import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager @@ -197,6 +198,9 @@ class BrowserTabFragment : @Inject lateinit var webViewHttpAuthStore: WebViewHttpAuthStore + @Inject + lateinit var thirdPartyCookieManager: ThirdPartyCookieManager + var messageFromPreviousTab: Message? = null private val initialUrl get() = requireArguments().getString(URL_EXTRA_ARG) @@ -612,6 +616,16 @@ class BrowserTabFragment : is DownloadCommand -> processDownloadCommand(it) is Command.ConvertBlobToDataUri -> convertBlobToDataUri(it) is Command.RequestFileDownload -> requestFileDownload(it.url, it.contentDisposition, it.mimeType, it.requestUserConfirmation) + is Command.ChildTabClosed -> processUriForThirdPartyCookies() + } + } + + private fun processUriForThirdPartyCookies() { + webView?.let { + val url = it.url ?: return + launch { + thirdPartyCookieManager.processUriForThirdPartyCookies(it, url.toUri()) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 7cff5eacb7ca..2ce551685780 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -113,6 +113,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect import timber.log.Timber import java.io.File import java.util.* @@ -283,6 +284,7 @@ class BrowserTabViewModel( class ShowDomainHasPermissionMessage(val domain: String) : Command() class ConvertBlobToDataUri(val url: String, val mimeType: String) : Command() class RequestFileDownload(val url: String, val contentDisposition: String?, val mimeType: String, val requestUserConfirmation: Boolean) : Command() + object ChildTabClosed : Command() sealed class DaxCommand : Command() { object FinishTrackerAnimation : DaxCommand() class HideDaxDialog(val cta: Cta) : DaxCommand() @@ -394,6 +396,13 @@ class BrowserTabViewModel( fireproofDialogsEventHandler.event.observeForever(fireproofDialogEventObserver) navigationAwareLoginDetector.loginEventLiveData.observeForever(loginDetectionObserver) showPulseAnimation.observeForever(fireButtonAnimation) + viewModelScope.launch(dispatchers.main()) { + tabRepository.childClosedTabs.collect { closedTab -> + if (this@BrowserTabViewModel::tabId.isInitialized && tabId == closedTab) { + command.value = ChildTabClosed + } + } + } } fun loadData(tabId: String, initialUrl: String?, skipHome: Boolean) { @@ -401,7 +410,6 @@ class BrowserTabViewModel( this.skipHome = skipHome siteLiveData = tabRepository.retrieveSiteData(tabId) site = siteLiveData.value - initialUrl?.let { buildSiteFactory(it) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index 832f8559b6ea..d313e781d895 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -25,8 +25,10 @@ import android.webkit.* import androidx.annotation.RequiresApi import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.net.toUri import com.duckduckgo.app.browser.certificates.rootstore.CertificateValidationState import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.httpauth.WebViewHttpAuthStore import com.duckduckgo.app.browser.logindetection.DOMLoginDetector import com.duckduckgo.app.browser.logindetection.WebNavigationEvent @@ -51,7 +53,8 @@ class BrowserWebViewClient( private val cookieManager: CookieManager, private val loginDetector: DOMLoginDetector, private val dosDetector: DosDetector, - private val globalPrivacyControl: GlobalPrivacyControl + private val globalPrivacyControl: GlobalPrivacyControl, + private val thirdPartyCookieManager: ThirdPartyCookieManager, ) : WebViewClient() { var webViewClientListener: WebViewClientListener? = null @@ -140,6 +143,11 @@ class BrowserWebViewClient( override fun onPageStarted(webView: WebView, url: String?, favicon: Bitmap?) { try { Timber.v("onPageStarted webViewUrl: ${webView.url} URL: $url") + url?.let { + GlobalScope.launch { + thirdPartyCookieManager.processUriForThirdPartyCookies(webView, url.toUri()) + } + } val navigationList = webView.safeCopyBackForwardList() ?: return webViewClientListener?.navigationStateChanged(WebViewNavigationState(navigationList)) if (url != null && url == lastPageStarted) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt new file mode 100644 index 000000000000..07e6fbef8d61 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.cookies + +import android.net.Uri +import android.webkit.CookieManager +import android.webkit.WebView +import androidx.core.net.toUri +import com.duckduckgo.app.browser.cookies.db.AllowedDomainEntity +import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber + +interface ThirdPartyCookieManager { + suspend fun processUriForThirdPartyCookies(webView: WebView, uri: Uri) + suspend fun clearAllData() +} + +class AppThirdPartyCookieManager( + private val cookieManager: CookieManager, + private val allowedDomainsRepository: AllowedDomainsRepository +) : ThirdPartyCookieManager { + + override suspend fun processUriForThirdPartyCookies(webView: WebView, uri: Uri) { + if (uri.host == GOOGLE_ACCOUNTS_HOST) { + addHostToList(uri) + } else { + enableThirdPartyCookies(webView, uri) + } + } + + override suspend fun clearAllData() { + allowedDomainsRepository.deleteAll(hostsThatAlwaysRequireThirdPartyCookies) + } + + private suspend fun enableThirdPartyCookies(webView: WebView, uri: Uri) { + val host = uri.host ?: return + val allowedDomain = allowedDomainsRepository.getDomain(host) + withContext(Dispatchers.Main) { + if (allowedDomain != null && hasUserIdCookie()) { + Timber.d("Cookies enabled for $uri") + cookieManager.setAcceptThirdPartyCookies(webView, true) + deleteHost(allowedDomain) + } else { + Timber.d("Cookies disabled for $uri") + allowedDomain?.let { deleteHost(it) } + cookieManager.setAcceptThirdPartyCookies(webView, false) + } + } + } + + private suspend fun deleteHost(allowedDomainEntity: AllowedDomainEntity) { + if (hostsThatAlwaysRequireThirdPartyCookies.contains(allowedDomainEntity.domain)) return + allowedDomainsRepository.removeDomain(allowedDomainEntity) + } + + private suspend fun addHostToList(uri: Uri) { + val ssDomain = uri.getQueryParameter("ss_domain") + val accessType = uri.getQueryParameter("response_type") + ssDomain?.let { + if (accessType?.contains("id_token") == true) { + ssDomain.toUri().host?.let { + allowedDomainsRepository.addDomain(it) + } + } + } + } + + private fun hasUserIdCookie(): Boolean { + return cookieManager.getCookie(GOOGLE_ACCOUNTS_URL)?.split(";")?.firstOrNull { + it.contains("user_id") + } != null + } + + companion object { + val hostsThatAlwaysRequireThirdPartyCookies = listOf("home.nest.com") + const val GOOGLE_ACCOUNTS_URL = "https://accounts.google.com" + const val GOOGLE_ACCOUNTS_HOST = "accounts.google.com" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt new file mode 100644 index 000000000000..520f5a92a3fa --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.cookies.db + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "allowed_domains") +data class AllowedDomainEntity( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + var domain: String +) diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt new file mode 100644 index 000000000000..5999eeeca006 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.cookies.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface AllowedDomainsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(allowedDomainEntity: AllowedDomainEntity): Long + + @Delete + fun delete(allowedDomainEntity: AllowedDomainEntity) + + @Query("SELECT * FROM allowed_domains WHERE domain = :host limit 1") + fun getDomain(host: String): AllowedDomainEntity? + + @Query("DELETE FROM allowed_domains WHERE domain NOT IN (:exceptionList)") + fun deleteAll(exceptionList: String) +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt new file mode 100644 index 000000000000..1df760c75c62 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.cookies.db + +import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.UriString +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class AllowedDomainsRepository @Inject constructor( + private val allowedDomainsDao: AllowedDomainsDao, + private val dispatcherProvider: DispatcherProvider +) { + + suspend fun addDomain(domain: String): Long? { + if (!UriString.isValidDomain(domain)) return null + + val allowedDomainEntity = AllowedDomainEntity(domain = domain) + + val id = withContext(dispatcherProvider.io()) { + allowedDomainsDao.insert(allowedDomainEntity) + } + + return if (id >= 0) { + id + } else { + null + } + } + + suspend fun getDomain(domain: String): AllowedDomainEntity? { + return withContext(dispatcherProvider.io()) { + allowedDomainsDao.getDomain(domain) + } + } + + suspend fun removeDomain(allowedDomainEntity: AllowedDomainEntity) { + withContext(dispatcherProvider.io()) { + allowedDomainsDao.delete(allowedDomainEntity) + } + } + + suspend fun deleteAll(exceptionList: List) { + withContext(dispatcherProvider.io()) { + allowedDomainsDao.deleteAll(exceptionList.joinToString(",")) + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 8e34d2d782b4..4650aa7574b9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -24,6 +24,9 @@ import com.duckduckgo.app.browser.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.addtohome.AddToHomeSystemCapabilityDetector import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore +import com.duckduckgo.app.browser.cookies.AppThirdPartyCookieManager +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager +import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository import com.duckduckgo.app.browser.defaultbrowsing.AndroidDefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserObserver @@ -94,7 +97,8 @@ class BrowserModule { cookieManager: CookieManager, loginDetector: DOMLoginDetector, dosDetector: DosDetector, - globalPrivacyControl: GlobalPrivacyControl + globalPrivacyControl: GlobalPrivacyControl, + thirdPartyCookieManager: ThirdPartyCookieManager ): BrowserWebViewClient { return BrowserWebViewClient( webViewHttpAuthStore, @@ -107,7 +111,8 @@ class BrowserModule { cookieManager, loginDetector, dosDetector, - globalPrivacyControl + globalPrivacyControl, + thirdPartyCookieManager ) } @@ -288,4 +293,9 @@ class BrowserModule { ): FireproofDialogsEventHandler { return BrowserTabFireproofDialogsEventHandler(userEventsStore, pixel, fireproofWebsiteRepository, appSettingsPreferencesStore, variantManager, dispatchers) } + + @Provides + fun thirdPartyCookieManager(cookieManager: CookieManager, allowedDomainsRepository: AllowedDomainsRepository): ThirdPartyCookieManager { + return AppThirdPartyCookieManager(cookieManager, allowedDomainsRepository) + } } diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index 013b46a209f4..8e542176a615 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -88,4 +88,7 @@ class DaoModule { @Provides fun locationPermissionsDao(database: AppDatabase) = database.locationPermissionsDao() + + @Provides + fun allowedDomainsDao(database: AppDatabase) = database.allowedDomainsDao() } diff --git a/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt b/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt index e87f66ea6fbf..8e790c2cadcc 100644 --- a/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.di import android.content.Context import androidx.work.WorkManager import com.duckduckgo.app.browser.WebDataManager +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.fire.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.DispatcherProvider @@ -63,7 +64,8 @@ class PrivacyModule { settingsDataStore: SettingsDataStore, cookieManager: DuckDuckGoCookieManager, appCacheClearer: AppCacheClearer, - geoLocationPermissions: GeoLocationPermissions + geoLocationPermissions: GeoLocationPermissions, + thirdPartyCookieManager: ThirdPartyCookieManager ): ClearDataAction { return ClearPersonalDataAction( context, @@ -73,7 +75,8 @@ class PrivacyModule { settingsDataStore, cookieManager, appCacheClearer, - geoLocationPermissions + geoLocationPermissions, + thirdPartyCookieManager ) } diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 80f17e973976..8a4fb8458494 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -25,6 +25,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector +import com.duckduckgo.app.browser.cookies.db.AllowedDomainsDao +import com.duckduckgo.app.browser.cookies.db.AllowedDomainEntity import com.duckduckgo.app.browser.rating.db.* import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.DismissedCta @@ -65,7 +67,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 30, + exportSchema = true, version = 31, entities = [ TdsTracker::class, TdsEntity::class, @@ -92,7 +94,8 @@ import com.duckduckgo.app.usage.search.SearchCountEntity FireproofWebsiteEntity::class, UserEventEntity::class, LocationPermissionEntity::class, - PixelEntity::class + PixelEntity::class, + AllowedDomainEntity::class ] ) @@ -136,6 +139,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun locationPermissionsDao(): LocationPermissionsDao abstract fun userEventsDao(): UserEventsDao abstract fun pixelDao(): PendingPixelDao + abstract fun allowedDomainsDao(): AllowedDomainsDao } @Suppress("PropertyName") @@ -387,6 +391,12 @@ class MigrationsProvider( } } + val MIGRATION_30_TO_31: Migration = object : Migration(26, 27) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_domains` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)") + } + } + val BOOKMARKS_DB_ON_CREATE = object : RoomDatabase.Callback() { override fun onCreate(database: SupportSQLiteDatabase) { MIGRATION_29_TO_30.migrate(database) @@ -423,7 +433,8 @@ class MigrationsProvider( MIGRATION_26_TO_27, MIGRATION_27_TO_28, MIGRATION_28_TO_29, - MIGRATION_29_TO_30 + MIGRATION_29_TO_30, + MIGRATION_30_TO_31 ) @Deprecated( diff --git a/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt b/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt index d870ea28e31e..8c34bea14f70 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/ClearPersonalDataAction.kt @@ -22,6 +22,7 @@ import android.webkit.WebView import androidx.annotation.UiThread import androidx.annotation.WorkerThread import com.duckduckgo.app.browser.WebDataManager +import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.fire.AppCacheClearer import com.duckduckgo.app.fire.DuckDuckGoCookieManager import com.duckduckgo.app.fire.FireActivity @@ -52,7 +53,8 @@ class ClearPersonalDataAction( private val settingsDataStore: SettingsDataStore, private val cookieManager: DuckDuckGoCookieManager, private val appCacheClearer: AppCacheClearer, - private val geoLocationPermissions: GeoLocationPermissions + private val geoLocationPermissions: GeoLocationPermissions, + private val thirdPartyCookieManager: ThirdPartyCookieManager ) : ClearDataAction { override fun killAndRestartProcess(notifyDataCleared: Boolean) { @@ -69,6 +71,7 @@ class ClearPersonalDataAction( withContext(Dispatchers.IO) { cookieManager.flush() geoLocationPermissions.clearAllButFireproofed() + thirdPartyCookieManager.clearAllData() clearTabsAsync(appInForeground) } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index 1f1e8ed006a7..eab2394685f4 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -30,6 +30,8 @@ import io.reactivex.Scheduler import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import timber.log.Timber @@ -51,6 +53,10 @@ class TabDataRepository @Inject constructor( override val flowTabs: Flow> = tabsDao.flowTabs() + private val childTabClosedSharedFlow = MutableSharedFlow() + + override val childClosedTabs = childTabClosedSharedFlow.asSharedFlow() + // We only want the new emissions when subscribing, however Room does not honour that contract so we // need to drop the first emission always (this is equivalent to the Observable semantics) @ExperimentalCoroutinesApi @@ -228,6 +234,12 @@ class TabDataRepository @Inject constructor( } tabsDao.deleteTabAndUpdateSelection(tabToDelete, tabToSelect) siteData.remove(tabToDelete.tabId) + + if (tabToSelect != null) { + appCoroutineScope.launch { + childTabClosedSharedFlow.emit(tabToSelect.tabId) + } + } } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index 2a28768d92b7..221e9837bd67 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.duckduckgo.app.global.model.Site import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow interface TabRepository { @@ -30,6 +31,8 @@ interface TabRepository { val flowTabs: Flow> + val childClosedTabs: SharedFlow + /** * @return the tabs that are marked as "deletable" in the DB */ From 0ea12761a29dd6e9c525befb9de46db7d91d0899 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Mar 2021 13:58:32 +0000 Subject: [PATCH 02/10] Remove unnecessary mock --- .../app/browser/BrowserTabViewModelTest.kt | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index cf62583cea05..e73fe89580c3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -175,7 +175,7 @@ class BrowserTabViewModelTest { private lateinit var mockOmnibarConverter: OmnibarEntryConverter @Mock - private lateinit var mockTabsRepositoryTabViewModel: TabRepository + private lateinit var mockTabRepository: TabRepository @Mock private lateinit var webViewSessionStorage: WebViewSessionStorage @@ -231,9 +231,6 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockFileDownloader: FileDownloader - @Mock - private lateinit var mockTabRepositoryCtaViewModel: TabRepository - @Mock private lateinit var geoLocationPermissions: GeoLocationPermissions @@ -282,7 +279,7 @@ class BrowserTabViewModelTest { val fireproofWebsiteRepository = FireproofWebsiteRepository(fireproofWebsiteDao, coroutineRule.testDispatcherProvider, lazyFaviconManager) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow()) - whenever(mockTabRepositoryCtaViewModel.flowTabs).thenReturn(flowOf(emptyList())) + whenever(mockTabRepository.flowTabs).thenReturn(flowOf(emptyList())) ctaViewModel = CtaViewModel( mockAppInstallStore, @@ -297,7 +294,7 @@ class BrowserTabViewModelTest { mockUserStageStore, mockUserEventsStore, UseOurAppDetector(mockUserEventsStore), - mockTabRepositoryCtaViewModel, + mockTabRepository, coroutineRule.testDispatcherProvider ) @@ -305,10 +302,10 @@ class BrowserTabViewModelTest { whenever(mockOmnibarConverter.convertQueryToUrl(any(), any())).thenReturn("duckduckgo.com") whenever(mockVariantManager.getVariant()).thenReturn(DEFAULT_VARIANT) - whenever(mockTabsRepositoryTabViewModel.liveSelectedTab).thenReturn(selectedTabLiveData) + whenever(mockTabRepository.liveSelectedTab).thenReturn(selectedTabLiveData) whenever(mockNavigationAwareLoginDetector.loginEventLiveData).thenReturn(loginEventLiveData) - whenever(mockTabsRepositoryTabViewModel.retrieveSiteData(any())).thenReturn(MutableLiveData()) - whenever(mockTabsRepositoryTabViewModel.childClosedTabs).thenReturn(childClosedTabsFlow) + whenever(mockTabRepository.retrieveSiteData(any())).thenReturn(MutableLiveData()) + whenever(mockTabRepository.childClosedTabs).thenReturn(childClosedTabsFlow) whenever(mockPrivacyPractices.privacyPracticesFor(any())).thenReturn(PrivacyPractices.UNKNOWN) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) whenever(mockUserWhitelistDao.contains(anyString())).thenReturn(false) @@ -319,7 +316,7 @@ class BrowserTabViewModelTest { queryUrlConverter = mockOmnibarConverter, duckDuckGoUrlDetector = DuckDuckGoUrlDetector(), siteFactory = siteFactory, - tabRepository = mockTabsRepositoryTabViewModel, + tabRepository = mockTabRepository, userWhitelistDao = mockUserWhitelistDao, networkLeaderboardDao = mockNetworkLeaderboardDao, autoComplete = mockAutoCompleteApi, @@ -396,7 +393,7 @@ class BrowserTabViewModelTest { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) assertTrue(commandCaptor.lastValue is Command.OpenInNewBackgroundTab) - verify(mockTabsRepositoryTabViewModel).addNewTabAfterExistingTab(url, "abc") + verify(mockTabRepository).addNewTabAfterExistingTab(url, "abc") } @Test @@ -560,7 +557,7 @@ class BrowserTabViewModelTest { testee.onUserSubmittedQuery("foo") coroutineRule.runBlocking { - verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } } @@ -1225,7 +1222,7 @@ class BrowserTabViewModelTest { testee.onRefreshRequested() coroutineRule.runBlocking { - verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } } @@ -1526,7 +1523,7 @@ class BrowserTabViewModelTest { showErrorWithAction.action() coroutineRule.runBlocking { - verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } } @@ -1698,7 +1695,7 @@ class BrowserTabViewModelTest { fun whenCloseCurrentTabSelectedThenTabDeletedFromRepository() = runBlocking { givenOneActiveTabSelected() testee.closeCurrentTab() - verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) + verify(mockTabRepository).deleteTabAndSelectSource(selectedTabLiveData.value!!.tabId) } @Test @@ -1730,7 +1727,7 @@ class BrowserTabViewModelTest { testee.onUserPressedBack() - verify(mockTabsRepositoryTabViewModel).deleteTabAndSelectSource("TAB_ID") + verify(mockTabRepository).deleteTabAndSelectSource("TAB_ID") } @Test @@ -2877,7 +2874,7 @@ class BrowserTabViewModelTest { testee.prefetchFavicon(url) - verify(mockTabsRepositoryTabViewModel).updateTabFavicon("TAB_ID", file.name) + verify(mockTabRepository).updateTabFavicon("TAB_ID", file.name) } @Test @@ -2886,7 +2883,7 @@ class BrowserTabViewModelTest { testee.prefetchFavicon("url") - verify(mockTabsRepositoryTabViewModel, never()).updateTabFavicon(any(), any()) + verify(mockTabRepository, never()).updateTabFavicon(any(), any()) } @Test @@ -2908,7 +2905,7 @@ class BrowserTabViewModelTest { testee.iconReceived(bitmap) - verify(mockTabsRepositoryTabViewModel).updateTabFavicon("TAB_ID", file.name) + verify(mockTabRepository).updateTabFavicon("TAB_ID", file.name) } @Test @@ -2919,7 +2916,7 @@ class BrowserTabViewModelTest { testee.iconReceived(bitmap) - verify(mockTabsRepositoryTabViewModel, never()).updateTabFavicon(any(), any()) + verify(mockTabRepository, never()).updateTabFavicon(any(), any()) } @Test @@ -3247,7 +3244,7 @@ class BrowserTabViewModelTest { whenever(site.url).thenReturn(USE_OUR_APP_DOMAIN) val siteLiveData = MutableLiveData() siteLiveData.value = site - whenever(mockTabsRepositoryTabViewModel.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + whenever(mockTabRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) testee.loadData("TAB_ID", USE_OUR_APP_DOMAIN, false) } @@ -3257,7 +3254,7 @@ class BrowserTabViewModelTest { whenever(site.url).thenReturn("example.com") val siteLiveData = MutableLiveData() siteLiveData.value = site - whenever(mockTabsRepositoryTabViewModel.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + whenever(mockTabRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) testee.loadData("TAB_ID", "example.com", false) } @@ -3275,7 +3272,7 @@ class BrowserTabViewModelTest { whenever(site.uri).thenReturn(Uri.parse(domain)) val siteLiveData = MutableLiveData() siteLiveData.value = site - whenever(mockTabsRepositoryTabViewModel.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) + whenever(mockTabRepository.retrieveSiteData("TAB_ID")).thenReturn(siteLiveData) testee.loadData("TAB_ID", domain, false) } From 25ea7b83f8acfdd3609f36109a4635c8e567dc44 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Mar 2021 14:03:41 +0000 Subject: [PATCH 03/10] Add constants --- .../cookies/AppThirdPartyCookieManagerTest.kt | 3 ++- .../duckduckgo/app/browser/BrowserTabViewModel.kt | 2 +- .../app/browser/cookies/ThirdPartyCookieManager.kt | 14 +++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt index 335344ff2650..bb5980d64939 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt @@ -24,6 +24,7 @@ import androidx.room.Room import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.browser.cookies.AppThirdPartyCookieManager.Companion.USER_ID_COOKIE import com.duckduckgo.app.browser.cookies.db.AllowedDomainsDao import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository import com.duckduckgo.app.global.db.AppDatabase @@ -168,7 +169,7 @@ class AppThirdPartyCookieManagerTest { private suspend fun givenUserIdCookieIsSet() { withContext(coroutinesTestRule.testDispatcherProvider.main()) { - cookieManager.setCookie("https://accounts.google.com", "user_id=test") + cookieManager.setCookie("https://accounts.google.com", "$USER_ID_COOKIE=test") } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 2ce551685780..76737426cc44 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -396,7 +396,7 @@ class BrowserTabViewModel( fireproofDialogsEventHandler.event.observeForever(fireproofDialogEventObserver) navigationAwareLoginDetector.loginEventLiveData.observeForever(loginDetectionObserver) showPulseAnimation.observeForever(fireButtonAnimation) - viewModelScope.launch(dispatchers.main()) { + viewModelScope.launch { tabRepository.childClosedTabs.collect { closedTab -> if (this@BrowserTabViewModel::tabId.isInitialized && tabId == closedTab) { command.value = ChildTabClosed diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt index 07e6fbef8d61..149ad4dd62df 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt @@ -70,10 +70,10 @@ class AppThirdPartyCookieManager( } private suspend fun addHostToList(uri: Uri) { - val ssDomain = uri.getQueryParameter("ss_domain") - val accessType = uri.getQueryParameter("response_type") + val ssDomain = uri.getQueryParameter(SS_DOMAIN) + val accessType = uri.getQueryParameter(RESPONSE_TYPE) ssDomain?.let { - if (accessType?.contains("id_token") == true) { + if (accessType?.contains(ID_TOKEN) == true) { ssDomain.toUri().host?.let { allowedDomainsRepository.addDomain(it) } @@ -83,13 +83,17 @@ class AppThirdPartyCookieManager( private fun hasUserIdCookie(): Boolean { return cookieManager.getCookie(GOOGLE_ACCOUNTS_URL)?.split(";")?.firstOrNull { - it.contains("user_id") + it.contains(USER_ID_COOKIE) } != null } companion object { - val hostsThatAlwaysRequireThirdPartyCookies = listOf("home.nest.com") + private const val SS_DOMAIN = "ss_domain" + private const val RESPONSE_TYPE = "response_type" + private const val ID_TOKEN = "id_token" + const val USER_ID_COOKIE = "user_id" const val GOOGLE_ACCOUNTS_URL = "https://accounts.google.com" const val GOOGLE_ACCOUNTS_HOST = "accounts.google.com" + val hostsThatAlwaysRequireThirdPartyCookies = listOf("home.nest.com") } } From 40ca2e7373d35a4631d86b99c04037da03711f08 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Mar 2021 14:17:56 +0000 Subject: [PATCH 04/10] minor changes --- .../main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 1 + .../java/com/duckduckgo/app/tabs/model/TabDataRepository.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 76737426cc44..9819674bea6d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -410,6 +410,7 @@ class BrowserTabViewModel( this.skipHome = skipHome siteLiveData = tabRepository.retrieveSiteData(tabId) site = siteLiveData.value + initialUrl?.let { buildSiteFactory(it) } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index eab2394685f4..8adbd593da9d 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -235,7 +235,7 @@ class TabDataRepository @Inject constructor( tabsDao.deleteTabAndUpdateSelection(tabToDelete, tabToSelect) siteData.remove(tabToDelete.tabId) - if (tabToSelect != null) { + tabToSelect?.let { appCoroutineScope.launch { childTabClosedSharedFlow.emit(tabToSelect.tabId) } From a424751ff2f5e00843ec308352b0c5e921e7dc80 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Mar 2021 14:59:54 +0000 Subject: [PATCH 05/10] Domain as primary key --- .../31.json | 16 +++++----------- .../browser/cookies/ThirdPartyCookieManager.kt | 4 ++-- .../browser/cookies/db/AllowedDomainEntity.kt | 2 +- .../com/duckduckgo/app/global/db/AppDatabase.kt | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json index baf152b33277..aa8d947e1e16 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 31, - "identityHash": "11c0c226b52d931b47fb5abe382c33e9", + "identityHash": "09d3ee04622c2e686f2030e14c18eb4e", "entities": [ { "tableName": "tds_tracker", @@ -838,14 +838,8 @@ }, { "tableName": "allowed_domains", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "domain", "columnName": "domain", @@ -855,9 +849,9 @@ ], "primaryKey": { "columnNames": [ - "id" + "domain" ], - "autoGenerate": true + "autoGenerate": false }, "indices": [], "foreignKeys": [] @@ -866,7 +860,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11c0c226b52d931b47fb5abe382c33e9')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09d3ee04622c2e686f2030e14c18eb4e')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt index 149ad4dd62df..dc0f2fa7702e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt @@ -73,7 +73,7 @@ class AppThirdPartyCookieManager( val ssDomain = uri.getQueryParameter(SS_DOMAIN) val accessType = uri.getQueryParameter(RESPONSE_TYPE) ssDomain?.let { - if (accessType?.contains(ID_TOKEN) == true) { + if (accessType?.contains(CODE) == false) { ssDomain.toUri().host?.let { allowedDomainsRepository.addDomain(it) } @@ -90,7 +90,7 @@ class AppThirdPartyCookieManager( companion object { private const val SS_DOMAIN = "ss_domain" private const val RESPONSE_TYPE = "response_type" - private const val ID_TOKEN = "id_token" + private const val CODE = "code" const val USER_ID_COOKIE = "user_id" const val GOOGLE_ACCOUNTS_URL = "https://accounts.google.com" const val GOOGLE_ACCOUNTS_HOST = "accounts.google.com" diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt index 520f5a92a3fa..f6ad3f2cc81b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt @@ -21,6 +21,6 @@ import androidx.room.PrimaryKey @Entity(tableName = "allowed_domains") data class AllowedDomainEntity( - @PrimaryKey(autoGenerate = true) var id: Long = 0, + @PrimaryKey var domain: String ) diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 8a4fb8458494..ef75df111ec9 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -393,7 +393,7 @@ class MigrationsProvider( val MIGRATION_30_TO_31: Migration = object : Migration(26, 27) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_domains` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL)") + database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_domains` (`domain` TEXT PRIMARY KEY NOT NULL)") } } From c3f13871f18bc55b3e73e44937af62a8182d0531 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Wed, 24 Mar 2021 15:28:15 +0000 Subject: [PATCH 06/10] Fix test --- app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index ef75df111ec9..5eca151110e5 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -391,7 +391,7 @@ class MigrationsProvider( } } - val MIGRATION_30_TO_31: Migration = object : Migration(26, 27) { + val MIGRATION_30_TO_31: Migration = object : Migration(30, 31) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_domains` (`domain` TEXT PRIMARY KEY NOT NULL)") } From c52d5b2e5314a11cfd8b4e7dfdb5c649374045d9 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Mar 2021 10:46:31 +0000 Subject: [PATCH 07/10] Amend code as per PR comments --- .../browser/cookies/AppThirdPartyCookieManagerTest.kt | 8 +++++--- .../browser/cookies/db/AllowedDomainsRepositoryTest.kt | 9 +++------ .../app/browser/cookies/ThirdPartyCookieManager.kt | 7 +++---- .../app/browser/cookies/db/AllowedDomainsRepository.kt | 2 +- .../java/com/duckduckgo/app/browser/di/BrowserModule.kt | 1 + 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt index bb5980d64939..c14968b19c73 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt @@ -29,6 +29,7 @@ import com.duckduckgo.app.browser.cookies.db.AllowedDomainsDao import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import org.junit.After import org.junit.Assert.* @@ -36,6 +37,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test +@ExperimentalCoroutinesApi class AppThirdPartyCookieManagerTest { @get:Rule @@ -69,7 +71,7 @@ class AppThirdPartyCookieManagerTest { @UiThreadTest @Test - fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAndIsNotInTheListThenThirdPartyCookiesDisabled() = coroutinesTestRule.runBlocking { + fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAuthAndIsNotInTheListThenThirdPartyCookiesDisabled() = coroutinesTestRule.runBlocking { testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) assertFalse(cookieManager.acceptThirdPartyCookies(webView)) @@ -77,7 +79,7 @@ class AppThirdPartyCookieManagerTest { @UiThreadTest @Test - fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAndIsInTheListAndHasCookieThenThirdPartyCookiesEnabled() = coroutinesTestRule.runBlocking { + fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAuthAndIsInTheListAndHasCookieThenThirdPartyCookiesEnabled() = coroutinesTestRule.runBlocking { givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) givenUserIdCookieIsSet() @@ -88,7 +90,7 @@ class AppThirdPartyCookieManagerTest { @UiThreadTest @Test - fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAndIsInTheListAndDoesNotHaveCookieThenThirdPartyCookiesDisabled() = coroutinesTestRule.runBlocking { + fun whenProcessUriForThirdPartyCookiesIfDomainIsNotGoogleAuthAndIsInTheListAndDoesNotHaveCookieThenThirdPartyCookiesDisabled() = coroutinesTestRule.runBlocking { givenDomainIsInTheThirdPartyCookieList(EXAMPLE_URI.host!!) testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt index fd49e86e567b..cfe2f8bb363b 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt @@ -22,12 +22,14 @@ import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test +@ExperimentalCoroutinesApi class AllowedDomainsRepositoryTest { @get:Rule @Suppress("unused") @@ -66,12 +68,7 @@ class AllowedDomainsRepositoryTest { } @Test - fun whenAddDomainWithSubDomainThenReturnNonNull() = coroutineRule.runBlocking { - assertNotNull(allowedDomainsRepository.addDomain("test.example.com")) - } - - @Test - fun whenAddDomainThenReturnNonNull() = coroutineRule.runBlocking { + fun whenAddValidDomainThenReturnNonNull() = coroutineRule.runBlocking { assertNotNull(allowedDomainsRepository.addDomain("example.com")) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt index dc0f2fa7702e..46755e0b6493 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt @@ -40,7 +40,7 @@ class AppThirdPartyCookieManager( if (uri.host == GOOGLE_ACCOUNTS_HOST) { addHostToList(uri) } else { - enableThirdPartyCookies(webView, uri) + processThirdPartyCookiesSetting(webView, uri) } } @@ -48,19 +48,18 @@ class AppThirdPartyCookieManager( allowedDomainsRepository.deleteAll(hostsThatAlwaysRequireThirdPartyCookies) } - private suspend fun enableThirdPartyCookies(webView: WebView, uri: Uri) { + private suspend fun processThirdPartyCookiesSetting(webView: WebView, uri: Uri) { val host = uri.host ?: return val allowedDomain = allowedDomainsRepository.getDomain(host) withContext(Dispatchers.Main) { if (allowedDomain != null && hasUserIdCookie()) { Timber.d("Cookies enabled for $uri") cookieManager.setAcceptThirdPartyCookies(webView, true) - deleteHost(allowedDomain) } else { Timber.d("Cookies disabled for $uri") - allowedDomain?.let { deleteHost(it) } cookieManager.setAcceptThirdPartyCookies(webView, false) } + allowedDomain?.let { deleteHost(it) } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt index 1df760c75c62..c5f06c475131 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt @@ -54,7 +54,7 @@ class AllowedDomainsRepository @Inject constructor( } } - suspend fun deleteAll(exceptionList: List) { + suspend fun deleteAll(exceptionList: List = emptyList()) { withContext(dispatcherProvider.io()) { allowedDomainsDao.deleteAll(exceptionList.joinToString(",")) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 4650aa7574b9..3e48e1202c71 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -294,6 +294,7 @@ class BrowserModule { return BrowserTabFireproofDialogsEventHandler(userEventsStore, pixel, fireproofWebsiteRepository, appSettingsPreferencesStore, variantManager, dispatchers) } + @Singleton @Provides fun thirdPartyCookieManager(cookieManager: CookieManager, allowedDomainsRepository: AllowedDomainsRepository): ThirdPartyCookieManager { return AppThirdPartyCookieManager(cookieManager, allowedDomainsRepository) From 0e2f866b350179893843c359da61041e0f8e3b30 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Mar 2021 11:06:48 +0000 Subject: [PATCH 08/10] Rename classes to AuthCookiesAllowedDomain... --- .../31.json | 6 +-- .../cookies/AppThirdPartyCookieManagerTest.kt | 30 ++++++------ ...uthCookiesAllowedDomainsRepositoryTest.kt} | 46 +++++++++---------- .../cookies/ThirdPartyCookieManager.kt | 22 ++++----- ...ty.kt => AuthCookieAllowedDomainEntity.kt} | 4 +- ...Dao.kt => AuthCookiesAllowedDomainsDao.kt} | 12 ++--- ...=> AuthCookiesAllowedDomainsRepository.kt} | 18 ++++---- .../app/browser/di/BrowserModule.kt | 6 +-- .../java/com/duckduckgo/app/di/DaoModule.kt | 2 +- .../duckduckgo/app/global/db/AppDatabase.kt | 10 ++-- 10 files changed, 78 insertions(+), 78 deletions(-) rename app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/{AllowedDomainsRepositoryTest.kt => AuthCookiesAllowedDomainsRepositoryTest.kt} (56%) rename app/src/main/java/com/duckduckgo/app/browser/cookies/db/{AllowedDomainEntity.kt => AuthCookieAllowedDomainEntity.kt} (88%) rename app/src/main/java/com/duckduckgo/app/browser/cookies/db/{AllowedDomainsDao.kt => AuthCookiesAllowedDomainsDao.kt} (67%) rename app/src/main/java/com/duckduckgo/app/browser/cookies/db/{AllowedDomainsRepository.kt => AuthCookiesAllowedDomainsRepository.kt} (66%) diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json index aa8d947e1e16..b87f96f614d8 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/31.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 31, - "identityHash": "09d3ee04622c2e686f2030e14c18eb4e", + "identityHash": "be6380785dcbf8f6793f852097f0d224", "entities": [ { "tableName": "tds_tracker", @@ -837,7 +837,7 @@ "foreignKeys": [] }, { - "tableName": "allowed_domains", + "tableName": "auth_cookies_allowed_domains", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", "fields": [ { @@ -860,7 +860,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09d3ee04622c2e686f2030e14c18eb4e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be6380785dcbf8f6793f852097f0d224')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt index c14968b19c73..6e7c0eb565c9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/AppThirdPartyCookieManagerTest.kt @@ -25,8 +25,8 @@ import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.browser.cookies.AppThirdPartyCookieManager.Companion.USER_ID_COOKIE -import com.duckduckgo.app.browser.cookies.db.AllowedDomainsDao -import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository +import com.duckduckgo.app.browser.cookies.db.AuthCookiesAllowedDomainsDao +import com.duckduckgo.app.browser.cookies.db.AuthCookiesAllowedDomainsRepository import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.runBlocking import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,8 +45,8 @@ class AppThirdPartyCookieManagerTest { private val cookieManager = CookieManager.getInstance() private lateinit var db: AppDatabase - private lateinit var allowedDomainsDao: AllowedDomainsDao - private lateinit var allowedDomainsRepository: AllowedDomainsRepository + private lateinit var authCookiesAllowedDomainsDao: AuthCookiesAllowedDomainsDao + private lateinit var authCookiesAllowedDomainsRepository: AuthCookiesAllowedDomainsRepository private lateinit var testee: AppThirdPartyCookieManager private lateinit var webView: WebView @@ -56,11 +56,11 @@ class AppThirdPartyCookieManagerTest { db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) .allowMainThreadQueries() .build() - allowedDomainsDao = db.allowedDomainsDao() - allowedDomainsRepository = AllowedDomainsRepository(allowedDomainsDao, coroutinesTestRule.testDispatcherProvider) + authCookiesAllowedDomainsDao = db.authCookiesAllowedDomainsDao() + authCookiesAllowedDomainsRepository = AuthCookiesAllowedDomainsRepository(authCookiesAllowedDomainsDao, coroutinesTestRule.testDispatcherProvider) webView = TestWebView(InstrumentationRegistry.getInstrumentation().targetContext) - testee = AppThirdPartyCookieManager(cookieManager, allowedDomainsRepository) + testee = AppThirdPartyCookieManager(cookieManager, authCookiesAllowedDomainsRepository) } @UiThreadTest @@ -106,7 +106,7 @@ class AppThirdPartyCookieManagerTest { testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) - assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + assertNull(authCookiesAllowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) } @UiThreadTest @@ -116,7 +116,7 @@ class AppThirdPartyCookieManagerTest { testee.processUriForThirdPartyCookies(webView, EXAMPLE_URI) - assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + assertNull(authCookiesAllowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) } @UiThreadTest @@ -126,7 +126,7 @@ class AppThirdPartyCookieManagerTest { testee.processUriForThirdPartyCookies(webView, EXCLUDED_DOMAIN_URI) - assertNotNull(allowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!)) + assertNotNull(authCookiesAllowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!)) } @UiThreadTest @@ -134,7 +134,7 @@ class AppThirdPartyCookieManagerTest { fun whenProcessUriForThirdPartyCookiesIfUrlIsGoogleAuthAndIsTokenTypeThenDomainAddedToTheList() = coroutinesTestRule.runBlocking { testee.processUriForThirdPartyCookies(webView, THIRD_PARTY_AUTH_URI) - assertNotNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + assertNotNull(authCookiesAllowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) } @UiThreadTest @@ -142,7 +142,7 @@ class AppThirdPartyCookieManagerTest { fun whenProcessUriForThirdPartyCookiesIfUrlIsGoogleAuthAndIsNotTokenTypeThenDomainNotAddedToTheList() = coroutinesTestRule.runBlocking { testee.processUriForThirdPartyCookies(webView, NON_THIRD_PARTY_AUTH_URI) - assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + assertNull(authCookiesAllowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) } @Test @@ -151,7 +151,7 @@ class AppThirdPartyCookieManagerTest { testee.clearAllData() - assertNull(allowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) + assertNull(authCookiesAllowedDomainsRepository.getDomain(EXAMPLE_URI.host!!)) } @Test @@ -160,12 +160,12 @@ class AppThirdPartyCookieManagerTest { testee.clearAllData() - assertNotNull(allowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!)) + assertNotNull(authCookiesAllowedDomainsRepository.getDomain(EXCLUDED_DOMAIN_URI.host!!)) } private suspend fun givenDomainIsInTheThirdPartyCookieList(domain: String) = coroutinesTestRule.runBlocking { withContext(coroutinesTestRule.testDispatcherProvider.io()) { - allowedDomainsRepository.addDomain(domain) + authCookiesAllowedDomainsRepository.addDomain(domain) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsRepositoryTest.kt similarity index 56% rename from app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt rename to app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsRepositoryTest.kt index cfe2f8bb363b..d13a286ad021 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsRepositoryTest.kt @@ -30,7 +30,7 @@ import org.junit.Rule import org.junit.Test @ExperimentalCoroutinesApi -class AllowedDomainsRepositoryTest { +class AuthCookiesAllowedDomainsRepositoryTest { @get:Rule @Suppress("unused") val coroutineRule = CoroutineTestRule() @@ -40,16 +40,16 @@ class AllowedDomainsRepositoryTest { var instantTaskExecutorRule = InstantTaskExecutorRule() private lateinit var db: AppDatabase - private lateinit var allowedDomainsDao: AllowedDomainsDao - private lateinit var allowedDomainsRepository: AllowedDomainsRepository + private lateinit var authCookiesAllowedDomainsDao: AuthCookiesAllowedDomainsDao + private lateinit var authCookiesAllowedDomainsRepository: AuthCookiesAllowedDomainsRepository @Before fun before() { db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) .allowMainThreadQueries() .build() - allowedDomainsDao = db.allowedDomainsDao() - allowedDomainsRepository = AllowedDomainsRepository(allowedDomainsDao, coroutineRule.testDispatcherProvider) + authCookiesAllowedDomainsDao = db.authCookiesAllowedDomainsDao() + authCookiesAllowedDomainsRepository = AuthCookiesAllowedDomainsRepository(authCookiesAllowedDomainsDao, coroutineRule.testDispatcherProvider) } @After @@ -59,57 +59,57 @@ class AllowedDomainsRepositoryTest { @Test fun whenAddDomainIfIsEmptyThenReturnNull() = coroutineRule.runBlocking { - assertNull(allowedDomainsRepository.addDomain("")) + assertNull(authCookiesAllowedDomainsRepository.addDomain("")) } @Test fun whenAddDomainAndDomainNotValidThenReturnNull() = coroutineRule.runBlocking { - assertNull(allowedDomainsRepository.addDomain("https://example.com")) + assertNull(authCookiesAllowedDomainsRepository.addDomain("https://example.com")) } @Test fun whenAddValidDomainThenReturnNonNull() = coroutineRule.runBlocking { - assertNotNull(allowedDomainsRepository.addDomain("example.com")) + assertNotNull(authCookiesAllowedDomainsRepository.addDomain("example.com")) } @Test fun whenGetDomainIfDomainExistsThenReturnAllowedDomainEntity() = coroutineRule.runBlocking { - givenAllowedDomain("example.com") + givenAuthCookieAllowedDomain("example.com") - val allowedDomainEntity = allowedDomainsRepository.getDomain("example.com") - assertEquals("example.com", allowedDomainEntity?.domain) + val authCookieAllowedDomainEntity = authCookiesAllowedDomainsRepository.getDomain("example.com") + assertEquals("example.com", authCookieAllowedDomainEntity?.domain) } @Test fun whenGetDomainIfDomainDoesNotExistThenReturnNull() = coroutineRule.runBlocking { - val allowedDomainEntity = allowedDomainsRepository.getDomain("example.com") - assertNull(allowedDomainEntity) + val authCookieAllowedDomainEntity = authCookiesAllowedDomainsRepository.getDomain("example.com") + assertNull(authCookieAllowedDomainEntity) } @Test fun whenRemoveDomainThenDomainDeletedFromDatabase() = coroutineRule.runBlocking { - givenAllowedDomain("example.com") - val allowedDomainEntity = allowedDomainsRepository.getDomain("example.com") + givenAuthCookieAllowedDomain("example.com") + val authCookieAllowedDomainEntity = authCookiesAllowedDomainsRepository.getDomain("example.com") - allowedDomainsRepository.removeDomain(allowedDomainEntity!!) + authCookiesAllowedDomainsRepository.removeDomain(authCookieAllowedDomainEntity!!) - val deletedEntity = allowedDomainsRepository.getDomain("example.com") + val deletedEntity = authCookiesAllowedDomainsRepository.getDomain("example.com") assertNull(deletedEntity) } @Test fun whenDeleteAllThenAllDomainsDeletedExceptFromTheExceptionList() = coroutineRule.runBlocking { - givenAllowedDomain("example.com", "example2.com") + givenAuthCookieAllowedDomain("example.com", "example2.com") - allowedDomainsRepository.deleteAll(listOf("example.com")) + authCookiesAllowedDomainsRepository.deleteAll(listOf("example.com")) - assertNull(allowedDomainsRepository.getDomain("example2.com")) - assertNotNull(allowedDomainsRepository.getDomain("example.com")) + assertNull(authCookiesAllowedDomainsRepository.getDomain("example2.com")) + assertNotNull(authCookiesAllowedDomainsRepository.getDomain("example.com")) } - private fun givenAllowedDomain(vararg allowedDomain: String) { + private fun givenAuthCookieAllowedDomain(vararg allowedDomain: String) { allowedDomain.forEach { - allowedDomainsDao.insert(AllowedDomainEntity(domain = it)) + authCookiesAllowedDomainsDao.insert(AuthCookieAllowedDomainEntity(domain = it)) } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt index 46755e0b6493..1a164db372b4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt @@ -20,8 +20,8 @@ import android.net.Uri import android.webkit.CookieManager import android.webkit.WebView import androidx.core.net.toUri -import com.duckduckgo.app.browser.cookies.db.AllowedDomainEntity -import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository +import com.duckduckgo.app.browser.cookies.db.AuthCookieAllowedDomainEntity +import com.duckduckgo.app.browser.cookies.db.AuthCookiesAllowedDomainsRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber @@ -33,7 +33,7 @@ interface ThirdPartyCookieManager { class AppThirdPartyCookieManager( private val cookieManager: CookieManager, - private val allowedDomainsRepository: AllowedDomainsRepository + private val authCookiesAllowedDomainsRepository: AuthCookiesAllowedDomainsRepository ) : ThirdPartyCookieManager { override suspend fun processUriForThirdPartyCookies(webView: WebView, uri: Uri) { @@ -45,27 +45,27 @@ class AppThirdPartyCookieManager( } override suspend fun clearAllData() { - allowedDomainsRepository.deleteAll(hostsThatAlwaysRequireThirdPartyCookies) + authCookiesAllowedDomainsRepository.deleteAll(hostsThatAlwaysRequireThirdPartyCookies) } private suspend fun processThirdPartyCookiesSetting(webView: WebView, uri: Uri) { val host = uri.host ?: return - val allowedDomain = allowedDomainsRepository.getDomain(host) + val domain = authCookiesAllowedDomainsRepository.getDomain(host) withContext(Dispatchers.Main) { - if (allowedDomain != null && hasUserIdCookie()) { + if (domain != null && hasUserIdCookie()) { Timber.d("Cookies enabled for $uri") cookieManager.setAcceptThirdPartyCookies(webView, true) } else { Timber.d("Cookies disabled for $uri") cookieManager.setAcceptThirdPartyCookies(webView, false) } - allowedDomain?.let { deleteHost(it) } + domain?.let { deleteHost(it) } } } - private suspend fun deleteHost(allowedDomainEntity: AllowedDomainEntity) { - if (hostsThatAlwaysRequireThirdPartyCookies.contains(allowedDomainEntity.domain)) return - allowedDomainsRepository.removeDomain(allowedDomainEntity) + private suspend fun deleteHost(authCookieAllowedDomainEntity: AuthCookieAllowedDomainEntity) { + if (hostsThatAlwaysRequireThirdPartyCookies.contains(authCookieAllowedDomainEntity.domain)) return + authCookiesAllowedDomainsRepository.removeDomain(authCookieAllowedDomainEntity) } private suspend fun addHostToList(uri: Uri) { @@ -74,7 +74,7 @@ class AppThirdPartyCookieManager( ssDomain?.let { if (accessType?.contains(CODE) == false) { ssDomain.toUri().host?.let { - allowedDomainsRepository.addDomain(it) + authCookiesAllowedDomainsRepository.addDomain(it) } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookieAllowedDomainEntity.kt similarity index 88% rename from app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt rename to app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookieAllowedDomainEntity.kt index f6ad3f2cc81b..7914f1dfbdcb 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainEntity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookieAllowedDomainEntity.kt @@ -19,8 +19,8 @@ package com.duckduckgo.app.browser.cookies.db import androidx.room.Entity import androidx.room.PrimaryKey -@Entity(tableName = "allowed_domains") -data class AllowedDomainEntity( +@Entity(tableName = "auth_cookies_allowed_domains") +data class AuthCookieAllowedDomainEntity( @PrimaryKey var domain: String ) diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsDao.kt similarity index 67% rename from app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt rename to app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsDao.kt index 5999eeeca006..3509fd90c7b6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsDao.kt @@ -23,17 +23,17 @@ import androidx.room.OnConflictStrategy import androidx.room.Query @Dao -interface AllowedDomainsDao { +interface AuthCookiesAllowedDomainsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(allowedDomainEntity: AllowedDomainEntity): Long + fun insert(authCookieAllowedDomainEntity: AuthCookieAllowedDomainEntity): Long @Delete - fun delete(allowedDomainEntity: AllowedDomainEntity) + fun delete(authCookieAllowedDomainEntity: AuthCookieAllowedDomainEntity) - @Query("SELECT * FROM allowed_domains WHERE domain = :host limit 1") - fun getDomain(host: String): AllowedDomainEntity? + @Query("SELECT * FROM auth_cookies_allowed_domains WHERE domain = :host limit 1") + fun getDomain(host: String): AuthCookieAllowedDomainEntity? - @Query("DELETE FROM allowed_domains WHERE domain NOT IN (:exceptionList)") + @Query("DELETE FROM auth_cookies_allowed_domains WHERE domain NOT IN (:exceptionList)") fun deleteAll(exceptionList: String) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsRepository.kt similarity index 66% rename from app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt rename to app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsRepository.kt index c5f06c475131..a1c18c02f149 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AllowedDomainsRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/db/AuthCookiesAllowedDomainsRepository.kt @@ -21,18 +21,18 @@ import com.duckduckgo.app.global.UriString import kotlinx.coroutines.withContext import javax.inject.Inject -class AllowedDomainsRepository @Inject constructor( - private val allowedDomainsDao: AllowedDomainsDao, +class AuthCookiesAllowedDomainsRepository @Inject constructor( + private val authCookiesAllowedDomainsDao: AuthCookiesAllowedDomainsDao, private val dispatcherProvider: DispatcherProvider ) { suspend fun addDomain(domain: String): Long? { if (!UriString.isValidDomain(domain)) return null - val allowedDomainEntity = AllowedDomainEntity(domain = domain) + val authCookieAllowedDomainEntity = AuthCookieAllowedDomainEntity(domain = domain) val id = withContext(dispatcherProvider.io()) { - allowedDomainsDao.insert(allowedDomainEntity) + authCookiesAllowedDomainsDao.insert(authCookieAllowedDomainEntity) } return if (id >= 0) { @@ -42,21 +42,21 @@ class AllowedDomainsRepository @Inject constructor( } } - suspend fun getDomain(domain: String): AllowedDomainEntity? { + suspend fun getDomain(domain: String): AuthCookieAllowedDomainEntity? { return withContext(dispatcherProvider.io()) { - allowedDomainsDao.getDomain(domain) + authCookiesAllowedDomainsDao.getDomain(domain) } } - suspend fun removeDomain(allowedDomainEntity: AllowedDomainEntity) { + suspend fun removeDomain(authCookieAllowedDomainEntity: AuthCookieAllowedDomainEntity) { withContext(dispatcherProvider.io()) { - allowedDomainsDao.delete(allowedDomainEntity) + authCookiesAllowedDomainsDao.delete(authCookieAllowedDomainEntity) } } suspend fun deleteAll(exceptionList: List = emptyList()) { withContext(dispatcherProvider.io()) { - allowedDomainsDao.deleteAll(exceptionList.joinToString(",")) + authCookiesAllowedDomainsDao.deleteAll(exceptionList.joinToString(",")) } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index 3e48e1202c71..b65668e81d6f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -26,7 +26,7 @@ import com.duckduckgo.app.browser.addtohome.AddToHomeSystemCapabilityDetector import com.duckduckgo.app.browser.certificates.rootstore.TrustedCertificateStore import com.duckduckgo.app.browser.cookies.AppThirdPartyCookieManager import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager -import com.duckduckgo.app.browser.cookies.db.AllowedDomainsRepository +import com.duckduckgo.app.browser.cookies.db.AuthCookiesAllowedDomainsRepository import com.duckduckgo.app.browser.defaultbrowsing.AndroidDefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserObserver @@ -296,7 +296,7 @@ class BrowserModule { @Singleton @Provides - fun thirdPartyCookieManager(cookieManager: CookieManager, allowedDomainsRepository: AllowedDomainsRepository): ThirdPartyCookieManager { - return AppThirdPartyCookieManager(cookieManager, allowedDomainsRepository) + fun thirdPartyCookieManager(cookieManager: CookieManager, authCookiesAllowedDomainsRepository: AuthCookiesAllowedDomainsRepository): ThirdPartyCookieManager { + return AppThirdPartyCookieManager(cookieManager, authCookiesAllowedDomainsRepository) } } diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index 8e542176a615..58b231843e78 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -90,5 +90,5 @@ class DaoModule { fun locationPermissionsDao(database: AppDatabase) = database.locationPermissionsDao() @Provides - fun allowedDomainsDao(database: AppDatabase) = database.allowedDomainsDao() + fun allowedDomainsDao(database: AppDatabase) = database.authCookiesAllowedDomainsDao() } diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 5eca151110e5..c4ede9a905be 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -25,8 +25,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector -import com.duckduckgo.app.browser.cookies.db.AllowedDomainsDao -import com.duckduckgo.app.browser.cookies.db.AllowedDomainEntity +import com.duckduckgo.app.browser.cookies.db.AuthCookiesAllowedDomainsDao +import com.duckduckgo.app.browser.cookies.db.AuthCookieAllowedDomainEntity import com.duckduckgo.app.browser.rating.db.* import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.DismissedCta @@ -95,7 +95,7 @@ import com.duckduckgo.app.usage.search.SearchCountEntity UserEventEntity::class, LocationPermissionEntity::class, PixelEntity::class, - AllowedDomainEntity::class + AuthCookieAllowedDomainEntity::class ] ) @@ -139,7 +139,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun locationPermissionsDao(): LocationPermissionsDao abstract fun userEventsDao(): UserEventsDao abstract fun pixelDao(): PendingPixelDao - abstract fun allowedDomainsDao(): AllowedDomainsDao + abstract fun authCookiesAllowedDomainsDao(): AuthCookiesAllowedDomainsDao } @Suppress("PropertyName") @@ -393,7 +393,7 @@ class MigrationsProvider( val MIGRATION_30_TO_31: Migration = object : Migration(30, 31) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_domains` (`domain` TEXT PRIMARY KEY NOT NULL)") + database.execSQL("CREATE TABLE IF NOT EXISTS `auth_cookies_allowed_domains` (`domain` TEXT PRIMARY KEY NOT NULL)") } } From 7110fce52c201ccccc13150781712eb1353521df Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Mar 2021 11:18:11 +0000 Subject: [PATCH 09/10] Amend as per PR comments --- .../duckduckgo/app/browser/BrowserWebViewClientTest.kt | 4 +++- .../com/duckduckgo/app/browser/BrowserWebViewClient.kt | 4 +++- .../app/browser/cookies/ThirdPartyCookieManager.kt | 1 + .../java/com/duckduckgo/app/browser/di/BrowserModule.kt | 8 ++++++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index b645523882c5..381fe042eaf1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -36,6 +36,7 @@ import com.duckduckgo.app.runBlocking import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope import org.junit.Before import org.junit.Rule import org.junit.Test @@ -79,7 +80,8 @@ class BrowserWebViewClientTest { loginDetector, dosDetector, globalPrivacyControl, - thirdPartyCookieManager + thirdPartyCookieManager, + GlobalScope ) testee.webViewClientListener = listener } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index d313e781d895..3f76fc36e2b3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -34,6 +34,7 @@ import com.duckduckgo.app.browser.logindetection.DOMLoginDetector import com.duckduckgo.app.browser.logindetection.WebNavigationEvent import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.navigation.safeCopyBackForwardList +import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.global.exception.UncaughtExceptionRepository import com.duckduckgo.app.global.exception.UncaughtExceptionSource.* import com.duckduckgo.app.globalprivacycontrol.GlobalPrivacyControl @@ -55,6 +56,7 @@ class BrowserWebViewClient( private val dosDetector: DosDetector, private val globalPrivacyControl: GlobalPrivacyControl, private val thirdPartyCookieManager: ThirdPartyCookieManager, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope ) : WebViewClient() { var webViewClientListener: WebViewClientListener? = null @@ -144,7 +146,7 @@ class BrowserWebViewClient( try { Timber.v("onPageStarted webViewUrl: ${webView.url} URL: $url") url?.let { - GlobalScope.launch { + appCoroutineScope.launch { thirdPartyCookieManager.processUriForThirdPartyCookies(webView, url.toUri()) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt index 1a164db372b4..33682fa17a97 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/cookies/ThirdPartyCookieManager.kt @@ -86,6 +86,7 @@ class AppThirdPartyCookieManager( } != null } + // See https://app.asana.com/0/1125189844152671/1200029737431978 for mor context about the below values companion object { private const val SS_DOMAIN = "ss_domain" private const val RESPONSE_TYPE = "response_type" diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index b65668e81d6f..08895f119be9 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -42,6 +42,7 @@ import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewPersister import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.useragent.UserAgentProvider +import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.fire.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository @@ -69,6 +70,7 @@ import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator import com.duckduckgo.app.trackerdetection.TrackerDetector import dagger.Module import dagger.Provides +import kotlinx.coroutines.CoroutineScope import javax.inject.Named import javax.inject.Singleton @@ -98,7 +100,8 @@ class BrowserModule { loginDetector: DOMLoginDetector, dosDetector: DosDetector, globalPrivacyControl: GlobalPrivacyControl, - thirdPartyCookieManager: ThirdPartyCookieManager + thirdPartyCookieManager: ThirdPartyCookieManager, + @AppCoroutineScope appCoroutineScope: CoroutineScope ): BrowserWebViewClient { return BrowserWebViewClient( webViewHttpAuthStore, @@ -112,7 +115,8 @@ class BrowserModule { loginDetector, dosDetector, globalPrivacyControl, - thirdPartyCookieManager + thirdPartyCookieManager, + appCoroutineScope ) } From 7468879eda561a224b58ba260c554a3df152daf8 Mon Sep 17 00:00:00 2001 From: Marcos Holgado Date: Fri, 26 Mar 2021 11:27:40 +0000 Subject: [PATCH 10/10] Add test --- .../app/tabs/model/TabDataRepositoryTest.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt index 6f14724b4b7f..53615eaf7401 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt @@ -423,6 +423,27 @@ class TabDataRepositoryTest { job.cancel() } + @Test + fun whenDeleteTabAndSelectSourceIfTabHadAParentThenEmitParentTabId() = runBlocking { + val db = createDatabase() + val dao = db.tabsDao() + val sourceTab = TabEntity(tabId = "sourceId", url = "http://www.example.com", position = 0) + val tabToDelete = TabEntity(tabId = "tabToDeleteId", url = "http://www.example.com", position = 1, sourceTabId = "sourceId") + dao.addAndSelectTab(sourceTab) + dao.addAndSelectTab(tabToDelete) + testee = tabDataRepository(dao) + + testee.deleteTabAndSelectSource("tabToDeleteId") + + val job = launch { + testee.childClosedTabs.collect { + assertEquals("sourceId", it) + } + } + + job.cancel() + } + private fun tabDataRepository(dao: TabsDao): TabDataRepository { return TabDataRepository( dao,