aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/core/test
diff options
context:
space:
mode:
authorGravatar Rich Gowman <rgowman@google.com>2018-06-12 10:27:17 -0400
committerGravatar Rich Gowman <rgowman@google.com>2018-06-12 10:27:17 -0400
commitcf2899a085f7ceca3fad2d1fb5336be25cecd7ff (patch)
tree38c835a29fcda279c8dd220781d2b5c726da307f /Firestore/core/test
parent1597765af8c897ab73d21d6d404f8eeede7890b1 (diff)
parent9307f4893008f7d6cf9473e906d4c896546c5c8c (diff)
Merge remote-tracking branch 'origin/master' into rsgowman/protobuf_cpp
Also "fixed" BadFieldValueTagWithOtherValidTagsPresent test by changing 'false' to 'true'. Details: Depending on the version of nanopb, nanopb would explicitly encode 'false', which shouldn't be done in proto3. When it's explicitly encoded, the test worked properly. But when it was (properly) dropped, the invalid tag is the only field that's actually encoded, thus violating the assumptions of the test, leading to a test failure. s/false/true fixes it, as now the boolean_value field is (properly) encoded regardless of version.
Diffstat (limited to 'Firestore/core/test')
-rw-r--r--Firestore/core/test/firebase/firestore/remote/serializer_test.cc241
-rw-r--r--Firestore/core/test/firebase/firestore/util/CMakeLists.txt13
-rw-r--r--Firestore/core/test/firebase/firestore/util/hashing_test.cc18
-rw-r--r--Firestore/core/test/firebase/firestore/util/string_format_apple_test.mm60
-rw-r--r--Firestore/core/test/firebase/firestore/util/type_traits_apple_test.mm50
5 files changed, 353 insertions, 29 deletions
diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc
index ba9ea47..1125fb4 100644
--- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc
+++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc
@@ -146,13 +146,21 @@ class SerializerTest : public ::testing::Test {
*
* @param status the expected (failed) status. Only the code() is verified.
*/
- void ExpectFailedStatusDuringDecode(Status status,
- const std::vector<uint8_t>& bytes) {
+ void ExpectFailedStatusDuringFieldValueDecode(
+ Status status, const std::vector<uint8_t>& bytes) {
StatusOr<FieldValue> bad_status = serializer.DecodeFieldValue(bytes);
ASSERT_NOT_OK(bad_status);
EXPECT_EQ(status.code(), bad_status.status().code());
}
+ void ExpectFailedStatusDuringMaybeDocumentDecode(
+ Status status, const std::vector<uint8_t>& bytes) {
+ StatusOr<std::unique_ptr<MaybeDocument>> bad_status =
+ serializer.DecodeMaybeDocument(bytes);
+ ASSERT_NOT_OK(bad_status);
+ EXPECT_EQ(status.code(), bad_status.status().code());
+ }
+
v1beta1::Value ValueProto(nullptr_t) {
std::vector<uint8_t> bytes =
EncodeFieldValue(&serializer, FieldValue::NullValue());
@@ -230,20 +238,21 @@ class SerializerTest : public ::testing::Test {
/**
* Creates entries in the proto that we don't care about.
*
- * We ignore create time in our serializer. We never set it, and never read it
- * (other than to throw it away). But the server could (and probably does) set
- * it, so we need to be able to discard it properly. The ExpectRoundTrip deals
- * with this asymmetry.
+ * We ignore certain fields in our serializer. We never set them, and never
+ * read them (other than to throw them away). But the server could (and
+ * probably does) set them, so we need to be able to discard them properly.
+ * The ExpectRoundTrip deals with this asymmetry.
*
* This method adds these ignored fields to the proto.
*/
void TouchIgnoredBatchGetDocumentsResponseFields(
v1beta1::BatchGetDocumentsResponse* proto) {
+ proto->set_transaction("random bytes");
+
// TODO(rsgowman): This method currently assumes that this is a 'found'
// document. We (probably) will need to adjust this to work with NoDocuments
// too.
v1beta1::Document* doc_proto = proto->mutable_found();
-
google::protobuf::Timestamp* create_time_proto =
doc_proto->mutable_create_time();
create_time_proto->set_seconds(8765);
@@ -465,6 +474,63 @@ TEST_F(SerializerTest, EncodesNestedObjects) {
ExpectRoundTrip(model, proto, FieldValue::Type::Object);
}
+TEST_F(SerializerTest, EncodesFieldValuesWithRepeatedEntries) {
+ // Technically, serialized Value protos can contain multiple values. (The last
+ // one "wins".) However, well-behaved proto emitters (such as libprotobuf)
+ // won't generate that, so to test, we either need to use hand-crafted, raw
+ // bytes or use a proto message that's *almost* the same as the real one, such
+ // that when it's encoded, you can generate these repeated fields. (This is
+ // how libprotobuf tests itself.)
+ //
+ // Using libprotobuf for this purpose is mildly inconvenient for us, since we
+ // don't run protoc as part of the build process, so we'd need to either add
+ // these fake messages to our protos tree (Protos/testprotos?) and then check
+ // in the results (which isn't great when writing new tests). Fortunately, we
+ // have another alternative: nanopb.
+ //
+ // So we'll create a nanopb struct that *looks* like
+ // google_firestore_v1beta1_Value, and then populate and serialize it using
+ // the normal nanopb mechanisms. This should give us a wire-compatible Value
+ // message, but with multiple values set.
+
+ // Copy of the real one (from the nanopb generated document.pb.h), but with
+ // only boolean_value and integer_value.
+ struct google_firestore_v1beta1_Value_Fake {
+ bool boolean_value;
+ int64_t integer_value;
+ };
+
+ // Copy of the real one (from the nanopb generated document.pb.c), but with
+ // only boolean_value and integer_value.
+ const pb_field_t google_firestore_v1beta1_Value_fields_Fake[3] = {
+ PB_FIELD(1, BOOL, SINGULAR, STATIC, FIRST,
+ google_firestore_v1beta1_Value_Fake, boolean_value,
+ boolean_value, 0),
+ PB_FIELD(2, INT64, SINGULAR, STATIC, OTHER,
+ google_firestore_v1beta1_Value_Fake, integer_value,
+ boolean_value, 0),
+ PB_LAST_FIELD,
+ };
+
+ // Craft the bytes. boolean_value has a smaller tag, so it'll get encoded
+ // first. Implying integer_value should "win".
+ google_firestore_v1beta1_Value_Fake crafty_value{false, int64_t{42}};
+ std::vector<uint8_t> bytes(128);
+ pb_ostream_t stream = pb_ostream_from_buffer(bytes.data(), bytes.size());
+ pb_encode(&stream, google_firestore_v1beta1_Value_fields_Fake, &crafty_value);
+ bytes.resize(stream.bytes_written);
+
+ // Decode the bytes into the model
+ StatusOr<FieldValue> actual_model_status = serializer.DecodeFieldValue(bytes);
+ EXPECT_OK(actual_model_status);
+ FieldValue actual_model = actual_model_status.ValueOrDie();
+
+ // Ensure the decoded model is as expected.
+ FieldValue expected_model = FieldValue::IntegerValue(42);
+ EXPECT_EQ(FieldValue::Type::Integer, actual_model.type());
+ EXPECT_EQ(expected_model, actual_model);
+}
+
TEST_F(SerializerTest, BadNullValue) {
std::vector<uint8_t> bytes =
EncodeFieldValue(&serializer, FieldValue::NullValue());
@@ -472,7 +538,7 @@ TEST_F(SerializerTest, BadNullValue) {
// Alter the null value from 0 to 1.
Mutate(&bytes[1], /*expected_initial_value=*/0, /*new_value=*/1);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -483,7 +549,7 @@ TEST_F(SerializerTest, BadBoolValue) {
// Alter the bool value from 1 to 2. (Value values are 0,1)
Mutate(&bytes[1], /*expected_initial_value=*/1, /*new_value=*/2);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -502,7 +568,7 @@ TEST_F(SerializerTest, BadIntegerValue) {
bytes.resize(12);
bytes[11] = 0x7f;
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -514,7 +580,7 @@ TEST_F(SerializerTest, BadStringValue) {
// used by the encoded tag.)
Mutate(&bytes[2], /*expected_initial_value=*/1, /*new_value=*/5);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -525,7 +591,7 @@ TEST_F(SerializerTest, BadTimestampValue_TooLarge) {
// Add some time, which should push us above the maximum allowed timestamp.
Mutate(&bytes[4], 0x82, 0x83);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -536,11 +602,16 @@ TEST_F(SerializerTest, BadTimestampValue_TooSmall) {
// Remove some time, which should push us below the minimum allowed timestamp.
Mutate(&bytes[4], 0x92, 0x91);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
-TEST_F(SerializerTest, BadTag) {
+TEST_F(SerializerTest, BadFieldValueTagAndNoOtherTagPresent) {
+ // A bad tag should be ignored. But if there are *no* valid tags, then we
+ // don't know the type of the FieldValue. Although it might be reasonable to
+ // assume some sort of default type in this situation, we've decided to fail
+ // the deserialization process in this case instead.
+
std::vector<uint8_t> bytes =
EncodeFieldValue(&serializer, FieldValue::NullValue());
@@ -550,15 +621,57 @@ TEST_F(SerializerTest, BadTag) {
// Specifically 31. 0xf8 represents field number 31 encoded as a varint.
Mutate(&bytes[0], /*expected_initial_value=*/0x58, /*new_value=*/0xf8);
- // TODO(rsgowman): The behaviour is *temporarily* slightly different during
- // development; this will cause a failed assertion rather than a failed
- // status. Remove this EXPECT_ANY_THROW statement (and reenable the
- // following commented out statement) once the corresponding assert has been
- // removed from serializer.cc.
- EXPECT_ANY_THROW(ExpectFailedStatusDuringDecode(
- Status(FirestoreErrorCode::DataLoss, "ignored"), bytes));
- // ExpectFailedStatusDuringDecode(
- // Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
+ ExpectFailedStatusDuringFieldValueDecode(
+ Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
+}
+
+TEST_F(SerializerTest, BadFieldValueTagWithOtherValidTagsPresent) {
+ // A bad tag should be ignored, in which case, we should successfully
+ // deserialize the rest of the bytes as if it wasn't there. To craft these
+ // bytes, we'll use the same technique as
+ // EncodesFieldValuesWithRepeatedEntries (so go read the comments there
+ // first).
+
+ // Copy of the real one (from the nanopb generated document.pb.h), but with
+ // only boolean_value and integer_value.
+ struct google_firestore_v1beta1_Value_Fake {
+ bool boolean_value;
+ int64_t integer_value;
+ };
+
+ // Copy of the real one (from the nanopb generated document.pb.c), but with
+ // only boolean_value and integer_value. Also modified such that integer_value
+ // now has an invalid tag (instead of 2).
+ const int invalid_tag = 31;
+ const pb_field_t google_firestore_v1beta1_Value_fields_Fake[3] = {
+ PB_FIELD(1, BOOL, SINGULAR, STATIC, FIRST,
+ google_firestore_v1beta1_Value_Fake, boolean_value,
+ boolean_value, 0),
+ PB_FIELD(invalid_tag, INT64, SINGULAR, STATIC, OTHER,
+ google_firestore_v1beta1_Value_Fake, integer_value,
+ boolean_value, 0),
+ PB_LAST_FIELD,
+ };
+
+ // Craft the bytes. boolean_value has a smaller tag, so it'll get encoded
+ // first, normally implying integer_value should "win". Except that
+ // integer_value isn't a valid tag, so it should be ignored here.
+ google_firestore_v1beta1_Value_Fake crafty_value{true, int64_t{42}};
+ std::vector<uint8_t> bytes(128);
+ pb_ostream_t stream = pb_ostream_from_buffer(bytes.data(), bytes.size());
+ pb_encode(&stream, google_firestore_v1beta1_Value_fields_Fake, &crafty_value);
+ bytes.resize(stream.bytes_written);
+
+ // Decode the bytes into the model
+ StatusOr<FieldValue> actual_model_status = serializer.DecodeFieldValue(bytes);
+ Status s = actual_model_status.status();
+ EXPECT_OK(actual_model_status);
+ FieldValue actual_model = actual_model_status.ValueOrDie();
+
+ // Ensure the decoded model is as expected.
+ FieldValue expected_model = FieldValue::BooleanValue(true);
+ EXPECT_EQ(FieldValue::Type::Boolean, actual_model.type());
+ EXPECT_EQ(expected_model, actual_model);
}
TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) {
@@ -570,7 +683,7 @@ TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) {
// would do.)
Mutate(&bytes[0], /*expected_initial_value=*/0x08, /*new_value=*/0x0a);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -581,7 +694,7 @@ TEST_F(SerializerTest, TagStringWiretypeVarintMismatch) {
// 0x88 represents a string value encoded as a varint.
Mutate(&bytes[0], /*expected_initial_value=*/0x8a, /*new_value=*/0x88);
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -594,13 +707,13 @@ TEST_F(SerializerTest, IncompleteFieldValue) {
ASSERT_EQ(0x00, bytes[1]);
bytes.pop_back();
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
TEST_F(SerializerTest, IncompleteTag) {
std::vector<uint8_t> bytes;
- ExpectFailedStatusDuringDecode(
+ ExpectFailedStatusDuringFieldValueDecode(
Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
}
@@ -694,6 +807,69 @@ TEST_F(SerializerTest, EncodesNonEmptyDocument) {
ExpectRoundTrip(key, fields, update_time, proto);
}
+TEST_F(SerializerTest,
+ BadBatchGetDocumentsResponseTagWithOtherValidTagsPresent) {
+ // A bad tag should be ignored, in which case, we should successfully
+ // deserialize the rest of the bytes as if it wasn't there. To craft these
+ // bytes, we'll use the same technique as
+ // EncodesFieldValuesWithRepeatedEntries (so go read the comments there
+ // first).
+
+ // Copy of the real one (from the nanopb generated firestore.pb.h), but with
+ // only "missing" (a field from the original proto) and an extra_field.
+ struct google_firestore_v1beta1_BatchGetDocumentsResponse_Fake {
+ pb_callback_t missing;
+ int64_t extra_field;
+ };
+
+ // Copy of the real one (from the nanopb generated firestore.pb.c), but with
+ // only missing and an extra_field. Also modified such that extra_field
+ // now has a tag of 31.
+ const int invalid_tag = 31;
+ const pb_field_t
+ google_firestore_v1beta1_BatchGetDocumentsResponse_fields_Fake[3] = {
+ PB_FIELD(2, STRING, SINGULAR, CALLBACK, FIRST,
+ google_firestore_v1beta1_BatchGetDocumentsResponse_Fake,
+ missing, missing, 0),
+ PB_FIELD(invalid_tag, INT64, SINGULAR, STATIC, OTHER,
+ google_firestore_v1beta1_BatchGetDocumentsResponse_Fake,
+ extra_field, missing, 0),
+ PB_LAST_FIELD,
+ };
+
+ const char* missing_value = "projects/p/databases/d/documents/one/two";
+ google_firestore_v1beta1_BatchGetDocumentsResponse_Fake crafty_value;
+ crafty_value.missing.funcs.encode =
+ [](pb_ostream_t* stream, const pb_field_t* field, void* const* arg) {
+ const char* missing_value = static_cast<const char*>(*arg);
+ if (!pb_encode_tag_for_field(stream, field)) return false;
+ return pb_encode_string(stream,
+ reinterpret_cast<const uint8_t*>(missing_value),
+ strlen(missing_value));
+ };
+ crafty_value.missing.arg = const_cast<char*>(missing_value);
+ crafty_value.extra_field = 42;
+
+ std::vector<uint8_t> bytes(128);
+ pb_ostream_t stream = pb_ostream_from_buffer(bytes.data(), bytes.size());
+ pb_encode(&stream,
+ google_firestore_v1beta1_BatchGetDocumentsResponse_fields_Fake,
+ &crafty_value);
+ bytes.resize(stream.bytes_written);
+
+ // Decode the bytes into the model
+ StatusOr<std::unique_ptr<MaybeDocument>> actual_model_status =
+ serializer.DecodeMaybeDocument(bytes);
+ EXPECT_OK(actual_model_status);
+ std::unique_ptr<MaybeDocument> actual_model =
+ std::move(actual_model_status).ValueOrDie();
+
+ // Ensure the decoded model is as expected.
+ NoDocument expected_model =
+ NoDocument(Key("one/two"), SnapshotVersion::None());
+ EXPECT_EQ(expected_model, *actual_model.get());
+}
+
TEST_F(SerializerTest, DecodesNoDocument) {
// We can't actually *encode* a NoDocument; the method exposed by the
// serializer requires both the document key and contents (as an ObjectValue,
@@ -713,5 +889,16 @@ TEST_F(SerializerTest, DecodesNoDocument) {
ExpectNoDocumentDeserializationRoundTrip(key, read_time, proto);
}
+TEST_F(SerializerTest, DecodeMaybeDocWithoutFoundOrMissingSetShouldFail) {
+ v1beta1::BatchGetDocumentsResponse proto;
+
+ std::vector<uint8_t> bytes(proto.ByteSizeLong());
+ bool status = proto.SerializeToArray(bytes.data(), bytes.size());
+ EXPECT_TRUE(status);
+
+ ExpectFailedStatusDuringMaybeDocumentDecode(
+ Status(FirestoreErrorCode::DataLoss, "ignored"), bytes);
+}
+
// TODO(rsgowman): Test [en|de]coding multiple protos into the same output
// vector.
diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt
index bcb1c84..44a3b61 100644
--- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt
+++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt
@@ -98,7 +98,7 @@ cc_test(
async_tests_util.h
DEPENDS
firebase_firestore_util_executor_std
- firebase_firestore_util_async_queue
+ firebase_firestore_util
)
if(HAVE_LIBDISPATCH)
@@ -111,7 +111,7 @@ if(HAVE_LIBDISPATCH)
async_tests_util.h
DEPENDS
firebase_firestore_util_executor_libdispatch
- firebase_firestore_util_async_queue
+ firebase_firestore_util
)
endif()
@@ -137,3 +137,12 @@ cc_test(
firebase_firestore_util
gmock
)
+
+if(APPLE)
+ target_sources(
+ firebase_firestore_util_test
+ PUBLIC
+ string_format_apple_test.mm
+ type_traits_apple_test.mm
+ )
+endif()
diff --git a/Firestore/core/test/firebase/firestore/util/hashing_test.cc b/Firestore/core/test/firebase/firestore/util/hashing_test.cc
index e5d9ff8..2c5c2f7 100644
--- a/Firestore/core/test/firebase/firestore/util/hashing_test.cc
+++ b/Firestore/core/test/firebase/firestore/util/hashing_test.cc
@@ -16,6 +16,9 @@
#include "Firestore/core/src/firebase/firestore/util/hashing.h"
+#include <map>
+#include <string>
+
#include "absl/strings/string_view.h"
#include "gtest/gtest.h"
@@ -29,6 +32,21 @@ struct HasHashMember {
}
};
+TEST(HashingTest, HasStdHash) {
+ EXPECT_TRUE(impl::has_std_hash<float>::value);
+ EXPECT_TRUE(impl::has_std_hash<double>::value);
+ EXPECT_TRUE(impl::has_std_hash<int>::value);
+ EXPECT_TRUE(impl::has_std_hash<int64_t>::value);
+ EXPECT_TRUE(impl::has_std_hash<std::string>::value);
+ EXPECT_TRUE(impl::has_std_hash<void*>::value);
+ EXPECT_TRUE(impl::has_std_hash<const char*>::value);
+
+ struct Foo {};
+ EXPECT_FALSE(impl::has_std_hash<Foo>::value);
+ EXPECT_FALSE(impl::has_std_hash<absl::string_view>::value);
+ EXPECT_FALSE((impl::has_std_hash<std::map<std::string, std::string>>::value));
+}
+
TEST(HashingTest, Int) {
ASSERT_EQ(std::hash<int>{}(0), Hash(0));
}
diff --git a/Firestore/core/test/firebase/firestore/util/string_format_apple_test.mm b/Firestore/core/test/firebase/firestore/util/string_format_apple_test.mm
new file mode 100644
index 0000000..f0bcd35
--- /dev/null
+++ b/Firestore/core/test/firebase/firestore/util/string_format_apple_test.mm
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Google
+ *
+ * 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.
+ */
+
+#include "Firestore/core/src/firebase/firestore/util/string_format.h"
+
+#import <Foundation/NSString.h>
+
+#include "gtest/gtest.h"
+
+@interface FSTDescribable : NSObject
+@end
+
+@implementation FSTDescribable
+
+- (NSString*)description {
+ return @"description";
+}
+
+@end
+
+namespace firebase {
+namespace firestore {
+namespace util {
+
+TEST(StringFormatTest, NSString) {
+ EXPECT_EQ("Hello World", StringFormat("Hello %s", @"World"));
+
+ NSString* hello = [NSString stringWithUTF8String:"Hello"];
+ EXPECT_EQ("Hello World", StringFormat("%s World", hello));
+
+ // NOLINTNEXTLINE false positive on "string"
+ NSMutableString* world = [NSMutableString string];
+ [world appendString:@"World"];
+ EXPECT_EQ("Hello World", StringFormat("Hello %s", world));
+}
+
+TEST(StringFormatTest, FSTDescribable) {
+ FSTDescribable* desc = [[FSTDescribable alloc] init];
+ EXPECT_EQ("Hello description", StringFormat("Hello %s", desc));
+
+ id desc_id = desc;
+ EXPECT_EQ("Hello description", StringFormat("Hello %s", desc_id));
+}
+
+} // namespace util
+} // namespace firestore
+} // namespace firebase
diff --git a/Firestore/core/test/firebase/firestore/util/type_traits_apple_test.mm b/Firestore/core/test/firebase/firestore/util/type_traits_apple_test.mm
new file mode 100644
index 0000000..dfb03bb
--- /dev/null
+++ b/Firestore/core/test/firebase/firestore/util/type_traits_apple_test.mm
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 Google
+ *
+ * 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.
+ */
+
+#include "Firestore/core/src/firebase/firestore/util/type_traits.h"
+
+#import <Foundation/NSArray.h>
+#import <Foundation/NSObject.h>
+#import <Foundation/NSString.h>
+
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+namespace util {
+
+TEST(TypeTraitsTest, IsObjectiveCPointer) {
+ static_assert(is_objective_c_pointer<NSObject*>{}, "NSObject");
+ static_assert(is_objective_c_pointer<NSString*>{}, "NSString");
+ static_assert(is_objective_c_pointer<NSArray<NSString*>*>{},
+ "NSArray<NSString*>");
+
+ static_assert(is_objective_c_pointer<id>{}, "id");
+ static_assert(is_objective_c_pointer<id<NSCopying>>{}, "id<NSCopying>");
+
+ static_assert(!is_objective_c_pointer<int*>{}, "int*");
+ static_assert(!is_objective_c_pointer<void*>{}, "void*");
+ static_assert(!is_objective_c_pointer<int>{}, "int");
+ static_assert(!is_objective_c_pointer<void>{}, "void");
+
+ struct Foo {};
+ static_assert(!is_objective_c_pointer<Foo>{}, "Foo");
+ static_assert(!is_objective_c_pointer<Foo*>{}, "Foo");
+}
+
+} // namespace util
+} // namespace firestore
+} // namespace firebase