blob: 0c8c308b72c58aaefb6d6fae1f0a998c3a7a4bfd [file]
// 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