summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreeHugger Robot <treehugger-gerrit@google.com>2019-09-13 14:45:32 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2019-09-13 14:45:32 +0000
commit272b953b9617c8efe2f52859f5fa89f5483c1a6c (patch)
tree144a4f1a3b9c2a5d814ca653e68053fe028b7ad3
parentb4c455b75578e0898742be0ac56213e64aa7196a (diff)
parentbb45da74cd427f1a7915f457a2c71e5199b73656 (diff)
Merge "Move TertiaryKeyRotationScheduler and TertiaryKeyRotationWindowedCount"
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java104
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java28
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java132
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java200
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java131
5 files changed, 588 insertions, 7 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java
new file mode 100644
index 000000000000..f16a68d64213
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.android.server.backup.encryption.keys;
+
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Schedules tertiary key rotations in a staggered fashion.
+ *
+ * <p>Apps are due a key rotation after a certain number of backups. Rotations are then staggerered
+ * over a period of time, through restricting the number of rotations allowed in a 24-hour window.
+ * This will causes the apps to enter a staggered cycle of regular rotations.
+ *
+ * <p>Note: the methods in this class are not optimized to be super fast. They make blocking IO to
+ * ensure that scheduler information is committed to disk, so that it is available after the user
+ * turns their device off and on. This ought to be fine as
+ *
+ * <ul>
+ * <li>It will be invoked before a backup, so should never be invoked on the UI thread
+ * <li>It will be invoked before a backup, so the vast amount of time is spent on the backup, not
+ * writing tiny amounts of data to disk.
+ * </ul>
+ */
+public class TertiaryKeyRotationScheduler {
+ /** Default number of key rotations allowed within 24 hours. */
+ private static final int KEY_ROTATION_LIMIT = 2;
+
+ /** A new instance, using {@code context} to determine where to store state. */
+ public static TertiaryKeyRotationScheduler getInstance(Context context) {
+ TertiaryKeyRotationWindowedCount windowedCount =
+ TertiaryKeyRotationWindowedCount.getInstance(context);
+ TertiaryKeyRotationTracker tracker = TertiaryKeyRotationTracker.getInstance(context);
+ return new TertiaryKeyRotationScheduler(tracker, windowedCount, KEY_ROTATION_LIMIT);
+ }
+
+ private final TertiaryKeyRotationTracker mTracker;
+ private final TertiaryKeyRotationWindowedCount mWindowedCount;
+ private final int mMaximumRotationsPerWindow;
+
+ /**
+ * A new instance.
+ *
+ * @param tracker Tracks how many times each application has backed up.
+ * @param windowedCount Tracks how many rotations have happened in the last 24 hours.
+ * @param maximumRotationsPerWindow The maximum number of key rotations allowed per 24 hours.
+ */
+ @VisibleForTesting
+ TertiaryKeyRotationScheduler(
+ TertiaryKeyRotationTracker tracker,
+ TertiaryKeyRotationWindowedCount windowedCount,
+ int maximumRotationsPerWindow) {
+ mTracker = tracker;
+ mWindowedCount = windowedCount;
+ mMaximumRotationsPerWindow = maximumRotationsPerWindow;
+ }
+
+ /**
+ * Returns {@code true} if the app with {@code packageName} is due having its key rotated.
+ *
+ * <p>This ought to be queried before backing up an app, to determine whether to do an
+ * incremental backup or a full backup. (A full backup forces key rotation.)
+ */
+ public boolean isKeyRotationDue(String packageName) {
+ if (mWindowedCount.getCount() >= mMaximumRotationsPerWindow) {
+ return false;
+ }
+ return mTracker.isKeyRotationDue(packageName);
+ }
+
+ /**
+ * Records that a backup happened for the app with the given {@code packageName}.
+ *
+ * <p>Each backup brings the app closer to the point at which a key rotation is due.
+ */
+ public void recordBackup(String packageName) {
+ mTracker.recordBackup(packageName);
+ }
+
+ /**
+ * Records a key rotation happened for the app with the given {@code packageName}.
+ *
+ * <p>This resets the countdown until the next key rotation is due.
+ */
+ public void recordKeyRotation(String packageName) {
+ mTracker.resetCountdown(packageName);
+ mWindowedCount.record();
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
index ec90f6c8c95e..1a281e79cc48 100644
--- a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package com.android.server.backup.encryption.keys;
+import static com.android.internal.util.Preconditions.checkArgument;
+
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Slog;
@@ -46,15 +48,27 @@ public class TertiaryKeyRotationTracker {
*/
public static TertiaryKeyRotationTracker getInstance(Context context) {
return new TertiaryKeyRotationTracker(
- context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE));
+ context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE),
+ MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION);
}
private final SharedPreferences mSharedPreferences;
+ private final int mMaxBackupsTillRotation;
- /** New instance, storing data in {@code mSharedPreferences}. */
+ /**
+ * New instance, storing data in {@code sharedPreferences} and initializing backup countdown to
+ * {@code maxBackupsTillRotation}.
+ */
@VisibleForTesting
- TertiaryKeyRotationTracker(SharedPreferences sharedPreferences) {
+ TertiaryKeyRotationTracker(SharedPreferences sharedPreferences, int maxBackupsTillRotation) {
+ checkArgument(
+ maxBackupsTillRotation >= 0,
+ String.format(
+ Locale.US,
+ "maxBackupsTillRotation should be non-negative but was %d",
+ maxBackupsTillRotation));
mSharedPreferences = sharedPreferences;
+ mMaxBackupsTillRotation = maxBackupsTillRotation;
}
/**
@@ -63,7 +77,7 @@ public class TertiaryKeyRotationTracker {
* @param packageName The package name of the app.
*/
public boolean isKeyRotationDue(String packageName) {
- return getBackupsSinceRotation(packageName) >= MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION;
+ return getBackupsSinceRotation(packageName) >= mMaxBackupsTillRotation;
}
/**
@@ -84,7 +98,7 @@ public class TertiaryKeyRotationTracker {
packageName,
Math.max(
0,
- MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION
+ mMaxBackupsTillRotation
- backupsSinceRotation)));
}
}
@@ -102,7 +116,7 @@ public class TertiaryKeyRotationTracker {
public void markAllForRotation() {
SharedPreferences.Editor editor = mSharedPreferences.edit();
for (String packageName : mSharedPreferences.getAll().keySet()) {
- editor.putInt(packageName, MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION);
+ editor.putInt(packageName, mMaxBackupsTillRotation);
}
editor.apply();
}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java
new file mode 100644
index 000000000000..b90343ad4b35
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.android.server.backup.encryption.keys;
+
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tracks (and commits to disk) how many key rotations have happened in the last 24 hours. This
+ * allows us to limit (and therefore stagger) the number of key rotations in a given period of time.
+ *
+ * <p>Note to engineers thinking of replacing the below with fancier algorithms and data structures:
+ * we expect the total size of this count at any time to be below however many rotations we allow in
+ * the window, which is going to be in single digits. Any changes that mean we write to disk more
+ * frequently, that the code is no longer resistant to clock changes, or that the code is more
+ * difficult to understand are almost certainly not worthwhile.
+ */
+public class TertiaryKeyRotationWindowedCount {
+ private static final String TAG = "TertiaryKeyRotCount";
+
+ private static final int WINDOW_IN_HOURS = 24;
+ private static final String LOG_FILE_NAME = "tertiary_key_rotation_windowed_count";
+
+ private final Clock mClock;
+ private final File mFile;
+ private ArrayList<Long> mEvents;
+
+ /** Returns a new instance, persisting state to the files dir of {@code context}. */
+ public static TertiaryKeyRotationWindowedCount getInstance(Context context) {
+ File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
+ return new TertiaryKeyRotationWindowedCount(logFile, Clock.systemDefaultZone());
+ }
+
+ /** A new instance, committing state to {@code file}, and reading time from {@code clock}. */
+ @VisibleForTesting
+ TertiaryKeyRotationWindowedCount(File file, Clock clock) {
+ mFile = file;
+ mClock = clock;
+ mEvents = new ArrayList<>();
+ try {
+ loadFromFile();
+ } catch (IOException e) {
+ Slog.e(TAG, "Error reading " + LOG_FILE_NAME, e);
+ }
+ }
+
+ /** Records a key rotation at the current time. */
+ public void record() {
+ mEvents.add(mClock.millis());
+ compact();
+ try {
+ saveToFile();
+ } catch (IOException e) {
+ Slog.e(TAG, "Error saving " + LOG_FILE_NAME, e);
+ }
+ }
+
+ /** Returns the number of key rotation that have been recorded in the window. */
+ public int getCount() {
+ compact();
+ return mEvents.size();
+ }
+
+ private void compact() {
+ long minimumTimestamp = getMinimumTimestamp();
+ long now = mClock.millis();
+ ArrayList<Long> compacted = new ArrayList<>();
+ for (long event : mEvents) {
+ if (event >= minimumTimestamp && event <= now) {
+ compacted.add(event);
+ }
+ }
+ mEvents = compacted;
+ }
+
+ private long getMinimumTimestamp() {
+ return mClock.millis() - TimeUnit.HOURS.toMillis(WINDOW_IN_HOURS) + 1;
+ }
+
+ private void loadFromFile() throws IOException {
+ if (!mFile.exists()) {
+ return;
+ }
+ try (FileInputStream fis = new FileInputStream(mFile);
+ DataInputStream dis = new DataInputStream(fis)) {
+ while (true) {
+ mEvents.add(dis.readLong());
+ }
+ } catch (EOFException eof) {
+ // expected
+ }
+ }
+
+ private void saveToFile() throws IOException {
+ // File size is maximum number of key rotations in window multiplied by 8 bytes, which is
+ // why
+ // we just overwrite it each time. We expect it will always be less than 100 bytes in size.
+ try (FileOutputStream fos = new FileOutputStream(mFile);
+ DataOutputStream dos = new DataOutputStream(fos)) {
+ for (long event : mEvents) {
+ dos.writeLong(event);
+ }
+ }
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java
new file mode 100644
index 000000000000..dfc7e2bfd4f7
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.android.server.backup.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Context;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for the tertiary key rotation scheduler */
+@RunWith(RobolectricTestRunner.class)
+public final class TertiaryKeyRotationSchedulerTest {
+
+ private static final int MAXIMUM_ROTATIONS_PER_WINDOW = 2;
+ private static final int MAX_BACKUPS_TILL_ROTATION = 31;
+ private static final String SHARED_PREFS_NAME = "tertiary_key_rotation_tracker";
+ private static final String PACKAGE_1 = "com.android.example1";
+ private static final String PACKAGE_2 = "com.android.example2";
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Mock private Clock mClock;
+
+ private File mFile;
+ private TertiaryKeyRotationScheduler mScheduler;
+
+ /** Setup the scheduler for test */
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mFile = temporaryFolder.newFile();
+ mScheduler =
+ new TertiaryKeyRotationScheduler(
+ new TertiaryKeyRotationTracker(
+ application.getSharedPreferences(
+ SHARED_PREFS_NAME, Context.MODE_PRIVATE),
+ MAX_BACKUPS_TILL_ROTATION),
+ new TertiaryKeyRotationWindowedCount(mFile, mClock),
+ MAXIMUM_ROTATIONS_PER_WINDOW);
+ }
+
+ /** Test we don't trigger a rotation straight off */
+ @Test
+ public void isKeyRotationDue_isFalseInitially() {
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+ }
+
+ /** Test we don't prematurely trigger a rotation */
+ @Test
+ public void isKeyRotationDue_isFalseAfterInsufficientBackups() {
+ simulateBackups(MAX_BACKUPS_TILL_ROTATION - 1);
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+ }
+
+ /** Test we do trigger a backup */
+ @Test
+ public void isKeyRotationDue_isTrueAfterEnoughBackups() {
+ simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
+ }
+
+ /** Test rotation will occur if the quota allows */
+ @Test
+ public void isKeyRotationDue_isTrueIfRotationQuotaRemainsInWindow() {
+ simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+ mScheduler.recordKeyRotation(PACKAGE_2);
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
+ }
+
+ /** Test rotation is blocked if the quota has been exhausted */
+ @Test
+ public void isKeyRotationDue_isFalseIfEnoughRotationsHaveHappenedInWindow() {
+ simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+ mScheduler.recordKeyRotation(PACKAGE_2);
+ mScheduler.recordKeyRotation(PACKAGE_2);
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+ }
+
+ /** Test rotation is due after one window has passed */
+ @Test
+ public void isKeyRotationDue_isTrueAfterAWholeWindowHasPassed() {
+ simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+ mScheduler.recordKeyRotation(PACKAGE_2);
+ mScheduler.recordKeyRotation(PACKAGE_2);
+ setTimeMillis(TimeUnit.HOURS.toMillis(24));
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue();
+ }
+
+ /** Test the rotation state changes after a rotation */
+ @Test
+ public void isKeyRotationDue_isFalseAfterRotation() {
+ simulateBackups(MAX_BACKUPS_TILL_ROTATION);
+ mScheduler.recordKeyRotation(PACKAGE_1);
+ assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse();
+ }
+
+ /** Test the rate limiting for a given window */
+ @Test
+ public void isKeyRotationDue_neverAllowsMoreThanInWindow() {
+ List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION);
+
+ // simulate backups of all apps each night
+ for (int i = 0; i < 300; i++) {
+ setTimeMillis(i * TimeUnit.HOURS.toMillis(24));
+ int rotationsThisNight = 0;
+ for (String app : apps) {
+ if (mScheduler.isKeyRotationDue(app)) {
+ rotationsThisNight++;
+ mScheduler.recordKeyRotation(app);
+ } else {
+ mScheduler.recordBackup(app);
+ }
+ }
+ assertThat(rotationsThisNight).isAtMost(MAXIMUM_ROTATIONS_PER_WINDOW);
+ }
+ }
+
+ /** Test that backups are staggered over the window */
+ @Test
+ public void isKeyRotationDue_naturallyStaggersBackupsOverTime() {
+ List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION);
+
+ HashMap<String, ArrayList<Integer>> rotationDays = new HashMap<>();
+ for (String app : apps) {
+ rotationDays.put(app, new ArrayList<>());
+ }
+
+ // simulate backups of all apps each night
+ for (int i = 0; i < 300; i++) {
+ setTimeMillis(i * TimeUnit.HOURS.toMillis(24));
+ for (String app : apps) {
+ if (mScheduler.isKeyRotationDue(app)) {
+ rotationDays.get(app).add(i);
+ mScheduler.recordKeyRotation(app);
+ } else {
+ mScheduler.recordBackup(app);
+ }
+ }
+ }
+
+ for (String app : apps) {
+ List<Integer> days = rotationDays.get(app);
+ for (int i = 1; i < days.size(); i++) {
+ assertThat(days.get(i) - days.get(i - 1)).isEqualTo(MAX_BACKUPS_TILL_ROTATION + 1);
+ }
+ }
+ }
+
+ private ArrayList<String> makeTestApps(int n) {
+ ArrayList<String> apps = new ArrayList<>();
+ for (int i = 0; i < n; i++) {
+ apps.add(String.format(Locale.US, "com.android.app%d", i));
+ }
+ return apps;
+ }
+
+ private void simulateBackups(int numberOfBackups) {
+ while (numberOfBackups > 0) {
+ mScheduler.recordBackup(PACKAGE_1);
+ numberOfBackups--;
+ }
+ }
+
+ private void setTimeMillis(long timeMillis) {
+ when(mClock.millis()).thenReturn(timeMillis);
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java
new file mode 100644
index 000000000000..bd309779f303
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.android.server.backup.encryption.keys;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Clock;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link TertiaryKeyRotationWindowedCount}. */
+@RunWith(RobolectricTestRunner.class)
+public class TertiaryKeyRotationWindowedCountTest {
+ private static final int TIMESTAMP_SIZE_IN_BYTES = 8;
+
+ @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Mock private Clock mClock;
+
+ private File mFile;
+ private TertiaryKeyRotationWindowedCount mWindowedcount;
+
+ /** Setup the windowed counter for testing */
+ @Before
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+ mFile = mTemporaryFolder.newFile();
+ mWindowedcount = new TertiaryKeyRotationWindowedCount(mFile, mClock);
+ }
+
+ /** Test handling bad files */
+ @Test
+ public void constructor_doesNotFailForBadFile() throws IOException {
+ new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock);
+ }
+
+ /** Test the count is 0 to start */
+ @Test
+ public void getCount_isZeroInitially() {
+ assertThat(mWindowedcount.getCount()).isEqualTo(0);
+ }
+
+ /** Test the count is correct for a time window */
+ @Test
+ public void getCount_includesResultsInLastTwentyFourHours() {
+ setTimeMillis(0);
+ mWindowedcount.record();
+ setTimeMillis(TimeUnit.HOURS.toMillis(4));
+ mWindowedcount.record();
+ setTimeMillis(TimeUnit.HOURS.toMillis(23));
+ mWindowedcount.record();
+ mWindowedcount.record();
+ assertThat(mWindowedcount.getCount()).isEqualTo(4);
+ }
+
+ /** Test old results are ignored */
+ @Test
+ public void getCount_ignoresResultsOlderThanTwentyFourHours() {
+ setTimeMillis(0);
+ mWindowedcount.record();
+ setTimeMillis(TimeUnit.HOURS.toMillis(24));
+ assertThat(mWindowedcount.getCount()).isEqualTo(0);
+ }
+
+ /** Test future events are removed if the clock moves backways (e.g. DST, TZ change) */
+ @Test
+ public void getCount_removesFutureEventsIfClockHasChanged() {
+ setTimeMillis(1000);
+ mWindowedcount.record();
+ setTimeMillis(0);
+ assertThat(mWindowedcount.getCount()).isEqualTo(0);
+ }
+
+ /** Check recording doesn't fail for a bad file */
+ @Test
+ public void record_doesNotFailForBadFile() throws Exception {
+ new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock).record();
+ }
+
+ /** Checks the state is persisted */
+ @Test
+ public void record_persistsStateToDisk() {
+ setTimeMillis(0);
+ mWindowedcount.record();
+ assertThat(new TertiaryKeyRotationWindowedCount(mFile, mClock).getCount()).isEqualTo(1);
+ }
+
+ /** Test the file doesn't contain unnecessary data */
+ @Test
+ public void record_compactsFileToLast24Hours() {
+ setTimeMillis(0);
+ mWindowedcount.record();
+ assertThat(mFile.length()).isEqualTo(TIMESTAMP_SIZE_IN_BYTES);
+ setTimeMillis(1);
+ mWindowedcount.record();
+ assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES);
+ setTimeMillis(TimeUnit.HOURS.toMillis(24));
+ mWindowedcount.record();
+ assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES);
+ }
+
+ private void setTimeMillis(long timeMillis) {
+ when(mClock.millis()).thenReturn(timeMillis);
+ }
+}