diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt
index b6dc003eb623b8e2a9bdf2a5a0119498d1dfbc2e..906b6b44c1dc7bbaa96e76d87349e68fd9e67411 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt
@@ -102,9 +102,12 @@ class WebRequestBuilder(
     ): File = withContext(Dispatchers.IO) {
         val fileName = "${UUID.nameUUIDFromBytes(url.toByteArray())}.zip"
         val file = File(FileStorageHelper.keyExportDirectory, fileName)
-        file.outputStream().use {
+        if (file.exists()) file.delete()
+        file.outputStream().use { fos ->
             Timber.v("Added $url to queue.")
-            distributionService.getKeyFiles(url).byteStream().copyTo(it, DEFAULT_BUFFER_SIZE)
+            distributionService.getKeyFiles(url).byteStream().use {
+                it.copyTo(fos, DEFAULT_BUFFER_SIZE)
+            }
             Timber.v("key file request successful.")
         }
         return@withContext file
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
index 889ab566942df188a9780255f3301afafc45daa3..48bc37e409fea1553cc046a816338afa94529ee2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
@@ -6,6 +6,7 @@ import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.risk.RiskLevel
 import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance
+import org.joda.time.Instant
 import java.util.Date
 
 /**
@@ -18,6 +19,11 @@ object LocalData {
 
     private val TAG: String? = LocalData::class.simpleName
 
+    private const val PREFERENCE_NEXT_TIME_RATE_LIMITING_UNLOCKS =
+        "preference_next_time_rate_limiting_unlocks"
+    private const val PREFERENCE_GOOGLE_API_PROVIDE_DIAGNOSIS_KEYS_CALL_COUNT =
+        "preference_google_api_provide_diagnosis_keys_call_count"
+
     /****************************************************
      * ONBOARDING DATA
      ****************************************************/
@@ -390,6 +396,40 @@ object LocalData {
         }
     }
 
+    var nextTimeRateLimitingUnlocks: Instant
+        get() {
+            return Instant.ofEpochMilli(
+                getSharedPreferenceInstance().getLong(
+                    PREFERENCE_NEXT_TIME_RATE_LIMITING_UNLOCKS,
+                    0L
+                )
+            )
+        }
+        set(value) {
+            getSharedPreferenceInstance().edit(true) {
+                putLong(
+                    PREFERENCE_NEXT_TIME_RATE_LIMITING_UNLOCKS,
+                    value.millis
+                )
+            }
+        }
+
+    var googleAPIProvideDiagnosisKeysCallCount: Int
+        get() {
+            return getSharedPreferenceInstance().getInt(
+                PREFERENCE_GOOGLE_API_PROVIDE_DIAGNOSIS_KEYS_CALL_COUNT,
+                0
+            )
+        }
+        set(value) {
+            getSharedPreferenceInstance().edit(true) {
+                putInt(
+                    PREFERENCE_GOOGLE_API_PROVIDE_DIAGNOSIS_KEYS_CALL_COUNT,
+                    value
+                )
+            }
+        }
+
     /**
      * Gets the last time of successful risk level calculation as long
      * from the EncryptedSharedPrefs
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDao.kt
index ec812964a597e8b6102eaa38a3d8b0bd2d3f3fda..0e6cbeadb1b6f5e2f6b5c06b6668ad3a80fab963 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDao.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDao.kt
@@ -35,6 +35,9 @@ interface KeyCacheDao {
     @Query("SELECT * FROM date")
     suspend fun getAllEntries(): List<KeyCacheEntity>
 
+    @Query("SELECT * FROM date WHERE id IN (:idList)")
+    suspend fun getAllEntries(idList: List<String>): List<KeyCacheEntity>
+
     @Query("DELETE FROM date")
     suspend fun clear()
 
@@ -44,6 +47,9 @@ interface KeyCacheDao {
     @Delete
     suspend fun deleteEntry(entity: KeyCacheEntity)
 
+    @Delete
+    suspend fun deleteEntries(entities: List<KeyCacheEntity>)
+
     @Insert
     suspend fun insertEntry(keyCacheEntity: KeyCacheEntity): Long
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheRepository.kt
index 9e3ce000310077d2827eefe94fb3ee4e5b2f4ecf..dd6b6425d39da8b125aac2b520d3e9f5f3ee7d5b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/keycache/KeyCacheRepository.kt
@@ -88,6 +88,14 @@ class KeyCacheRepository(private val keyCacheDao: KeyCacheDao) {
         keyCacheDao.clear()
     }
 
+    suspend fun clear(idList: List<String>) {
+        if (idList.isNotEmpty()) {
+            val entries = keyCacheDao.getAllEntries(idList)
+            entries.forEach { deleteFileForEntry(it) }
+            keyCacheDao.deleteEntries(entries)
+        }
+    }
+
     suspend fun getFilesFromEntries() = keyCacheDao
         .getAllEntries()
         .map { File(it.path) }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
index 37f736283fd6be6574b05560cf7dab98c6646e65..20f3a38eec5d70f161842cef456a43390e7a2ac3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
@@ -20,26 +20,29 @@
 package de.rki.coronawarnapp.transaction
 
 import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
-import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
 import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService
 import de.rki.coronawarnapp.storage.FileStorageHelper
 import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.API_SUBMISSION
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.CLOSE
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.FETCH_DATE_UPDATE
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.FILES_FROM_WEB_REQUESTS
+import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.QUOTA_CALCULATION
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.RETRIEVE_RISK_SCORE_PARAMS
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.SETUP
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.TOKEN
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.rollback
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.start
 import de.rki.coronawarnapp.util.CachedKeyFileHolder
+import de.rki.coronawarnapp.util.GoogleQuotaCalculator
+import de.rki.coronawarnapp.util.QuotaCalculator
 import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import org.joda.time.DateTime
 import org.joda.time.DateTimeZone
+import org.joda.time.Duration
 import org.joda.time.Instant
+import org.joda.time.chrono.GJChronology
 import timber.log.Timber
 import java.io.File
 import java.util.Date
@@ -90,6 +93,9 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
         /** Initial Setup of the Transaction and Transaction ID Generation and Date Lock */
         SETUP,
 
+        /** calculates the Quota so that the rate limiting is caught gracefully*/
+        QUOTA_CALCULATION,
+
         /** Initialisation of the identifying token used during the entire transaction */
         TOKEN,
 
@@ -118,12 +124,25 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
     /** atomic reference for the rollback value for created files during the transaction */
     private val exportFilesForRollback = AtomicReference<List<File>>()
 
+    private val progressTowardsQuotaForRollback = AtomicReference<Int>()
+
+    private const val QUOTA_RESET_PERIOD_IN_HOURS = 24
+
+    private val quotaCalculator: QuotaCalculator<Int> = GoogleQuotaCalculator(
+        incrementByAmount = 14,
+        quotaLimit = 20,
+        quotaResetPeriod = Duration.standardHours(QUOTA_RESET_PERIOD_IN_HOURS.toLong()),
+        quotaTimeZone = DateTimeZone.UTC,
+        quotaChronology = GJChronology.getInstanceUTC()
+    )
+
     suspend fun startWithConstraints() {
         val currentDate = DateTime(Instant.now(), DateTimeZone.UTC)
         val lastFetch = DateTime(
             LocalData.lastTimeDiagnosisKeysFromServerFetch(),
             DateTimeZone.UTC
         )
+
         if (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
             currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()
         ) {
@@ -152,6 +171,18 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
          ****************************************************/
         val currentDate = executeSetup()
 
+        /****************************************************
+         * CALCULATE QUOTA FOR PROVIDE DIAGNOSIS KEYS
+         ****************************************************/
+        val hasExceededQuota = executeQuotaCalculation()
+
+        // When we are above the Quote, cancel the execution entirely
+        if (hasExceededQuota) {
+            Timber.tag(TAG).w("above quota, skipping RetrieveDiagnosisKeys")
+            executeClose()
+            return@lockAndExecuteUnique
+        }
+
         /****************************************************
          * RETRIEVE TOKEN
          ****************************************************/
@@ -194,8 +225,9 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
             if (TOKEN.isInStateStack()) {
                 rollbackToken()
             }
-            if (FILES_FROM_WEB_REQUESTS.isInStateStack()) {
-                rollbackFilesFromWebRequests()
+            // we reset the quota only if the submission has not happened yet
+            if (QUOTA_CALCULATION.isInStateStack() && !API_SUBMISSION.isInStateStack()) {
+                rollbackProgressTowardsQuota()
             }
         } catch (e: Exception) {
             // We handle every exception through a RollbackException to make sure that a single EntryPoint
@@ -214,10 +246,9 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
         LocalData.googleApiToken(googleAPITokenForRollback.get())
     }
 
-    private suspend fun rollbackFilesFromWebRequests() {
-        Timber.v("rollback $FILES_FROM_WEB_REQUESTS")
-        KeyCacheRepository.getDateRepository(CoronaWarnApplication.getAppContext())
-            .clear()
+    private fun rollbackProgressTowardsQuota() {
+        Timber.v("rollback $QUOTA_CALCULATION")
+        quotaCalculator.resetProgressTowardsQuota(progressTowardsQuotaForRollback.get())
     }
 
     /**
@@ -230,6 +261,16 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
         currentDate
     }
 
+    /**
+     * Executes the QUOTA_CALCULATION Transaction State
+     */
+    private suspend fun executeQuotaCalculation() = executeState(
+        QUOTA_CALCULATION
+    ) {
+        progressTowardsQuotaForRollback.set(quotaCalculator.getProgressTowardsQuota())
+        quotaCalculator.calculateQuota()
+    }
+
     /**
      * Executes the TOKEN Transaction State
      */
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt
index 15437cf475839ca2d6fb7f8588fb58c37eac474e..97998f348d1a8ba6fc35b1a4d2dde5b53f0536a3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt
@@ -37,6 +37,7 @@ import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.withContext
 import timber.log.Timber
 import java.io.File
+import java.util.Collections
 import java.util.Date
 import java.util.UUID
 
@@ -97,22 +98,30 @@ object CachedKeyFileHolder {
             val deferredQueries: MutableCollection<Deferred<Any>> = mutableListOf()
             keyCache.deleteOutdatedEntries(uuidListFromServer)
             val missingDays = getMissingDaysFromDiff(serverDates)
+            val failedEntryCacheKeys = Collections.synchronizedList(mutableListOf<String>())
             if (missingDays.isNotEmpty()) {
                 // we have a date difference
                 deferredQueries.addAll(
                     missingDays
                         .map { getURLForDay(it) }
-                        .map { url -> async { url.createDayEntryForUrl() } }
+                        .map { url ->
+                            val cacheKey = url.generateCacheKeyFromString()
+                            async {
+                                try {
+                                    url.createDayEntryForUrl(cacheKey)
+                                } catch (e: Exception) {
+                                    Timber.v("failed entry: $cacheKey")
+                                    failedEntryCacheKeys.add(cacheKey)
+                                }
+                            }
+                        }
                 )
             }
             // execute the query plan
-            try {
-                deferredQueries.awaitAll()
-            } catch (e: Exception) {
-                // For an error we clear the cache to try again
-                keyCache.clear()
-                throw e
-            }
+            deferredQueries.awaitAll()
+            Timber.v("${failedEntryCacheKeys.size} failed entries ")
+            // For an error we clear the cache to try again
+            keyCache.clear(failedEntryCacheKeys)
             keyCache.getFilesFromEntries()
                 .also { it.forEach { file -> Timber.v("cached file:${file.path}") } }
         }
@@ -161,8 +170,8 @@ object CachedKeyFileHolder {
      * Creates a date entry in the Key Cache for a given String with a unique Key Name derived from the URL
      * and the URI of the downloaded File for that given key
      */
-    private suspend fun String.createDayEntryForUrl() = keyCache.createEntry(
-        this.generateCacheKeyFromString(),
+    private suspend fun String.createDayEntryForUrl(cacheKey: String) = keyCache.createEntry(
+        cacheKey,
         WebRequestBuilder.getInstance().asyncGetKeyFilesFromServer(this).toURI(),
         DAY
     )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleQuotaCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleQuotaCalculator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e7aace190ed9a6217487d3f76b6356c07c6aa267
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleQuotaCalculator.kt
@@ -0,0 +1,62 @@
+package de.rki.coronawarnapp.util
+
+import de.rki.coronawarnapp.storage.LocalData
+import org.joda.time.Chronology
+import org.joda.time.DateTime
+import org.joda.time.DateTimeZone
+import org.joda.time.Duration
+import org.joda.time.Instant
+
+/**
+ * This Calculator class takes multiple parameters to check if the Google API
+ * can be called or the Rate Limit has been reached. The Quota is expected to reset at
+ * the start of the day in the given timeZone and Chronology
+ *
+ * @property incrementByAmount The amount of Quota Calls to increment per Call
+ * @property quotaLimit The maximum amount of Quota Calls allowed before Rate Limiting
+ * @property quotaResetPeriod The Period after which the Quota Resets
+ * @property quotaTimeZone The Timezone to work in
+ * @property quotaChronology The Chronology to work in
+ */
+class GoogleQuotaCalculator(
+    val incrementByAmount: Int,
+    val quotaLimit: Int,
+    val quotaResetPeriod: Duration,
+    val quotaTimeZone: DateTimeZone,
+    val quotaChronology: Chronology
+) : QuotaCalculator<Int> {
+    override var hasExceededQuota: Boolean = false
+
+    override fun calculateQuota(): Boolean {
+        if (Instant.now().isAfter(LocalData.nextTimeRateLimitingUnlocks)) {
+            LocalData.nextTimeRateLimitingUnlocks = DateTime
+                .now(quotaTimeZone)
+                .withChronology(quotaChronology)
+                .plus(quotaResetPeriod)
+                .withTimeAtStartOfDay()
+                .toInstant()
+            LocalData.googleAPIProvideDiagnosisKeysCallCount = 0
+        }
+
+        if (LocalData.googleAPIProvideDiagnosisKeysCallCount <= quotaLimit) {
+            LocalData.googleAPIProvideDiagnosisKeysCallCount += incrementByAmount
+        }
+
+        hasExceededQuota = LocalData.googleAPIProvideDiagnosisKeysCallCount > quotaLimit
+
+        return hasExceededQuota
+    }
+
+    override fun resetProgressTowardsQuota(newProgress: Int) {
+        if (newProgress > quotaLimit) {
+            throw IllegalArgumentException("cannot reset progress to a value higher than the quota limit")
+        }
+        if (newProgress % incrementByAmount != 0) {
+            throw IllegalArgumentException("supplied progress is no multiple of $incrementByAmount")
+        }
+        LocalData.googleAPIProvideDiagnosisKeysCallCount = newProgress
+        hasExceededQuota = false
+    }
+
+    override fun getProgressTowardsQuota(): Int = LocalData.googleAPIProvideDiagnosisKeysCallCount
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/QuotaCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/QuotaCalculator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..682f4a6002b06ef548e04da1c68012372f3c6743
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/QuotaCalculator.kt
@@ -0,0 +1,29 @@
+package de.rki.coronawarnapp.util
+
+/**
+ * Class to check if a Quota has been reached based on the calculation done inside
+ * the Calculator
+ *
+ */
+interface QuotaCalculator<T> {
+    val hasExceededQuota: Boolean
+
+    /**
+     * This function is called to recalculate an old quota score
+     */
+    fun calculateQuota(): Boolean
+
+    /**
+     * Reset the quota progress
+     *
+     * @param newProgress new progress towards the quota
+     */
+    fun resetProgressTowardsQuota(newProgress: T)
+
+    /**
+     * Retrieve the current progress towards the quota
+     *
+     * @return current progress count
+     */
+    fun getProgressTowardsQuota(): T
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt
index e8c59395e8583acf1d6313379823f3b75d1edb94..929b0fcf82b875d4f6441ff31333b76ec2a4aaf6 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt
@@ -13,6 +13,7 @@ import io.mockk.mockk
 import io.mockk.mockkObject
 import io.mockk.unmockkAll
 import kotlinx.coroutines.runBlocking
+import org.joda.time.Instant
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -46,6 +47,10 @@ class RetrieveDiagnosisKeysTransactionTest {
         every { LocalData.lastTimeDiagnosisKeysFromServerFetch() } returns Date()
         every { LocalData.lastTimeDiagnosisKeysFromServerFetch(any()) } just Runs
         every { LocalData.googleApiToken() } returns UUID.randomUUID().toString()
+        every { LocalData.googleAPIProvideDiagnosisKeysCallCount = any() } just Runs
+        every { LocalData.googleAPIProvideDiagnosisKeysCallCount } returns 0
+        every { LocalData.nextTimeRateLimitingUnlocks = any() } just Runs
+        every { LocalData.nextTimeRateLimitingUnlocks } returns Instant.now()
     }
 
     @Test
@@ -57,6 +62,7 @@ class RetrieveDiagnosisKeysTransactionTest {
 
             coVerifyOrder {
                 RetrieveDiagnosisKeysTransaction["executeSetup"]()
+                RetrieveDiagnosisKeysTransaction["executeQuotaCalculation"]()
                 RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]()
                 RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](any<Date>())
                 RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>())
@@ -77,6 +83,7 @@ class RetrieveDiagnosisKeysTransactionTest {
 
             coVerifyOrder {
                 RetrieveDiagnosisKeysTransaction["executeSetup"]()
+                RetrieveDiagnosisKeysTransaction["executeQuotaCalculation"]()
                 RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]()
                 RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](any<Date>())
                 RetrieveDiagnosisKeysTransaction["executeAPISubmission"](
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt
index b03e57dd79478303134648a0bd26d9d8638d2b04..1d6e897947f32d9ffd2b948a821691e78cf1f184 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt
@@ -38,6 +38,7 @@ class CachedKeyFileHolderTest {
         every { KeyCacheRepository.getDateRepository(any()) } returns keyCacheRepository
         mockkObject(CachedKeyFileHolder)
         coEvery { keyCacheRepository.deleteOutdatedEntries(any()) } just Runs
+        coEvery { keyCacheRepository.clear(any()) } just Runs
     }
 
     /**
@@ -53,6 +54,7 @@ class CachedKeyFileHolderTest {
         every { CachedKeyFileHolder["checkForFreeSpace"]() } returns Unit
         every { CachedKeyFileHolder["getDatesFromServer"]() } returns arrayListOf<String>()
 
+
         runBlocking {
 
             CachedKeyFileHolder.asyncFetchFiles(date)
@@ -63,6 +65,7 @@ class CachedKeyFileHolderTest {
                 keyCacheRepository.deleteOutdatedEntries(any())
                 CachedKeyFileHolder["getMissingDaysFromDiff"](arrayListOf<String>())
                 keyCacheRepository.getDates()
+                keyCacheRepository.clear(emptyList())
                 keyCacheRepository.getFilesFromEntries()
             }
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleQuotaCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleQuotaCalculatorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7428be1353049ec3c94a2e6c1215b5b76e108089
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleQuotaCalculatorTest.kt
@@ -0,0 +1,295 @@
+package de.rki.coronawarnapp.util
+
+import de.rki.coronawarnapp.storage.LocalData
+import io.mockk.every
+import io.mockk.mockkObject
+import io.mockk.unmockkObject
+import org.joda.time.DateTime
+import org.joda.time.DateTimeUtils
+import org.joda.time.DateTimeZone
+import org.joda.time.Duration
+import org.joda.time.Instant
+import org.joda.time.chrono.GJChronology
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import testhelpers.BaseTest
+import timber.log.Timber
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicLong
+
+internal class GoogleQuotaCalculatorTest : BaseTest() {
+
+    private val timeInTest = DateTimeUtils.currentTimeMillis()
+
+    private lateinit var classUnderTest: GoogleQuotaCalculator
+    private val nextTimeRateLimitingUnlocksInTesting = AtomicLong()
+    private val googleAPIProvideDiagnosisKeysCallCount = AtomicInteger()
+
+    private val defaultIncrementByAmountInTest = 14
+    private val defaultQuotaLimitInTest = 20
+
+    @BeforeEach
+    fun setUpClassUnderTest() {
+        classUnderTest = GoogleQuotaCalculator(
+            incrementByAmount = defaultIncrementByAmountInTest,
+            quotaLimit = defaultQuotaLimitInTest,
+            quotaResetPeriod = Duration.standardHours(24),
+            quotaTimeZone = DateTimeZone.UTC,
+            quotaChronology = GJChronology.getInstanceUTC()
+        )
+        DateTimeUtils.setCurrentMillisFixed(timeInTest)
+
+        // Since LocalData is simple to mock
+        mockkObject(LocalData)
+        every { LocalData.nextTimeRateLimitingUnlocks = any() } answers {
+            nextTimeRateLimitingUnlocksInTesting.set((this.arg(0) as Instant).millis)
+        }
+        every { LocalData.nextTimeRateLimitingUnlocks } answers {
+            Instant.ofEpochMilli(nextTimeRateLimitingUnlocksInTesting.get())
+        }
+        every { LocalData.googleAPIProvideDiagnosisKeysCallCount = any() } answers {
+            googleAPIProvideDiagnosisKeysCallCount.set(this.arg(0))
+        }
+        every { LocalData.googleAPIProvideDiagnosisKeysCallCount } answers {
+            googleAPIProvideDiagnosisKeysCallCount.get()
+        }
+
+    }
+
+    @Test
+    fun `isAboveQuota false if called initially`() {
+        assertEquals(classUnderTest.hasExceededQuota, false)
+    }
+
+    @Test
+    fun `isAboveQuota true if called above quota limit when calling with amount bigger than one`() {
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            if (callNumber > 1) {
+                assertEquals(true, aboveQuota)
+            } else {
+                assertEquals(false, aboveQuota)
+            }
+        }
+    }
+
+    @Test
+    fun `getProgressTowardsQuota increases with calls to isAboveQuota but is stopped once increased above the quota`() {
+        var latestCallNumberWithoutLimiting = 1
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            val expectedIncrement = callNumber * defaultIncrementByAmountInTest
+            if (expectedIncrement >= defaultQuotaLimitInTest) {
+                assertEquals(
+                    (latestCallNumberWithoutLimiting + 1) * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+            } else {
+                assertEquals(
+                    callNumber * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+                latestCallNumberWithoutLimiting = callNumber
+            }
+        }
+    }
+
+    @Test
+    fun `getProgressTowardsQuota is reset and the quota is not recalculated but isAboveQuota should still be false`() {
+        var latestCallNumberWithoutLimiting = 1
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            val expectedIncrement = callNumber * defaultIncrementByAmountInTest
+            if (expectedIncrement >= defaultQuotaLimitInTest) {
+                assertEquals(
+                    (latestCallNumberWithoutLimiting + 1) * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+            } else {
+                assertEquals(
+                    callNumber * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+                latestCallNumberWithoutLimiting = callNumber
+            }
+        }
+
+        classUnderTest.resetProgressTowardsQuota(0)
+        assertEquals(false, classUnderTest.hasExceededQuota)
+    }
+
+    @Test
+    fun `getProgressTowardsQuota is reset but the reset value is no multiple of incrementByAmount`() {
+        var latestCallNumberWithoutLimiting = 1
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            val expectedIncrement = callNumber * defaultIncrementByAmountInTest
+            if (expectedIncrement >= defaultQuotaLimitInTest) {
+                assertEquals(
+                    (latestCallNumberWithoutLimiting + 1) * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+            } else {
+                assertEquals(
+                    callNumber * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+                latestCallNumberWithoutLimiting = callNumber
+            }
+        }
+
+        assertThrows<IllegalArgumentException> {
+            classUnderTest.resetProgressTowardsQuota(defaultIncrementByAmountInTest + 1)
+        }
+    }
+
+    @Test
+    fun `getProgressTowardsQuota is reset and the quota is not recalculated and the progress should update`() {
+        var latestCallNumberWithoutLimiting = 1
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            val expectedIncrement = callNumber * defaultIncrementByAmountInTest
+            if (expectedIncrement >= defaultQuotaLimitInTest) {
+                assertEquals(
+                    (latestCallNumberWithoutLimiting + 1) * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+            } else {
+                assertEquals(
+                    callNumber * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+                latestCallNumberWithoutLimiting = callNumber
+            }
+        }
+
+        val newProgressAfterReset = 14
+        classUnderTest.resetProgressTowardsQuota(newProgressAfterReset)
+        assertEquals(false, classUnderTest.hasExceededQuota)
+        assertEquals(newProgressAfterReset, classUnderTest.getProgressTowardsQuota())
+    }
+
+    @Test
+    fun `getProgressTowardsQuota is reset and the quota is not recalculated and the progress throws an error because of too high newProgress`() {
+        var latestCallNumberWithoutLimiting = 1
+        var progressBeforeReset: Int? = null
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            val expectedIncrement = callNumber * defaultIncrementByAmountInTest
+            if (expectedIncrement >= defaultQuotaLimitInTest) {
+                progressBeforeReset =
+                    (latestCallNumberWithoutLimiting + 1) * defaultIncrementByAmountInTest
+                assertEquals(
+                    (latestCallNumberWithoutLimiting + 1) * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+            } else {
+                assertEquals(
+                    callNumber * defaultIncrementByAmountInTest,
+                    classUnderTest.getProgressTowardsQuota()
+                )
+                latestCallNumberWithoutLimiting = callNumber
+            }
+        }
+
+        val newProgressAfterReset = defaultQuotaLimitInTest + 1
+        assertThrows<IllegalArgumentException> {
+            classUnderTest.resetProgressTowardsQuota(newProgressAfterReset)
+        }
+        assertEquals(true, classUnderTest.hasExceededQuota)
+        assertEquals(
+            (progressBeforeReset
+                ?: throw IllegalStateException("progressBeforeReset was not set during test")),
+            classUnderTest.getProgressTowardsQuota()
+        )
+    }
+
+    @Test
+    fun `isAboveQuota true if called above quota limit when calling with amount one`() {
+        classUnderTest = GoogleQuotaCalculator(
+            incrementByAmount = 1,
+            quotaLimit = 3,
+            quotaResetPeriod = Duration.standardHours(24),
+            quotaTimeZone = DateTimeZone.UTC,
+            quotaChronology = GJChronology.getInstanceUTC()
+        )
+        for (callNumber in 1..15) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            if (callNumber > 3) {
+                assertEquals(true, aboveQuota)
+            } else {
+                assertEquals(false, aboveQuota)
+            }
+        }
+    }
+
+    @Test
+    fun `isAboveQuota false if called above quota limit but next day resets quota`() {
+        for (callNumber in 1..5) {
+            classUnderTest.calculateQuota()
+            val aboveQuota = classUnderTest.hasExceededQuota
+            Timber.v("call number $callNumber above quota: $aboveQuota")
+            if (callNumber > 1) {
+                assertEquals(true, aboveQuota)
+            } else {
+                assertEquals(false, aboveQuota)
+            }
+        }
+
+        // Day Change
+        val timeInTestAdvancedByADay = timeInTest + Duration.standardDays(1).millis
+        DateTimeUtils.setCurrentMillisFixed(timeInTestAdvancedByADay)
+        classUnderTest.calculateQuota()
+        val aboveQuotaAfterDayAdvance = classUnderTest.hasExceededQuota
+        Timber.v("above quota after day advance: $aboveQuotaAfterDayAdvance")
+
+        assertEquals(false, aboveQuotaAfterDayAdvance)
+    }
+
+    @Test
+    fun `test if isAfter is affected by Timezone to make sure we do not run into Shifting Errors`() {
+        val testTimeUTC = DateTime(
+            timeInTest,
+            DateTimeZone.UTC
+        ).withChronology(GJChronology.getInstanceUTC())
+        val testTimeGMT = DateTime(
+            timeInTest,
+            DateTimeZone.forID("Etc/GMT+2")
+        ).withChronology(GJChronology.getInstanceUTC())
+
+        assertEquals(testTimeGMT, testTimeUTC)
+        assertEquals(testTimeGMT.millis, testTimeUTC.millis)
+
+        val testTimeUTCAfterGMT = testTimeUTC.plusMinutes(1)
+
+        assertEquals(true, testTimeUTCAfterGMT.isAfter(testTimeGMT))
+
+        val testTimeGMTAfterUTC = testTimeGMT.plusMinutes(1)
+
+        assertEquals(true, testTimeGMTAfterUTC.isAfter(testTimeUTC))
+
+    }
+
+    @AfterEach
+    fun cleanup() {
+        DateTimeUtils.setCurrentMillisSystem()
+        unmockkObject(LocalData)
+    }
+}
\ No newline at end of file