| // 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 |