Merge "Import the code related to Key/Value backup encryption"
This commit is contained in:
40
packages/BackupEncryption/proto/key_value_listing.proto
Normal file
40
packages/BackupEncryption/proto/key_value_listing.proto
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package android_backup_crypto;
|
||||
|
||||
option java_package = "com.android.server.backup.encryption.protos";
|
||||
option java_outer_classname = "KeyValueListingProto";
|
||||
|
||||
// An entry of a key-value pair.
|
||||
message KeyValueEntry {
|
||||
// Plaintext key of the key-value pair.
|
||||
optional string key = 1;
|
||||
// SHA-256 MAC of the plaintext of the chunk containing the pair
|
||||
optional bytes hash = 2;
|
||||
}
|
||||
|
||||
// Describes the key/value pairs currently in the backup blob, mapping from the
|
||||
// plaintext key to the hash of the chunk containing the pair.
|
||||
//
|
||||
// This is local state stored on the device. It is never sent to the
|
||||
// backup server. See ChunkOrdering for how the device restores the
|
||||
// key-value pairs in the correct order.
|
||||
message KeyValueListing {
|
||||
repeated KeyValueEntry entries = 1;
|
||||
}
|
||||
31
packages/BackupEncryption/proto/key_value_pair.proto
Normal file
31
packages/BackupEncryption/proto/key_value_pair.proto
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package android_backup_crypto;
|
||||
|
||||
option java_package = "com.android.server.backup.encryption.protos";
|
||||
option java_outer_classname = "KeyValuePairProto";
|
||||
|
||||
// Serialized form of a key-value pair, when it is to be encrypted in a blob.
|
||||
// The backup blob for a key-value database consists of repeated encrypted
|
||||
// key-value pairs like this, in a randomized order. See ChunkOrdering for how
|
||||
// these are then reconstructed during a restore.
|
||||
message KeyValuePair {
|
||||
optional string key = 1;
|
||||
optional bytes value = 2;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.kv;
|
||||
|
||||
import static com.android.internal.util.Preconditions.checkState;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.backup.encryption.chunk.ChunkHash;
|
||||
import com.android.server.backup.encryption.chunking.ChunkHasher;
|
||||
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
|
||||
import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds a key value backup set from plaintext chunks. Computes a digest over the sorted SHA-256
|
||||
* hashes of the chunks.
|
||||
*/
|
||||
public class DecryptedChunkKvOutput implements DecryptedChunkOutput {
|
||||
@VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
|
||||
|
||||
private final ChunkHasher mChunkHasher;
|
||||
private final List<KeyValuePairProto.KeyValuePair> mUnsortedPairs = new ArrayList<>();
|
||||
private final List<ChunkHash> mUnsortedHashes = new ArrayList<>();
|
||||
private boolean mClosed;
|
||||
|
||||
/** Constructs a new instance which computers the digest using the given hasher. */
|
||||
public DecryptedChunkKvOutput(ChunkHasher chunkHasher) {
|
||||
mChunkHasher = chunkHasher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecryptedChunkOutput open() {
|
||||
// As we don't have any resources there is nothing to open.
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processChunk(byte[] plaintextBuffer, int length)
|
||||
throws IOException, InvalidKeyException {
|
||||
checkState(!mClosed, "Cannot process chunk after close()");
|
||||
KeyValuePairProto.KeyValuePair kvPair = new KeyValuePairProto.KeyValuePair();
|
||||
KeyValuePairProto.KeyValuePair.mergeFrom(kvPair, plaintextBuffer, 0, length);
|
||||
mUnsortedPairs.add(kvPair);
|
||||
// TODO(b/71492289): Update ChunkHasher to accept offset and length so we don't have to copy
|
||||
// the buffer into a smaller array.
|
||||
mUnsortedHashes.add(mChunkHasher.computeHash(Arrays.copyOf(plaintextBuffer, length)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// As we don't have any resources there is nothing to close.
|
||||
mClosed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getDigest() throws NoSuchAlgorithmException {
|
||||
checkState(mClosed, "Must close() before getDigest()");
|
||||
MessageDigest digest = getMessageDigest();
|
||||
Collections.sort(mUnsortedHashes);
|
||||
for (ChunkHash hash : mUnsortedHashes) {
|
||||
digest.update(hash.getHash());
|
||||
}
|
||||
return digest.digest();
|
||||
}
|
||||
|
||||
private static MessageDigest getMessageDigest() throws NoSuchAlgorithmException {
|
||||
return MessageDigest.getInstance(DIGEST_ALGORITHM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key value pairs from the backup, sorted lexicographically by key.
|
||||
*
|
||||
* <p>You must call {@link #close} first.
|
||||
*/
|
||||
public List<KeyValuePairProto.KeyValuePair> getPairs() {
|
||||
checkState(mClosed, "Must close() before getPairs()");
|
||||
Collections.sort(
|
||||
mUnsortedPairs,
|
||||
new Comparator<KeyValuePairProto.KeyValuePair>() {
|
||||
@Override
|
||||
public int compare(
|
||||
KeyValuePairProto.KeyValuePair o1, KeyValuePairProto.KeyValuePair o2) {
|
||||
return o1.key.compareTo(o2.key);
|
||||
}
|
||||
});
|
||||
return mUnsortedPairs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.kv;
|
||||
|
||||
import static com.android.internal.util.Preconditions.checkArgument;
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
|
||||
import com.android.server.backup.encryption.chunk.ChunkHash;
|
||||
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* Builds a {@link KeyValueListingProto.KeyValueListing}, which is a nano proto and so has no
|
||||
* builder.
|
||||
*/
|
||||
public class KeyValueListingBuilder {
|
||||
private final List<KeyValueListingProto.KeyValueEntry> mEntries = new ArrayList<>();
|
||||
|
||||
/** Adds a new pair entry to the listing. */
|
||||
public KeyValueListingBuilder addPair(String key, ChunkHash hash) {
|
||||
checkArgument(key.length() != 0, "Key must have non-zero length");
|
||||
checkNotNull(hash, "Hash must not be null");
|
||||
|
||||
KeyValueListingProto.KeyValueEntry entry = new KeyValueListingProto.KeyValueEntry();
|
||||
entry.key = key;
|
||||
entry.hash = hash.getHash();
|
||||
mEntries.add(entry);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Adds all pairs contained in a map, where the map is from key to hash. */
|
||||
public KeyValueListingBuilder addAll(Map<String, ChunkHash> map) {
|
||||
for (Entry<String, ChunkHash> entry : map.entrySet()) {
|
||||
addPair(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a new listing containing all the pairs added so far. */
|
||||
public KeyValueListingProto.KeyValueListing build() {
|
||||
if (mEntries.size() == 0) {
|
||||
return emptyListing();
|
||||
}
|
||||
|
||||
KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
|
||||
listing.entries = new KeyValueListingProto.KeyValueEntry[mEntries.size()];
|
||||
mEntries.toArray(listing.entries);
|
||||
return listing;
|
||||
}
|
||||
|
||||
/** Returns a new listing which does not contain any pairs. */
|
||||
public static KeyValueListingProto.KeyValueListing emptyListing() {
|
||||
KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
|
||||
listing.entries = KeyValueListingProto.KeyValueEntry.emptyArray();
|
||||
return listing;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package com.android.server.backup.encryption.tasks;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Accepts the plaintext bytes of decrypted chunks and writes them to some output. Also keeps track
|
||||
@@ -30,7 +31,7 @@ public interface DecryptedChunkOutput extends Closeable {
|
||||
*
|
||||
* @return {@code this}, to allow use with try-with-resources
|
||||
*/
|
||||
DecryptedChunkOutput open() throws IOException;
|
||||
DecryptedChunkOutput open() throws IOException, NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Writes the plaintext bytes of chunk to whatever output the implementation chooses. Also
|
||||
@@ -43,12 +44,13 @@ public interface DecryptedChunkOutput extends Closeable {
|
||||
* at index 0.
|
||||
* @param length The length in bytes of the plaintext contained in {@code plaintextBuffer}.
|
||||
*/
|
||||
void processChunk(byte[] plaintextBuffer, int length) throws IOException, InvalidKeyException;
|
||||
void processChunk(byte[] plaintextBuffer, int length)
|
||||
throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Returns the message digest of all the chunks processed by {@link #processChunk}.
|
||||
*
|
||||
* <p>You must call {@link Closeable#close()} before calling this method.
|
||||
*/
|
||||
byte[] getDigest();
|
||||
byte[] getDigest() throws NoSuchAlgorithmException;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.kv;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.os.Debug;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
import com.android.server.backup.encryption.chunk.ChunkHash;
|
||||
import com.android.server.backup.encryption.chunking.ChunkHasher;
|
||||
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Presubmit
|
||||
public class DecryptedChunkKvOutputTest {
|
||||
private static final String TEST_KEY_1 = "key_1";
|
||||
private static final String TEST_KEY_2 = "key_2";
|
||||
private static final byte[] TEST_VALUE_1 = {1, 2, 3};
|
||||
private static final byte[] TEST_VALUE_2 = {10, 11, 12, 13};
|
||||
private static final byte[] TEST_PAIR_1 = toByteArray(createPair(TEST_KEY_1, TEST_VALUE_1));
|
||||
private static final byte[] TEST_PAIR_2 = toByteArray(createPair(TEST_KEY_2, TEST_VALUE_2));
|
||||
private static final int TEST_BUFFER_SIZE = Math.max(TEST_PAIR_1.length, TEST_PAIR_2.length);
|
||||
|
||||
@Mock private ChunkHasher mChunkHasher;
|
||||
private DecryptedChunkKvOutput mOutput;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
when(mChunkHasher.computeHash(any()))
|
||||
.thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
|
||||
mOutput = new DecryptedChunkKvOutput(mChunkHasher);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void open_returnsInstance() throws Exception {
|
||||
assertThat(mOutput.open()).isEqualTo(mOutput);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processChunk_alreadyClosed_throws() throws Exception {
|
||||
mOutput.open();
|
||||
mOutput.close();
|
||||
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> mOutput.processChunk(TEST_PAIR_1, TEST_PAIR_1.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDigest_beforeClose_throws() throws Exception {
|
||||
// TODO: b/141356823 We should add a test which calls .open() here
|
||||
assertThrows(IllegalStateException.class, () -> mOutput.getDigest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDigest_returnsDigestOfSortedHashes() throws Exception {
|
||||
mOutput.open();
|
||||
Debug.waitForDebugger();
|
||||
mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
|
||||
mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
|
||||
mOutput.close();
|
||||
|
||||
byte[] actualDigest = mOutput.getDigest();
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance(DecryptedChunkKvOutput.DIGEST_ALGORITHM);
|
||||
Stream.of(TEST_PAIR_1, TEST_PAIR_2)
|
||||
.map(DecryptedChunkKvOutputTest::fakeHash)
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.forEachOrdered(hash -> digest.update(hash.getHash()));
|
||||
assertThat(actualDigest).isEqualTo(digest.digest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPairs_beforeClose_throws() throws Exception {
|
||||
// TODO: b/141356823 We should add a test which calls .open() here
|
||||
assertThrows(IllegalStateException.class, () -> mOutput.getPairs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPairs_returnsPairsSortedByKey() throws Exception {
|
||||
mOutput.open();
|
||||
// Write out of order to check that it sorts the chunks.
|
||||
mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length);
|
||||
mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length);
|
||||
mOutput.close();
|
||||
|
||||
List<KeyValuePairProto.KeyValuePair> pairs = mOutput.getPairs();
|
||||
|
||||
assertThat(
|
||||
isInOrder(
|
||||
pairs,
|
||||
Comparator.comparing(
|
||||
(KeyValuePairProto.KeyValuePair pair) -> pair.key)))
|
||||
.isTrue();
|
||||
assertThat(pairs).hasSize(2);
|
||||
assertThat(pairs.get(0).key).isEqualTo(TEST_KEY_1);
|
||||
assertThat(pairs.get(0).value).isEqualTo(TEST_VALUE_1);
|
||||
assertThat(pairs.get(1).key).isEqualTo(TEST_KEY_2);
|
||||
assertThat(pairs.get(1).value).isEqualTo(TEST_VALUE_2);
|
||||
}
|
||||
|
||||
private static KeyValuePairProto.KeyValuePair createPair(String key, byte[] value) {
|
||||
KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
|
||||
pair.key = key;
|
||||
pair.value = value;
|
||||
return pair;
|
||||
}
|
||||
|
||||
private boolean isInOrder(
|
||||
List<KeyValuePairProto.KeyValuePair> list,
|
||||
Comparator<KeyValuePairProto.KeyValuePair> comparator) {
|
||||
if (list.size() < 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<KeyValuePairProto.KeyValuePair> sortedList = new ArrayList<>(list);
|
||||
Collections.sort(sortedList, comparator);
|
||||
return list.equals(sortedList);
|
||||
}
|
||||
|
||||
private static byte[] toByteArray(KeyValuePairProto.KeyValuePair nano) {
|
||||
return KeyValuePairProto.KeyValuePair.toByteArray(nano);
|
||||
}
|
||||
|
||||
private static ChunkHash fakeHash(byte[] data) {
|
||||
return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.kv;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
import com.android.server.backup.encryption.chunk.ChunkHash;
|
||||
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Presubmit
|
||||
public class KeyValueListingBuilderTest {
|
||||
private static final String TEST_KEY_1 = "test_key_1";
|
||||
private static final String TEST_KEY_2 = "test_key_2";
|
||||
private static final ChunkHash TEST_HASH_1 =
|
||||
new ChunkHash(Arrays.copyOf(new byte[] {1, 2}, ChunkHash.HASH_LENGTH_BYTES));
|
||||
private static final ChunkHash TEST_HASH_2 =
|
||||
new ChunkHash(Arrays.copyOf(new byte[] {5, 6}, ChunkHash.HASH_LENGTH_BYTES));
|
||||
|
||||
private KeyValueListingBuilder mBuilder;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mBuilder = new KeyValueListingBuilder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPair_nullKey_throws() {
|
||||
assertThrows(NullPointerException.class, () -> mBuilder.addPair(null, TEST_HASH_1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPair_emptyKey_throws() {
|
||||
assertThrows(IllegalArgumentException.class, () -> mBuilder.addPair("", TEST_HASH_1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPair_nullHash_throws() {
|
||||
assertThrows(NullPointerException.class, () -> mBuilder.addPair(TEST_KEY_1, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_noPairs_buildsEmptyListing() {
|
||||
KeyValueListingProto.KeyValueListing listing = mBuilder.build();
|
||||
|
||||
assertThat(listing.entries).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void build_returnsCorrectListing() {
|
||||
mBuilder.addPair(TEST_KEY_1, TEST_HASH_1);
|
||||
|
||||
KeyValueListingProto.KeyValueListing listing = mBuilder.build();
|
||||
|
||||
assertThat(listing.entries.length).isEqualTo(1);
|
||||
assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
|
||||
assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addAll_addsAllPairsInMap() {
|
||||
ImmutableMap<String, ChunkHash> pairs =
|
||||
new ImmutableMap.Builder<String, ChunkHash>()
|
||||
.put(TEST_KEY_1, TEST_HASH_1)
|
||||
.put(TEST_KEY_2, TEST_HASH_2)
|
||||
.build();
|
||||
|
||||
mBuilder.addAll(pairs);
|
||||
KeyValueListingProto.KeyValueListing listing = mBuilder.build();
|
||||
|
||||
assertThat(listing.entries.length).isEqualTo(2);
|
||||
assertThat(listing.entries[0].key).isEqualTo(TEST_KEY_1);
|
||||
assertThat(listing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
|
||||
assertThat(listing.entries[1].key).isEqualTo(TEST_KEY_2);
|
||||
assertThat(listing.entries[1].hash).isEqualTo(TEST_HASH_2.getHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyListing_returnsListingWithoutAnyPairs() {
|
||||
KeyValueListingProto.KeyValueListing emptyListing = KeyValueListingBuilder.emptyListing();
|
||||
assertThat(emptyListing.entries).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user