| // Copyright 2026 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 |
| // |
| // https://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 "utils/dual_status_tracker.h" |
| |
| #include <string> |
| |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/strings/str_cat.h" |
| #include "constants/status_codes.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "proto/assertion.pb.h" |
| #include "proto/ingredient_assertion.pb.h" |
| #include "proto/manifest.pb.h" |
| #include "proto/validation_result.pb.h" |
| #include "proto/validation_status.pb.h" |
| #include "testing/proto_test_utils.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::StatusIs; |
| using ::credentio_testing::EqualsProto; |
| using ::credentio_testing::ParseTextProtoOrDie; |
| using ::testing::ElementsAre; |
| using ::testing::IsEmpty; |
| using ::testing::TestParamInfo; |
| using ::testing::TestWithParam; |
| |
| struct TrackerTestCase { |
| std::string test_name; |
| credentio::SuccessStatusCode success_code; |
| credentio::FailureStatusCode failure_code; |
| std::string expected_success_codes; |
| std::string expected_failure_codes; |
| }; |
| using TrackerTest = TestWithParam<TrackerTestCase>; |
| |
| absl::flat_hash_set<std::string> GetFailures(ValidationStatusSet validation) { |
| absl::flat_hash_set<std::string> failures; |
| for (const auto& failure : validation.failures()) { |
| failures.insert(std::string(failure.code())); |
| } |
| return failures; |
| } |
| |
| absl::flat_hash_set<std::string> GetSuccesses(ValidationStatusSet validation) { |
| absl::flat_hash_set<std::string> successes; |
| for (const auto& success : validation.successes()) { |
| successes.insert(std::string(success.code())); |
| } |
| return successes; |
| } |
| |
| TEST(DualStatusTrackerTest, ManifestNotFound) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| hard_binding_uri: "self#jumbf=/c2pa/1234/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| EXPECT_THAT(DualStatusTracker::FromPartialValidationResult(&result), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(DualStatusTrackerTest, NoHardBindingUri) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| )pb"); |
| EXPECT_THAT(DualStatusTracker::FromPartialValidationResult(&result), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(DualStatusTrackerTest, WritesStatusCodesToSingleTracker) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| hard_binding_uri: "self#jumbf=/c2pa/a/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| const auto& active = result.active_manifest(); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordSuccess(SuccessStatusCode::kAssertionBmffHashMatch, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre("assertion.bmffHash.match")); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordFailure(FailureStatusCode::kAssertionBmffHashMismatch, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre("assertion.bmffHash.match")); |
| EXPECT_THAT(GetFailures(active.validation()), |
| ElementsAre("assertion.bmffHash.mismatch")); |
| } |
| |
| TEST(DualStatusTrackerTest, WritesDifferentSuccessCode) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| ingredient_manifests { label: "i" } |
| hard_binding_uri: "self#jumbf=/c2pa/i/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| |
| const auto& active = result.active_manifest(); |
| const auto& ingredient = result.ingredient_manifests(0); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), IsEmpty()); |
| |
| (*tracker)->RecordSuccess(SuccessStatusCode::kAssertionBmffHashMatch, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre("com.google.assertion.bmffHash.match")); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), |
| ElementsAre("assertion.bmffHash.match")); |
| EXPECT_THAT(GetFailures(ingredient.validation()), IsEmpty()); |
| } |
| |
| TEST(DualStatusTrackerTest, WritesSameFailureCode) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| ingredient_manifests { label: "i" } |
| hard_binding_uri: "self#jumbf=/c2pa/i/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| |
| const auto& active = result.active_manifest(); |
| const auto& ingredient = result.ingredient_manifests(0); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), IsEmpty()); |
| |
| (*tracker)->RecordFailure(FailureStatusCode::kClaimCborInvalid, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), |
| ElementsAre("claim.cbor.invalid")); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), |
| ElementsAre("claim.cbor.invalid")); |
| } |
| |
| TEST(DualStatusTrackerTest, WritesDifferentFailureCode) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| ingredient_manifests { label: "i" } |
| hard_binding_uri: "self#jumbf=/c2pa/i/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| |
| const auto& active = result.active_manifest(); |
| const auto& ingredient = result.ingredient_manifests(0); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), IsEmpty()); |
| |
| (*tracker)->RecordFailure(FailureStatusCode::kAssertionBmffHashMismatch, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), |
| ElementsAre("com.google.assertion.bmffHash.mismatch")); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), |
| ElementsAre("assertion.bmffHash.mismatch")); |
| } |
| |
| TEST(DualStatusTrackerTest, StatusUrls) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| ingredient_manifests { label: "i" } |
| hard_binding_uri: "self#jumbf=/c2pa/i/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| |
| const auto& active = result.active_manifest(); |
| const auto& ingredient = result.ingredient_manifests(0); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), IsEmpty()); |
| |
| (*tracker)->RecordFailure(FailureStatusCode::kAssertionBmffHashMismatch, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), |
| ElementsAre("com.google.assertion.bmffHash.mismatch")); |
| EXPECT_THAT(GetSuccesses(ingredient.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(ingredient.validation()), |
| ElementsAre("assertion.bmffHash.mismatch")); |
| |
| EXPECT_THAT(active.validation(), |
| EqualsProto(ParseTextProtoOrDie<ValidationStatusSet>( |
| R"pb( |
| failures { |
| code: "com.google.assertion.bmffHash.mismatch" |
| url: "self#jumbf=/c2pa/a" |
| } |
| )pb"))); |
| EXPECT_THAT(ingredient.validation(), |
| EqualsProto(ParseTextProtoOrDie<ValidationStatusSet>( |
| R"pb( |
| failures { |
| code: "assertion.bmffHash.mismatch" |
| url: "self#jumbf=/c2pa/i" |
| } |
| )pb"))); |
| } |
| |
| TEST(DualStatusTrackerTest, WritesUnexpectedSuccessCode) { |
| auto result = ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( |
| active_manifest { label: "a" } |
| hard_binding_uri: "self#jumbf=/c2pa/a/c2pa.assertions/c2pa.hash.boxes" |
| )pb"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| const auto& active = result.active_manifest(); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordSuccess(SuccessStatusCode::kClaimSignatureValidated, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre("claimSignature.validated")); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| } |
| |
| // Parametrized tests for success and failure codes. |
| TEST_P(TrackerTest, WritesStatusCodesToSingleTrackerAssertionsInActive) { |
| const TrackerTestCase& test_case = GetParam(); |
| |
| PartialValidationResultProto result; |
| result.mutable_active_manifest()->set_label("active_manifest"); |
| result.mutable_active_manifest()->add_assertions()->set_label("assertion1"); |
| result.set_hard_binding_uri( |
| "self#jumbf=/c2pa/active_manifest/c2pa.assertions/assertion1"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| const auto& active = result.active_manifest(); |
| |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordSuccess(test_case.success_code, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre(test_case.expected_success_codes)); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordFailure(test_case.failure_code, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre(test_case.expected_success_codes)); |
| EXPECT_THAT(GetFailures(active.validation()), |
| ElementsAre(test_case.expected_failure_codes)); |
| } |
| |
| TEST_P(TrackerTest, WritesStatusCodesToSingleTrackerAssertionsInIngredient) { |
| const TrackerTestCase& test_case = GetParam(); |
| |
| PartialValidationResultProto result; |
| result.mutable_active_manifest()->set_label("active_manifest"); |
| result.add_ingredient_manifests()->set_label("ingredient_manifest"); |
| result.mutable_ingredient_manifests(0)->add_assertions()->set_label( |
| "assertion1"); |
| result.set_hard_binding_uri( |
| "self#jumbf=/c2pa/ingredient_manifest/c2pa.assertions/assertion1"); |
| |
| auto tracker = DualStatusTracker::FromPartialValidationResult(&result); |
| ASSERT_TRUE(tracker.ok()); |
| const auto& active = result.active_manifest(); |
| |
| // It is expected to have "com.google." prefixes here. |
| std::string expected_success_codes = |
| absl::StrCat("com.google.", test_case.expected_success_codes); |
| std::string expected_failure_codes = |
| absl::StrCat("com.google.", test_case.expected_failure_codes); |
| EXPECT_THAT(GetSuccesses(active.validation()), IsEmpty()); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordSuccess(test_case.success_code, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre(expected_success_codes)); |
| EXPECT_THAT(GetFailures(active.validation()), IsEmpty()); |
| |
| (*tracker)->RecordFailure(test_case.failure_code, {}); |
| EXPECT_THAT(GetSuccesses(active.validation()), |
| ElementsAre(expected_success_codes)); |
| EXPECT_THAT(GetFailures(active.validation()), |
| ElementsAre(expected_failure_codes)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| TrackerTest, TrackerTest, |
| testing::ValuesIn<TrackerTestCase>({ |
| {"BmffWithMismatch", SuccessStatusCode::kAssertionBmffHashMatch, |
| FailureStatusCode::kAssertionBmffHashMismatch, |
| "assertion.bmffHash.match", "assertion.bmffHash.mismatch"}, |
| {"BmffWithMalformed", SuccessStatusCode::kAssertionBmffHashMatch, |
| FailureStatusCode::kAssertionBmffHashMalformed, |
| "assertion.bmffHash.match", "assertion.bmffHash.malformed"}, |
| {"BoxesWithMismatch", SuccessStatusCode::kAssertionBoxesHashMatch, |
| FailureStatusCode::kAssertionBoxesHashMismatch, |
| "assertion.boxesHash.match", "assertion.boxesHash.mismatch"}, |
| {"BoxesWithUnknown", SuccessStatusCode::kAssertionBoxesHashMatch, |
| FailureStatusCode::kAssertionBoxesHashUnknownBox, |
| "assertion.boxesHash.match", "assertion.boxesHash.unknownBox"}, |
| {"BoxesWithMalformed", SuccessStatusCode::kAssertionBoxesHashMatch, |
| FailureStatusCode::kAssertionBoxesHashMalformed, |
| "assertion.boxesHash.match", "assertion.boxesHash.malformed"}, |
| {"DataWithMismatch", SuccessStatusCode::kAssertionDataHashMatch, |
| FailureStatusCode::kAssertionDataHashMismatch, |
| "assertion.dataHash.match", "assertion.dataHash.mismatch"}, |
| {"DataWithMalformed", SuccessStatusCode::kAssertionDataHashMatch, |
| FailureStatusCode::kAssertionDataHashMalformed, |
| "assertion.dataHash.match", "assertion.dataHash.malformed"}, |
| {"MultiAssetWithMalformed", |
| SuccessStatusCode::kAssertionMultiAssetHashMatch, |
| FailureStatusCode::kAssertionMultiAssetHashMalformed, |
| "assertion.multiAssetHash.match", |
| "assertion.multiAssetHash.malformed"}, |
| {"MultiAssetWithMismatch", |
| SuccessStatusCode::kAssertionMultiAssetHashMatch, |
| FailureStatusCode::kAssertionMultiAssetHashMismatch, |
| "assertion.multiAssetHash.match", "assertion.multiAssetHash.mismatch"}, |
| {"MultiAssetWithMissingPart", |
| SuccessStatusCode::kAssertionMultiAssetHashMatch, |
| FailureStatusCode::kAssertionMultiAssetHashMissingPart, |
| "assertion.multiAssetHash.match", |
| "assertion.multiAssetHash.missingPart"}, |
| }), |
| [](const TestParamInfo<TrackerTest::ParamType>& info) { |
| return info.param.test_name; |
| }); |
| |
| } // namespace |
| } // namespace credentio |