206 lines
9.9 KiB
C++
206 lines
9.9 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "authority/Authority.h"
|
|
#include "common/Snapshot.h"
|
|
#include "definitions/Definitions.h"
|
|
|
|
#include <cstdint>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
using eve::ContentId;
|
|
using eve::DiagnosticCode;
|
|
using eve::LogicalId;
|
|
using eve::PersistentId;
|
|
using eve::Result;
|
|
using eve::SchemaVersion;
|
|
using eve::SimulationTick;
|
|
using eve::SnapshotEnvelope;
|
|
using eve::SnapshotHashProvider;
|
|
using eve::SnapshotMigrationChain;
|
|
using eve::Value;
|
|
|
|
namespace {
|
|
|
|
PersistentId persistentId(std::string_view text) {
|
|
const auto parsed = PersistentId::parse(text);
|
|
return parsed ? *parsed : PersistentId::nil();
|
|
}
|
|
|
|
LogicalId logicalId(std::string_view text) {
|
|
const auto parsed = LogicalId::parse(text);
|
|
return parsed ? *parsed : LogicalId{};
|
|
}
|
|
|
|
/** Test-only deterministic non-cryptographic digest provider. */
|
|
SnapshotHashProvider testHashProvider() {
|
|
return [](std::string_view input) -> Result<ContentId> {
|
|
std::uint64_t left = 14695981039346656037ull;
|
|
std::uint64_t right = 1099511628211ull;
|
|
for (const unsigned char byte : input) {
|
|
left ^= byte;
|
|
left *= 1099511628211ull;
|
|
right ^= static_cast<std::uint64_t>(byte) + 0x9e3779b97f4a7c15ull;
|
|
right *= 14029467366897019727ull;
|
|
}
|
|
ContentId::Bytes bytes{};
|
|
for (int index = 0; index < 8; ++index) {
|
|
bytes[static_cast<std::size_t>(index)] = static_cast<std::uint8_t>(left >> (56 - index * 8));
|
|
bytes[static_cast<std::size_t>(index + 8)] = static_cast<std::uint8_t>(right >> (56 - index * 8));
|
|
}
|
|
return Result<ContentId>::success(ContentId(bytes));
|
|
};
|
|
}
|
|
|
|
SnapshotEnvelope makeEnvelope(std::string type, LogicalId schema, SchemaVersion version, PersistentId instanceId,
|
|
Value payload, const SnapshotHashProvider& hashProvider) {
|
|
auto result = eve::makeSnapshotEnvelope(std::move(type), std::move(schema), version, instanceId, eve::Revision(7),
|
|
SimulationTick(11), std::move(payload), hashProvider);
|
|
REQUIRE(result.ok());
|
|
return std::move(result).takeValue();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("snapshot.envelope.canonicalHashRoundTripAndNoWallClock") {
|
|
const auto hash = testHashProvider();
|
|
Value::Object payloadObject;
|
|
payloadObject.emplace("z", Value(std::int64_t(2)));
|
|
payloadObject.emplace("a", Value("stable"));
|
|
const auto envelope =
|
|
makeEnvelope("test.snapshot", logicalId("test:payload"), SchemaVersion(1),
|
|
persistentId("01234567-89ab-4cde-8fab-0123456789ab"), Value(std::move(payloadObject)), hash);
|
|
|
|
auto hashInput = eve::snapshotHashInput(envelope);
|
|
REQUIRE(hashInput.ok());
|
|
CHECK(hashInput.value().find("createdAt") == std::string::npos);
|
|
CHECK(hashInput.value().find("wallClock") == std::string::npos);
|
|
CHECK(hashInput.value().find("\"a\"") < hashInput.value().find("\"z\""));
|
|
|
|
auto serialized = eve::serializeSnapshotEnvelope(envelope);
|
|
REQUIRE(serialized.ok());
|
|
auto parsed = eve::parseSnapshotEnvelope(std::string_view(serialized.value()), hash);
|
|
REQUIRE(parsed.ok());
|
|
CHECK_EQ(parsed.value().type, envelope.type);
|
|
CHECK_EQ(parsed.value().schema, envelope.schema);
|
|
CHECK_EQ(parsed.value().schemaVersion, envelope.schemaVersion);
|
|
CHECK_EQ(parsed.value().instanceId, envelope.instanceId);
|
|
CHECK_EQ(parsed.value().revision, envelope.revision);
|
|
CHECK_EQ(parsed.value().tick, envelope.tick);
|
|
CHECK_EQ(parsed.value().contentHash, envelope.contentHash);
|
|
CHECK_EQ(parsed.value().payload, envelope.payload);
|
|
}
|
|
|
|
TEST_CASE("snapshot.migration.rejectsUnknownNewVersionAndHashMismatch") {
|
|
const auto hash = testHashProvider();
|
|
const auto schema = logicalId("test:payload");
|
|
SnapshotMigrationChain chain;
|
|
auto registration =
|
|
chain.add(schema, SchemaVersion(0), SchemaVersion(1), [](const Value& payload) -> Result<Value> {
|
|
const auto* object = payload.getIf<Value::Object>();
|
|
if (!object)
|
|
return Result<Value>::failure(
|
|
eve::Diagnostic::error(DiagnosticCode::ParseError, "payload must be an object"));
|
|
Value::Object migrated = *object;
|
|
migrated["version"] = Value(std::int64_t(1));
|
|
return Result<Value>::success(Value(std::move(migrated)));
|
|
});
|
|
REQUIRE(registration.ok());
|
|
|
|
Value::Object oldPayload;
|
|
oldPayload.emplace("version", Value(std::int64_t(0)));
|
|
auto old = makeEnvelope("test.snapshot", schema, SchemaVersion(0), PersistentId::nil(),
|
|
Value(std::move(oldPayload)), hash);
|
|
auto migrated = chain.migrate(std::move(old), SchemaVersion(1), hash);
|
|
REQUIRE(migrated.ok());
|
|
CHECK_EQ(migrated.value().schemaVersion, SchemaVersion(1));
|
|
CHECK_EQ(migrated.value().payload.getIf<Value::Object>()->at("version"), Value(std::int64_t(1)));
|
|
|
|
Value::Object newPayload;
|
|
newPayload.emplace("version", Value(std::int64_t(2)));
|
|
auto newer = makeEnvelope("test.snapshot", schema, SchemaVersion(2), PersistentId::nil(),
|
|
Value(std::move(newPayload)), hash);
|
|
auto rejected = chain.migrate(std::move(newer), SchemaVersion(1), hash);
|
|
CHECK(!rejected.ok());
|
|
CHECK_EQ(static_cast<int>(rejected.code()), static_cast<int>(eve::StatusCode::Failed));
|
|
CHECK_EQ(static_cast<int>(rejected.error()->code()), static_cast<int>(DiagnosticCode::UnknownVersion));
|
|
|
|
auto valid =
|
|
makeEnvelope("test.snapshot", schema, SchemaVersion(1), PersistentId::nil(), Value(Value::Object{}), hash);
|
|
valid.contentHash = ContentId::nil();
|
|
auto mismatch = eve::verifySnapshotEnvelope(valid, hash);
|
|
CHECK(!mismatch.ok());
|
|
CHECK_EQ(static_cast<int>(mismatch.error()->code()), static_cast<int>(DiagnosticCode::HashMismatch));
|
|
}
|
|
|
|
TEST_CASE("snapshot.consumers.authorityAndDefinitionsRoundTripAndMetadata") {
|
|
const auto hash = testHashProvider();
|
|
const auto authorityId = persistentId("11111111-2222-4333-8444-555555555555");
|
|
eve::authority::Store authority(authorityId);
|
|
const auto ruleId = authority.grant("actor", "scope", "read", "test", 3);
|
|
REQUIRE(!ruleId.empty());
|
|
auto authoritySnapshot = authority.snapshot(hash);
|
|
REQUIRE(authoritySnapshot.ok());
|
|
CHECK_EQ(authoritySnapshot.value().instanceId, authorityId);
|
|
CHECK_EQ(authoritySnapshot.value().revision, eve::Revision(1));
|
|
|
|
eve::authority::Store authorityCopy(authorityId);
|
|
auto authorityRestored = authorityCopy.restoreSnapshot(authoritySnapshot.value(), hash);
|
|
REQUIRE(authorityRestored.ok());
|
|
CHECK(authorityCopy.can("actor", "scope", "read"));
|
|
CHECK_EQ(authorityCopy.snapshot(hash).value().contentHash, authoritySnapshot.value().contentHash);
|
|
|
|
const auto definitionsId = persistentId("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
|
|
eve::definitions::DefinitionRegistry definitions(definitionsId);
|
|
REQUIRE(definitions.insert("unit", "tank", 1, "{\"speed\":4}").ok());
|
|
auto definitionsSnapshot = definitions.snapshot(hash);
|
|
REQUIRE(definitionsSnapshot.ok());
|
|
CHECK_EQ(definitionsSnapshot.value().instanceId, definitionsId);
|
|
|
|
eve::definitions::DefinitionRegistry definitionsCopy(definitionsId);
|
|
auto definitionsRestored = definitionsCopy.restoreSnapshot(definitionsSnapshot.value(), hash);
|
|
REQUIRE(definitionsRestored.ok());
|
|
auto tank = definitionsCopy.resolve("unit", "tank");
|
|
REQUIRE(tank.ok());
|
|
CHECK_EQ(tank.value().get().json, std::string("{\"speed\":4}"));
|
|
}
|
|
|
|
TEST_CASE("snapshot.consumers.malformedPayloadAndUnknownVersionLeaveStateUnchanged") {
|
|
const auto hash = testHashProvider();
|
|
const auto authorityId = persistentId("99999999-8888-4777-8666-555555555555");
|
|
eve::authority::Store authority(authorityId);
|
|
REQUIRE(!authority.grant("actor", "scope", "read", "before").empty());
|
|
const std::string authorityBefore = authority.snapshotJson();
|
|
|
|
Value::Object malformedAuthority;
|
|
malformedAuthority.emplace("version", Value(std::int64_t(1)));
|
|
auto malformedAuthorityEnvelope = makeEnvelope("authority.store", logicalId("authority:store"), SchemaVersion(1),
|
|
authorityId, Value(std::move(malformedAuthority)), hash);
|
|
auto authorityFailure = authority.restoreSnapshot(malformedAuthorityEnvelope, hash);
|
|
CHECK(!authorityFailure.ok());
|
|
CHECK_EQ(authority.snapshotJson(), authorityBefore);
|
|
|
|
Value::Object newerAuthority;
|
|
newerAuthority.emplace("version", Value(std::int64_t(2)));
|
|
auto newerAuthorityEnvelope = makeEnvelope("authority.store", logicalId("authority:store"), SchemaVersion(2),
|
|
authorityId, Value(std::move(newerAuthority)), hash);
|
|
auto unknownAuthority = authority.restoreSnapshot(newerAuthorityEnvelope, hash);
|
|
CHECK(!unknownAuthority.ok());
|
|
CHECK_EQ(static_cast<int>(unknownAuthority.error()->code()), static_cast<int>(DiagnosticCode::UnknownVersion));
|
|
CHECK_EQ(authority.snapshotJson(), authorityBefore);
|
|
|
|
const auto definitionsId = persistentId("12345678-1234-4123-8123-123456789012");
|
|
eve::definitions::DefinitionRegistry definitions(definitionsId);
|
|
REQUIRE(definitions.insert("unit", "before", 1, "{}").ok());
|
|
const std::string definitionsBefore = definitions.snapshotJson();
|
|
Value::Object malformedDefinitions;
|
|
malformedDefinitions.emplace("version", Value(std::int64_t(1)));
|
|
auto malformedDefinitionsEnvelope =
|
|
makeEnvelope("definitions.registry", logicalId("definitions:registry"), SchemaVersion(1), definitionsId,
|
|
Value(std::move(malformedDefinitions)), hash);
|
|
auto definitionsFailure = definitions.restoreSnapshot(malformedDefinitionsEnvelope, hash);
|
|
CHECK(!definitionsFailure.ok());
|
|
CHECK_EQ(definitions.snapshotJson(), definitionsBefore);
|
|
}
|