blob: 6067162dc033ee59630af351ea77b00b3bc45957 [file] [log] [blame]
// Copyright (C) 2019 Google LLC
//
// 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 "icing/util/document-validator.h"
#include <cstdint>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "icing/document-builder.h"
#include "icing/file/filesystem.h"
#include "icing/proto/schema.pb.h"
#include "icing/schema/schema-store.h"
#include "icing/testing/common-matchers.h"
#include "icing/testing/fake-clock.h"
#include "icing/testing/tmp-directory.h"
namespace icing {
namespace lib {
namespace {
using ::testing::HasSubstr;
// type and property names of EmailMessage
constexpr char kTypeEmail[] = "EmailMessage";
constexpr char kPropertySubject[] = "subject";
constexpr char kPropertyText[] = "text";
constexpr char kPropertyRecipients[] = "recipients";
// type and property names of Conversation
constexpr char kTypeConversation[] = "Conversation";
constexpr char kPropertyName[] = "name";
constexpr char kPropertyEmails[] = "emails";
// Other values
constexpr char kDefaultNamespace[] = "icing";
constexpr char kDefaultString[] = "This is a string.";
class DocumentValidatorTest : public ::testing::Test {
protected:
DocumentValidatorTest() {}
void SetUp() override {
SchemaProto schema;
auto type_config = schema.add_types();
CreateEmailTypeConfig(type_config);
type_config = schema.add_types();
CreateConversationTypeConfig(type_config);
ICING_ASSERT_OK_AND_ASSIGN(
schema_store_,
SchemaStore::Create(&filesystem_, GetTestTempDir(), &fake_clock_));
ASSERT_THAT(schema_store_->SetSchema(schema), IsOk());
document_validator_ =
std::make_unique<DocumentValidator>(schema_store_.get());
}
static void CreateEmailTypeConfig(SchemaTypeConfigProto* type_config) {
type_config->set_schema_type(kTypeEmail);
auto subject = type_config->add_properties();
subject->set_property_name(kPropertySubject);
subject->set_data_type(PropertyConfigProto::DataType::STRING);
subject->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
auto text = type_config->add_properties();
text->set_property_name(kPropertyText);
text->set_data_type(PropertyConfigProto::DataType::STRING);
text->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
auto recipients = type_config->add_properties();
recipients->set_property_name(kPropertyRecipients);
recipients->set_data_type(PropertyConfigProto::DataType::STRING);
recipients->set_cardinality(PropertyConfigProto::Cardinality::REPEATED);
}
static DocumentBuilder SimpleEmailBuilder() {
return DocumentBuilder()
.SetKey(kDefaultNamespace, "email/1")
.SetSchema(kTypeEmail)
.AddStringProperty(kPropertySubject, kDefaultString)
.AddStringProperty(kPropertyText, kDefaultString)
.AddStringProperty(kPropertyRecipients, kDefaultString, kDefaultString,
kDefaultString);
}
static void CreateConversationTypeConfig(SchemaTypeConfigProto* type_config) {
type_config->set_schema_type(kTypeConversation);
auto name = type_config->add_properties();
name->set_property_name(kPropertyName);
name->set_data_type(PropertyConfigProto::DataType::STRING);
name->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
auto emails = type_config->add_properties();
emails->set_property_name(kPropertyEmails);
emails->set_data_type(PropertyConfigProto::DataType::DOCUMENT);
emails->set_cardinality(PropertyConfigProto::Cardinality::REPEATED);
emails->set_schema_type(kTypeEmail);
}
static DocumentBuilder SimpleConversationBuilder() {
return DocumentBuilder()
.SetKey(kDefaultNamespace, "conversation/1")
.SetSchema(kTypeConversation)
.AddStringProperty(kPropertyName, kDefaultString)
.AddDocumentProperty(kPropertyEmails, SimpleEmailBuilder().Build(),
SimpleEmailBuilder().Build(),
SimpleEmailBuilder().Build());
}
std::unique_ptr<DocumentValidator> document_validator_;
std::unique_ptr<SchemaStore> schema_store_;
Filesystem filesystem_;
FakeClock fake_clock_;
};
TEST_F(DocumentValidatorTest, ValidateSimpleSchemasOk) {
DocumentProto email = SimpleEmailBuilder().Build();
EXPECT_THAT(document_validator_->Validate(email), IsOk());
DocumentProto conversation = SimpleConversationBuilder().Build();
EXPECT_THAT(document_validator_->Validate(conversation), IsOk());
}
TEST_F(DocumentValidatorTest, ValidateEmptyNamespaceInvalid) {
DocumentProto email = SimpleEmailBuilder().SetNamespace("").Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'namespace' is empty")));
}
TEST_F(DocumentValidatorTest, ValidateEmptyUriInvalid) {
DocumentProto email = SimpleEmailBuilder().SetUri("").Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'uri' is empty")));
}
TEST_F(DocumentValidatorTest, ValidateEmptySchemaInvalid) {
DocumentProto email = SimpleEmailBuilder().SetSchema("").Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'schema' is empty")));
}
TEST_F(DocumentValidatorTest, ValidateNonexistentSchemaNotFound) {
DocumentProto email =
SimpleEmailBuilder().SetSchema("WrongEmailType").Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::NOT_FOUND,
HasSubstr("'WrongEmailType' not found")));
}
TEST_F(DocumentValidatorTest, ValidateEmptyPropertyInvalid) {
DocumentProto email =
SimpleEmailBuilder().AddStringProperty("", kDefaultString).Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'name' is empty")));
}
TEST_F(DocumentValidatorTest, ValidateDuplicatePropertyAlreadyExists) {
DocumentProto email = SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject, kDefaultString)
.AddStringProperty(kPropertySubject, kDefaultString)
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::ALREADY_EXISTS,
HasSubstr("'subject' already exists")));
}
TEST_F(DocumentValidatorTest, ValidateNonexistentPropertyNotFound) {
DocumentProto email =
SimpleEmailBuilder()
.AddStringProperty("WrongPropertyName", kDefaultString)
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::NOT_FOUND,
HasSubstr("'WrongPropertyName' not found")));
}
TEST_F(DocumentValidatorTest, ValidateAllCustomPropertyOk) {
DocumentProto email =
SimpleEmailBuilder()
// A nonexistent property, would've triggered a NotFound message
.AddCustomStringProperty("WrongPropertyName", kDefaultString)
// 'subject' property should've been a string according to the schema
.AddCustomBooleanProperty(kPropertySubject, false, true)
.Build();
EXPECT_THAT(document_validator_->Validate(email), IsOk());
}
TEST_F(DocumentValidatorTest, ValidateExactlyOneRequiredValueOk) {
// Required property should have exactly 1 value
DocumentProto email =
SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject, kDefaultString) // 1 value
.Build();
EXPECT_THAT(document_validator_->Validate(email), IsOk());
}
TEST_F(DocumentValidatorTest, ValidateInvalidNumberOfRequiredValues) {
// Required property should have exactly 1 value
DocumentProto email = SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject) // 0 values
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'subject' with only 1 value is required "
"but 0 elements are found")));
email =
SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject, kDefaultString, kDefaultString)
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'subject' with only 1 value is required "
"but 2 elements are found")));
}
TEST_F(DocumentValidatorTest, ValidateZeroOrOneOptionalValueOk) {
DocumentProto email = SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject, kDefaultString)
.AddStringProperty(kPropertyText) // 0 values
.Build();
EXPECT_THAT(document_validator_->Validate(email), IsOk());
email = SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject, kDefaultString)
.AddStringProperty(kPropertyText, kDefaultString) // 1 value
.Build();
EXPECT_THAT(document_validator_->Validate(email), IsOk());
}
TEST_F(DocumentValidatorTest, ValidateInvalidNumberOfOptionalValues) {
DocumentProto email =
SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertySubject, kDefaultString)
.AddStringProperty(kPropertyText, kDefaultString, kDefaultString)
.Build();
EXPECT_THAT(
document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'text' is optional but 2 elements are found")));
}
TEST_F(DocumentValidatorTest, ValidateMissingRequiredPropertyInvalid) {
// All required properties should be present in document
DocumentProto email = SimpleEmailBuilder()
.ClearProperties()
.AddStringProperty(kPropertyText, kDefaultString)
.Build();
// The required property 'subject' isn't added in email.
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("One or more required fields missing")));
}
TEST_F(DocumentValidatorTest,
ValidateNestedPropertyDoesntMatchSchemaTypeInvalid) {
// Nested DocumentProto should have the expected schema type
DocumentProto conversation =
SimpleConversationBuilder()
.ClearProperties()
.AddStringProperty(kPropertyName, kDefaultString)
.AddDocumentProperty(
kPropertyEmails, SimpleEmailBuilder().Build(),
SimpleConversationBuilder().Build(), // Wrong document type
SimpleEmailBuilder().Build())
.Build();
EXPECT_THAT(document_validator_->Validate(conversation),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'emails' should have type 'EmailMessage' but "
"actual value has type 'Conversation'")));
}
TEST_F(DocumentValidatorTest, ValidateNestedPropertyInvalid) {
// Issues in nested DocumentProto should be detected
DocumentProto conversation =
SimpleConversationBuilder()
.ClearProperties()
.AddStringProperty(kPropertyName, kDefaultString)
.AddDocumentProperty(kPropertyEmails,
SimpleEmailBuilder()
.SetNamespace("")
.Build()) // Bad nested document
.Build();
EXPECT_THAT(document_validator_->Validate(conversation),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("'namespace' is empty")));
}
TEST_F(DocumentValidatorTest, HandleTypeConfigMapChangesOk) {
SchemaProto email_schema;
auto type_config = email_schema.add_types();
CreateEmailTypeConfig(type_config);
// Create a custom directory so we don't collide with the test's preset schema
// in SetUp
const std::string custom_schema_dir = GetTestTempDir() + "/custom_schema";
filesystem_.DeleteDirectoryRecursively(custom_schema_dir.c_str());
filesystem_.CreateDirectoryRecursively(custom_schema_dir.c_str());
// Set a schema with only the 'Email' type
ICING_ASSERT_OK_AND_ASSIGN(
std::unique_ptr<SchemaStore> schema_store,
SchemaStore::Create(&filesystem_, custom_schema_dir, &fake_clock_));
ASSERT_THAT(schema_store->SetSchema(email_schema), IsOk());
DocumentValidator document_validator(schema_store.get());
DocumentProto conversation = SimpleConversationBuilder().Build();
// Schema doesn't know about the 'Conversation' type yet
EXPECT_THAT(document_validator.Validate(conversation),
StatusIs(libtextclassifier3::StatusCode::NOT_FOUND,
HasSubstr("'Conversation' not found")));
// Add the 'Conversation' type
SchemaProto email_and_conversation_schema = email_schema;
type_config = email_and_conversation_schema.add_types();
CreateConversationTypeConfig(type_config);
// DocumentValidator should be able to handle the SchemaStore getting updated
// separately
ASSERT_THAT(schema_store->SetSchema(email_and_conversation_schema), IsOk());
ICING_EXPECT_OK(document_validator.Validate(conversation));
}
TEST_F(DocumentValidatorTest, PositiveDocumentScoreOk) {
DocumentProto email = SimpleEmailBuilder().SetScore(1).Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
email = SimpleEmailBuilder()
.SetScore(std::numeric_limits<int32_t>::max())
.Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
}
TEST_F(DocumentValidatorTest, NegativeDocumentScoreInvalid) {
DocumentProto email = SimpleEmailBuilder().SetScore(-1).Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("is negative")));
email = SimpleEmailBuilder()
.SetScore(std::numeric_limits<int32_t>::min())
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("is negative")));
}
TEST_F(DocumentValidatorTest, PositiveDocumentCreationTimestampMsOk) {
DocumentProto email = SimpleEmailBuilder().SetCreationTimestampMs(1).Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
email = SimpleEmailBuilder()
.SetCreationTimestampMs(std::numeric_limits<int32_t>::max())
.Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
}
TEST_F(DocumentValidatorTest, ZeroDocumentCreationTimestampMsOk) {
DocumentProto email = SimpleEmailBuilder().SetCreationTimestampMs(0).Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
}
TEST_F(DocumentValidatorTest, NegativeDocumentCreationTimestampMsInvalid) {
DocumentProto email = SimpleEmailBuilder().SetCreationTimestampMs(-1).Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("is negative")));
email = SimpleEmailBuilder()
.SetCreationTimestampMs(std::numeric_limits<int32_t>::min())
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("is negative")));
}
TEST_F(DocumentValidatorTest, PositiveDocumentTtlMsOk) {
DocumentProto email = SimpleEmailBuilder().SetTtlMs(1).Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
email = SimpleEmailBuilder()
.SetTtlMs(std::numeric_limits<int32_t>::max())
.Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
}
TEST_F(DocumentValidatorTest, ZeroDocumentTtlMsOk) {
DocumentProto email = SimpleEmailBuilder().SetTtlMs(0).Build();
ICING_EXPECT_OK(document_validator_->Validate(email));
}
TEST_F(DocumentValidatorTest, NegativeDocumentTtlMsInvalid) {
DocumentProto email = SimpleEmailBuilder().SetTtlMs(-1).Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("is negative")));
email = SimpleEmailBuilder()
.SetTtlMs(std::numeric_limits<int32_t>::min())
.Build();
EXPECT_THAT(document_validator_->Validate(email),
StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
HasSubstr("is negative")));
}
} // namespace
} // namespace lib
} // namespace icing