aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/core/test
diff options
context:
space:
mode:
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