use std::collections::{BTreeMap, BTreeSet};
use std::time::SystemTime;
use super::access::Directory;
use super::canonical::{Canonical, CanonicalError, CanonicalValue, Checksum, SerializerVersion};
use super::credentials::{CredentialError, Credentials};
use super::ids::{AuditEventId, MutationId, ResourceId, RevisionId, Slug};
use super::models::{ModelEnablementBody, ModelError, ModelValidationMode, Models};
use super::mutation::{AuditEvent, ExpectedRevision, Mutation};
use super::policy::{PolicyError, PolicySet};
use super::pricing::{PriceBooks, PricingError};
use super::providers::{ProviderError, Providers};
use super::resource::{BlobRef, ResourceKind, ResourceRef, ResourceScope, ResourceVersion};
use super::secrets::ForbiddenTransition;
use super::tenancy::{Tenancy, TenancyError};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ValidationError {
#[error("a revision must contain at least one resource")]
Empty,
#[error("{reference} appears twice in one revision")]
DuplicateResourceVersion { reference: ResourceRef },
#[error("{first} and {second} are two versions of one resource in one revision")]
MultipleVersions {
first: ResourceRef,
second: ResourceRef,
},
#[error("{proposed} does not advance on {previous}")]
VersionNotAdvanced {
previous: ResourceRef,
proposed: ResourceRef,
},
#[error("`{slug}` names both {first} and {second} in the same scope")]
DuplicateSlug {
slug: Slug,
first: ResourceRef,
second: ResourceRef,
},
#[error("{reference} cannot live at {scope:?}")]
ScopeMismatch {
reference: ResourceRef,
scope: ResourceScope,
},
#[error("{from} depends on {to}, which this revision does not contain")]
DanglingResourceReference { from: ResourceRef, to: ResourceRef },
#[error("{from} references blob {digest}, which this revision does not declare")]
DanglingBlobReference { from: ResourceRef, digest: Checksum },
#[error("blob {digest} is declared but referenced by no resource")]
UnreferencedBlob { digest: Checksum },
#[error(
"{catalog} still supplies the snapshot {digest} that {enablement} is pinned to; \
an enablement's snapshot is immutable, so publish the refreshed catalogue as its own \
resource and enable offerings against it"
)]
PinnedSnapshotWithdrawn {
catalog: ResourceRef,
enablement: ResourceRef,
digest: Checksum,
},
#[error("{from} depends on {to}, which belongs to another tenant")]
CrossTenantReference { from: ResourceRef, to: ResourceRef },
#[error("deployment-scoped {from} depends on tenant-scoped {to}")]
TenantScopedDependency { from: ResourceRef, to: ResourceRef },
#[error("this revision's tenancy is not valid: {0}")]
Tenancy(#[from] TenancyError),
#[error("this revision's provider credentials are not valid: {0}")]
Credential(#[from] CredentialError),
#[error("this revision moves a credential's material illegally: {0}")]
CredentialTransition(#[from] ForbiddenTransition),
#[error("this revision's policy is not valid: {0}")]
Policy(#[from] PolicyError),
#[error("this revision's provider connections are not valid: {0}")]
Provider(#[from] ProviderError),
#[error("this revision's model contracts are not valid: {0}")]
Model(Box<ModelError>),
#[error("this revision's pricing is not valid: {0}")]
Pricing(Box<PricingError>),
#[error("audit event {audit} records mutation {recorded}, not this candidate's {mutation}")]
AuditMutationMismatch {
audit: AuditEventId,
recorded: MutationId,
mutation: MutationId,
},
#[error("desired state has no canonical form: {0}")]
Canonical(#[from] CanonicalError),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DesiredState {
resources: BTreeMap<ResourceRef, ResourceVersion>,
blobs: BTreeMap<Checksum, BlobRef>,
}
impl DesiredState {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, resource: ResourceVersion) -> Result<&mut Self, ValidationError> {
if self.resources.contains_key(&resource.reference) {
return Err(ValidationError::DuplicateResourceVersion {
reference: resource.reference,
});
}
self.resources.insert(resource.reference, resource);
Ok(self)
}
pub fn declare_blob(&mut self, blob: BlobRef) -> &mut Self {
self.blobs.insert(blob.digest, blob);
self
}
pub fn retain_referenced_blobs(&mut self) -> &mut Self {
let mut referenced: BTreeSet<Checksum> = self
.resources
.values()
.filter_map(|resource| resource.body.blob())
.map(|blob| blob.digest)
.collect();
referenced.extend(
self.resources
.values()
.filter(|resource| resource.reference.kind == ResourceKind::ModelEnablement)
.filter_map(|resource| ModelEnablementBody::read(resource).ok())
.map(|body| body.offering().snapshot),
);
self.blobs.retain(|digest, _| referenced.contains(digest));
self
}
pub fn resources(&self) -> impl ExactSizeIterator<Item = &ResourceVersion> {
self.resources.values()
}
pub fn blobs(&self) -> impl ExactSizeIterator<Item = &BlobRef> {
self.blobs.values()
}
pub fn get(&self, reference: &ResourceRef) -> Option<&ResourceVersion> {
self.resources.get(reference)
}
pub fn len(&self) -> usize {
self.resources.len()
}
pub fn is_empty(&self) -> bool {
self.resources.is_empty()
}
pub fn validate(&self) -> Result<(), ValidationError> {
self.validate_with_model_mode(ModelValidationMode::Strict, None)
}
pub(crate) fn validate_legacy_read(&self) -> Result<(), ValidationError> {
self.validate_with_model_mode(ModelValidationMode::LegacyRead, None)
}
fn validate_with_model_mode(
&self,
model_validation: ModelValidationMode,
legacy_aliases: Option<&BTreeSet<ResourceRef>>,
) -> Result<(), ValidationError> {
if self.resources.is_empty() {
return Err(ValidationError::Empty);
}
let mut by_resource: BTreeMap<(_, _), ResourceRef> = BTreeMap::new();
let mut by_slug: BTreeMap<(&ResourceScope, _, &Slug), ResourceRef> = BTreeMap::new();
let mut referenced_blobs = BTreeSet::new();
for resource in self.resources.values() {
let reference = resource.reference;
if !reference.kind.permits(&resource.scope) {
return Err(ValidationError::ScopeMismatch {
reference,
scope: resource.scope.clone(),
});
}
if let Some(first) = by_resource.insert((reference.kind, reference.id), reference) {
return Err(ValidationError::MultipleVersions {
first,
second: reference,
});
}
if let Some(first) =
by_slug.insert((&resource.scope, reference.kind, &resource.slug), reference)
{
return Err(ValidationError::DuplicateSlug {
slug: resource.slug.clone(),
first,
second: reference,
});
}
if let Some(blob) = resource.body.blob() {
if !self.blobs.contains_key(&blob.digest) {
return Err(ValidationError::DanglingBlobReference {
from: reference,
digest: blob.digest,
});
}
referenced_blobs.insert(blob.digest);
}
}
for resource in self.resources.values() {
for dependency in &resource.depends_on {
let Some(target) = self.resources.get(dependency) else {
return Err(ValidationError::DanglingResourceReference {
from: resource.reference,
to: *dependency,
});
};
match (resource.scope.tenant(), target.scope.tenant()) {
(Some(from), Some(to)) if from != to => {
return Err(ValidationError::CrossTenantReference {
from: resource.reference,
to: *dependency,
});
}
(None, Some(_)) => {
return Err(ValidationError::TenantScopedDependency {
from: resource.reference,
to: *dependency,
});
}
_ => {}
}
}
}
if let Some(digest) = self
.blobs
.keys()
.find(|digest| !referenced_blobs.contains(*digest))
{
return Err(ValidationError::UnreferencedBlob { digest: *digest });
}
let tenancy = Tenancy::of(self)?;
Directory::of(self, &tenancy)?;
PriceBooks::of(self).map_err(|error| ValidationError::Pricing(Box::new(error)))?;
Providers::of(self)?;
Credentials::of(self)?;
PolicySet::of(self)?;
Models::of_with_mode(self, model_validation, legacy_aliases)?;
Ok(())
}
pub fn supersede(&mut self, resource: ResourceVersion) -> Result<&mut Self, ValidationError> {
let held: Vec<ResourceRef> = self
.resources
.keys()
.filter(|reference| {
reference.kind == resource.reference.kind && reference.id == resource.reference.id
})
.copied()
.collect();
if let Some(previous) = held
.iter()
.copied()
.max_by_key(|reference| reference.version)
&& previous.version >= resource.reference.version
{
return Err(ValidationError::VersionNotAdvanced {
previous,
proposed: resource.reference,
});
}
for previous in held {
self.resources.remove(&previous);
}
self.insert(resource)
}
pub fn version_of(&self, kind: ResourceKind, id: ResourceId) -> Option<&ResourceVersion> {
self.resources
.values()
.find(|resource| resource.reference.kind == kind && resource.reference.id == id)
}
pub fn checksum(&self) -> Result<Checksum, CanonicalError> {
Canonical::checksum(self)
}
}
impl Canonical for DesiredState {
fn canonical(&self) -> CanonicalValue {
CanonicalValue::map([
(
"resources",
CanonicalValue::set(self.resources.values().map(Canonical::canonical)),
),
(
"blobs",
CanonicalValue::set(self.blobs.values().map(Canonical::canonical)),
),
])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RevisionCandidate {
pub expected: ExpectedRevision,
pub state: DesiredState,
pub legacy_aliases: BTreeSet<ResourceRef>,
pub mutation: Mutation,
pub audit: AuditEvent,
}
impl RevisionCandidate {
pub fn validated_checksum(&self) -> Result<Checksum, ValidationError> {
self.validated_checksum_with(ModelValidationMode::Strict, None)
}
pub(crate) fn validated_checksum_legacy_read(&self) -> Result<Checksum, ValidationError> {
self.validated_checksum_with(ModelValidationMode::LegacyRead, None)
}
pub(crate) fn validated_checksum_for_publication(&self) -> Result<Checksum, ValidationError> {
if self.mutation.kind == super::mutation::MutationKind::Rollback {
self.validated_checksum_legacy_read()
} else {
self.validated_checksum_with(ModelValidationMode::Strict, Some(&self.legacy_aliases))
}
}
fn validated_checksum_with(
&self,
model_validation: ModelValidationMode,
legacy_aliases: Option<&BTreeSet<ResourceRef>>,
) -> Result<Checksum, ValidationError> {
if self.audit.mutation != self.mutation.id {
return Err(ValidationError::AuditMutationMismatch {
audit: self.audit.id,
recorded: self.audit.mutation,
mutation: self.mutation.id,
});
}
self.state
.validate_with_model_mode(model_validation, legacy_aliases)?;
Ok(self.state.checksum()?)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManifestEntry {
pub reference: ResourceRef,
pub scope: ResourceScope,
pub slug: Slug,
pub content: Checksum,
}
impl ManifestEntry {
fn of(resource: &ResourceVersion) -> Result<Self, CanonicalError> {
Ok(Self {
reference: resource.reference,
scope: resource.scope.clone(),
slug: resource.slug.clone(),
content: resource.content_checksum()?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RevisionManifest {
pub id: RevisionId,
pub parent: Option<RevisionId>,
pub created_at: SystemTime,
pub serializer: SerializerVersion,
pub mutation: MutationId,
pub entries: Vec<ManifestEntry>,
pub blobs: Vec<BlobRef>,
pub checksum: Checksum,
}
impl RevisionManifest {
pub fn of(
id: RevisionId,
parent: Option<RevisionId>,
created_at: SystemTime,
candidate: &RevisionCandidate,
) -> Result<Self, ValidationError> {
let checksum = candidate.validated_checksum_for_publication()?;
let mut entries = candidate
.state
.resources()
.map(ManifestEntry::of)
.collect::<Result<Vec<_>, _>>()?;
entries.sort_by_key(|entry| entry.reference);
let mut blobs: Vec<BlobRef> = candidate.state.blobs().copied().collect();
blobs.sort_by_key(|blob| blob.digest);
Ok(Self {
id,
parent,
created_at,
serializer: SerializerVersion::default(),
mutation: candidate.mutation.id,
entries,
blobs,
checksum,
})
}
pub fn references(&self) -> impl ExactSizeIterator<Item = ResourceRef> + '_ {
self.entries.iter().map(|entry| entry.reference)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BodySkew {
#[error(transparent)]
Tenancy(TenancyError),
#[error(transparent)]
Credential(CredentialError),
#[error(transparent)]
Policy(PolicyError),
#[error(transparent)]
Provider(ProviderError),
#[error(transparent)]
Model(Box<ModelError>),
#[error(transparent)]
Pricing(Box<PricingError>),
}
impl From<ModelError> for ValidationError {
fn from(error: ModelError) -> Self {
Self::Model(Box::new(error))
}
}
impl From<TenancyError> for BodySkew {
fn from(error: TenancyError) -> Self {
Self::Tenancy(error)
}
}
impl From<CredentialError> for BodySkew {
fn from(error: CredentialError) -> Self {
Self::Credential(error)
}
}
impl From<PolicyError> for BodySkew {
fn from(error: PolicyError) -> Self {
Self::Policy(error)
}
}
impl From<ProviderError> for BodySkew {
fn from(error: ProviderError) -> Self {
Self::Provider(error)
}
}
impl From<ModelError> for BodySkew {
fn from(error: ModelError) -> Self {
Self::Model(Box::new(error))
}
}
impl From<Box<PricingError>> for BodySkew {
fn from(error: Box<PricingError>) -> Self {
Self::Pricing(error)
}
}
impl BodySkew {
pub const fn reference(&self) -> ResourceRef {
match self {
Self::Tenancy(error) => error.reference(),
Self::Credential(error) => error.reference(),
Self::Policy(error) => error.reference(),
Self::Provider(error) => error.reference(),
Self::Model(error) => error.reference(),
Self::Pricing(error) => error.reference(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IntegrityError {
#[error(
"revision was written by serializer `{stored}`, but this build canonicalizes with `{current}`"
)]
Serializer {
stored: SerializerVersion,
current: SerializerVersion,
},
#[error(
"stored serializer `{stored}` is a canonical encoding this build does not know; \
it canonicalizes with `{current}`"
)]
UnknownSerializer {
stored: String,
current: SerializerVersion,
},
#[error("revision checksum is {expected}, but the loaded state hashes to {actual}")]
ChecksumMismatch {
expected: Checksum,
actual: Checksum,
},
#[error("manifest names {reference}, which the loaded state does not contain")]
MissingResource { reference: ResourceRef },
#[error("loaded state contains {reference}, which the manifest does not name")]
UnexpectedResource { reference: ResourceRef },
#[error("{reference} hashes to {actual}, but the manifest recorded {expected}")]
ContentMismatch {
reference: ResourceRef,
expected: Checksum,
actual: Checksum,
},
#[error(
"{reference} was stored as `{stored}` in {scope:?}, but the manifest recorded `{manifest_slug}`"
)]
EntryMismatch {
reference: ResourceRef,
stored: Slug,
manifest_slug: Slug,
scope: ResourceScope,
},
#[error("manifest declares blob {digest}, which the loaded state does not")]
MissingBlob { digest: Checksum },
#[error("loaded state declares blob {digest}, which the manifest does not")]
UnexpectedBlob { digest: Checksum },
#[error("stored revision is not valid desired state: {0}")]
Invalid(#[from] ValidationError),
#[error("stored revision is not compatible with this build: {0}")]
Incompatible(BodySkew),
#[error("stored revision is unreadable: {detail}")]
Unreadable { detail: String },
}
impl IntegrityError {
fn classify(error: ValidationError) -> Self {
match error {
ValidationError::Tenancy(tenancy) if tenancy.is_incompatible() => {
Self::Incompatible(BodySkew::Tenancy(tenancy))
}
ValidationError::Credential(credential) if credential.is_incompatible() => {
Self::Incompatible(BodySkew::Credential(credential))
}
ValidationError::Policy(policy) if policy.is_incompatible() => {
Self::Incompatible(BodySkew::Policy(policy))
}
ValidationError::Provider(provider) if provider.is_incompatible() => {
Self::Incompatible(BodySkew::Provider(provider))
}
ValidationError::Model(model) if model.is_incompatible() => {
Self::Incompatible(BodySkew::Model(model))
}
ValidationError::Pricing(pricing) if pricing.is_incompatible() => {
Self::Incompatible(BodySkew::Pricing(pricing))
}
other => Self::Invalid(other),
}
}
pub const fn is_incompatible(&self) -> bool {
matches!(
self,
Self::Incompatible(_) | Self::Serializer { .. } | Self::UnknownSerializer { .. }
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedRevision {
manifest: RevisionManifest,
state: DesiredState,
}
impl LoadedRevision {
pub fn assemble(
manifest: RevisionManifest,
state: DesiredState,
) -> Result<Self, IntegrityError> {
let current = SerializerVersion::default();
if manifest.serializer != current {
return Err(IntegrityError::Serializer {
stored: manifest.serializer,
current,
});
}
state
.validate_legacy_read()
.map_err(IntegrityError::classify)?;
for entry in &manifest.entries {
let Some(resource) = state.get(&entry.reference) else {
return Err(IntegrityError::MissingResource {
reference: entry.reference,
});
};
if resource.slug != entry.slug || resource.scope != entry.scope {
return Err(IntegrityError::EntryMismatch {
reference: entry.reference,
stored: resource.slug.clone(),
manifest_slug: entry.slug.clone(),
scope: resource.scope.clone(),
});
}
let actual = resource
.content_checksum()
.map_err(ValidationError::Canonical)?;
if actual != entry.content {
return Err(IntegrityError::ContentMismatch {
reference: entry.reference,
expected: entry.content,
actual,
});
}
}
let named: BTreeSet<ResourceRef> = manifest.references().collect();
if let Some(resource) = state
.resources()
.find(|resource| !named.contains(&resource.reference))
{
return Err(IntegrityError::UnexpectedResource {
reference: resource.reference,
});
}
let declared: BTreeSet<Checksum> = state.blobs().map(|blob| blob.digest).collect();
if let Some(blob) = manifest
.blobs
.iter()
.find(|blob| !declared.contains(&blob.digest))
{
return Err(IntegrityError::MissingBlob {
digest: blob.digest,
});
}
let manifested: BTreeSet<Checksum> =
manifest.blobs.iter().map(|blob| blob.digest).collect();
if let Some(blob) = state
.blobs()
.find(|blob| !manifested.contains(&blob.digest))
{
return Err(IntegrityError::UnexpectedBlob {
digest: blob.digest,
});
}
let actual = state.checksum().map_err(ValidationError::Canonical)?;
if actual != manifest.checksum {
return Err(IntegrityError::ChecksumMismatch {
expected: manifest.checksum,
actual,
});
}
Ok(Self { manifest, state })
}
pub fn manifest(&self) -> &RevisionManifest {
&self.manifest
}
pub fn state(&self) -> &DesiredState {
&self.state
}
pub fn id(&self) -> RevisionId {
self.manifest.id
}
pub fn into_state(self) -> DesiredState {
self.state
}
}
#[cfg(test)]
mod tests {
use super::super::fixtures::{
DESIRED_STATE_RESOURCES, alias, blob_backed_catalog, catalog_payload, credential, project,
reference, resource_id, revision_id, state, state_with_models, tenant, tenant_body,
tenant_id,
};
use super::super::ids::Uuid7;
use super::super::models::{
AliasTarget, ModelAliasBody, ModelEnablementBody, ModelLifecycle, legacy_alias_allowlist,
};
use super::super::mutation::MutationKind;
use super::super::resource::{
BlobKind, ResourceBody, ResourceKind, ResourceVersion, ResourceVersionNumber,
};
use super::*;
fn candidate(state: DesiredState) -> RevisionCandidate {
super::super::fixtures::candidate(ExpectedRevision::Empty, "publish-1", state)
}
fn manifest(candidate: &RevisionCandidate) -> RevisionManifest {
RevisionManifest::of(revision_id(1), None, SystemTime::UNIX_EPOCH, candidate)
.expect("a valid candidate")
}
#[test]
fn insertion_order_does_not_change_the_checksum() {
let forward = state();
let mut backward = DesiredState::new();
let mut resources: Vec<_> = forward.resources().cloned().collect();
resources.reverse();
for resource in resources {
backward.insert(resource).unwrap();
}
for blob in forward.blobs() {
backward.declare_blob(*blob);
}
assert_eq!(forward.checksum().unwrap(), backward.checksum().unwrap());
assert_eq!(forward, backward, "the state itself is order-independent");
assert_eq!(forward.len(), DESIRED_STATE_RESOURCES);
assert!(!forward.is_empty());
}
#[test]
fn a_refused_supersede_leaves_the_state_it_found() {
let second = ResourceVersionNumber::FIRST.next();
let third = second.next();
let name = Slug::parse("acme").unwrap();
let mut state = DesiredState::new();
state
.insert(tenant_body(1, "Acme").version_at(name.clone(), ResourceVersionNumber::FIRST))
.and_then(|state| state.insert(tenant_body(1, "Acme").version_at(name.clone(), third)))
.expect("two versions of one tenant are insertable, if not publishable");
let held = state.clone();
let refused = state
.supersede(tenant_body(1, "Renamed").version_at(name.clone(), second))
.expect_err("v2 does not advance past the v3 the state holds");
assert!(matches!(
refused,
ValidationError::VersionNotAdvanced { previous, .. } if previous.version == third
));
assert_eq!(
state, held,
"a refusal removed a version the caller still holds"
);
state
.supersede(tenant_body(1, "Renamed").version_at(name, third.next()))
.expect("v4 advances past every version held");
assert_eq!(
state.len(),
1,
"an accepted supersede left an older version behind"
);
}
#[test]
fn semantically_identical_states_have_identical_bytes() {
let one = state();
let other = state();
assert_eq!(
one.canonical().to_canonical_bytes().unwrap(),
other.canonical().to_canonical_bytes().unwrap()
);
assert_eq!(one.checksum().unwrap(), other.checksum().unwrap());
}
#[test]
fn any_semantic_change_changes_the_checksum() {
let base = state().checksum().unwrap();
let tenant = tenant_id(1);
let mut renamed = DesiredState::new();
for resource in state().resources() {
let mut resource = resource.clone();
if resource.reference.kind == ResourceKind::Alias {
resource.slug = Slug::parse("renamed").unwrap();
}
renamed.insert(resource).unwrap();
}
for blob in state().blobs() {
renamed.declare_blob(*blob);
}
assert_ne!(base, renamed.checksum().unwrap(), "a rename is a change");
let mut extra = state();
extra
.insert(alias(
&tenant,
7,
"spare",
&[reference(ResourceKind::ProviderCredential, 3)],
))
.unwrap();
assert_ne!(base, extra.checksum().unwrap(), "an addition is a change");
let mut reblobbed = DesiredState::new();
let payload = catalog_payload(b"other");
let replacement = BlobRef::of(BlobKind::CatalogSnapshot, &payload);
for resource in state().resources() {
let mut resource = resource.clone();
if resource.body.blob().is_some() {
resource.body = ResourceBody::Blob(replacement);
}
reblobbed.insert(resource).unwrap();
}
reblobbed.declare_blob(replacement);
assert_ne!(base, reblobbed.checksum().unwrap());
}
#[test]
fn a_blob_is_referenced_not_duplicated() {
let payload = catalog_payload(b"models");
let state = state();
let blob = *state.blobs().next().expect("a declared blob");
assert_eq!(blob.size_bytes, payload.len() as u64);
let bytes = state.canonical().to_canonical_bytes().unwrap();
assert!(
bytes.len() < payload.len(),
"{} canonical bytes must not carry the {}-byte payload",
bytes.len(),
payload.len()
);
blob.verify(&payload).expect("the payload it addresses");
}
#[test]
fn an_empty_state_is_not_a_revision() {
assert_eq!(DesiredState::new().validate(), Err(ValidationError::Empty));
}
#[test]
fn the_same_reference_cannot_be_inserted_twice() {
let mut state = DesiredState::new();
let tenant = tenant(1, "acme");
state.insert(tenant.clone()).unwrap();
assert_eq!(
state.insert(tenant.clone()),
Err(ValidationError::DuplicateResourceVersion {
reference: tenant.reference
})
);
}
#[test]
fn one_revision_pins_one_version_of_a_resource() {
let mut state = DesiredState::new();
let first = tenant(1, "acme");
let second = ResourceVersion {
reference: first.reference.at(ResourceVersionNumber::FIRST.next()),
slug: Slug::parse("acme-renamed").unwrap(),
..first.clone()
};
state.insert(first.clone()).unwrap();
state.insert(second.clone()).unwrap();
assert_eq!(
state.validate(),
Err(ValidationError::MultipleVersions {
first: first.reference,
second: second.reference
})
);
}
#[test]
fn slugs_are_unique_per_scope_and_kind_but_not_across_them() {
let tenant = tenant_id(1);
let mut clashing = DesiredState::new();
clashing.insert(self::tenant(1, "acme")).unwrap();
let first = alias(&tenant, 2, "fast", &[]);
let second = alias(&tenant, 3, "fast", &[]);
clashing.insert(first.clone()).unwrap();
clashing.insert(second.clone()).unwrap();
assert_eq!(
clashing.validate(),
Err(ValidationError::DuplicateSlug {
slug: Slug::parse("fast").unwrap(),
first: first.reference,
second: second.reference
})
);
let other = tenant_id(9);
let mut distinct = DesiredState::new();
distinct.insert(self::tenant(1, "acme")).unwrap();
distinct.insert(self::tenant(9, "globex")).unwrap();
distinct.insert(alias(&tenant, 2, "fast", &[])).unwrap();
distinct.insert(alias(&other, 3, "fast", &[])).unwrap();
distinct.insert(credential(&tenant, 4, "fast")).unwrap();
distinct.validate().expect("scoped slugs do not collide");
}
#[test]
fn a_kind_cannot_live_outside_its_scope() {
let mut state = DesiredState::new();
let misplaced = ResourceVersion::new(
reference(ResourceKind::Alias, 1),
ResourceScope::Deployment,
Slug::parse("fast").unwrap(),
ResourceBody::Inline(CanonicalValue::Bool(true)),
);
state.insert(misplaced.clone()).unwrap();
assert_eq!(
state.validate(),
Err(ValidationError::ScopeMismatch {
reference: misplaced.reference,
scope: ResourceScope::Deployment
})
);
}
#[test]
fn a_dangling_resource_reference_is_refused() {
let tenant = tenant_id(1);
let missing = reference(ResourceKind::ProviderCredential, 99);
let mut state = DesiredState::new();
state.insert(self::tenant(1, "acme")).unwrap();
let alias = alias(&tenant, 2, "fast", &[missing]);
state.insert(alias.clone()).unwrap();
assert_eq!(
state.validate(),
Err(ValidationError::DanglingResourceReference {
from: alias.reference,
to: missing
})
);
let credential = credential(&tenant, 3, "primary");
let mut versioned = DesiredState::new();
versioned.insert(self::tenant(1, "acme")).unwrap();
versioned.insert(credential.clone()).unwrap();
let stale = credential.reference.at(ResourceVersionNumber::FIRST.next());
let alias = self::alias(&tenant, 2, "fast", &[stale]);
versioned.insert(alias.clone()).unwrap();
assert_eq!(
versioned.validate(),
Err(ValidationError::DanglingResourceReference {
from: alias.reference,
to: stale
})
);
}
#[test]
fn a_cross_tenant_reference_is_refused_but_deployment_state_is_shared() {
let acme = tenant_id(1);
let globex = tenant_id(9);
let leaked = credential(&globex, 3, "primary");
let mut state = DesiredState::new();
state.insert(tenant(1, "acme")).unwrap();
state.insert(tenant(9, "globex")).unwrap();
state.insert(leaked.clone()).unwrap();
let alias = alias(&acme, 2, "fast", &[leaked.reference]);
state.insert(alias.clone()).unwrap();
assert_eq!(
state.validate(),
Err(ValidationError::CrossTenantReference {
from: alias.reference,
to: leaked.reference
})
);
let shared = blob_backed_catalog(5);
let mut allowed = DesiredState::new();
allowed.insert(tenant(1, "acme")).unwrap();
allowed.insert(shared.clone()).unwrap();
allowed.declare_blob(*shared.body.blob().unwrap());
allowed
.insert(super::super::fixtures::alias(
&acme,
2,
"fast",
&[shared.reference],
))
.unwrap();
allowed
.validate()
.expect("deployment-scoped state is referenceable from a tenant");
}
#[test]
fn deployment_scoped_state_may_not_depend_on_one_tenants_resource() {
let acme = tenant_id(1);
let credential = credential(&acme, 3, "primary");
let shared = tenant(9, "globex").depending_on([credential.reference]);
let mut state = DesiredState::new();
state.insert(tenant(1, "acme")).unwrap();
state.insert(credential.clone()).unwrap();
state.insert(shared.clone()).unwrap();
assert_eq!(
state.validate(),
Err(ValidationError::TenantScopedDependency {
from: shared.reference,
to: credential.reference
})
);
}
#[test]
fn project_scoped_state_may_reference_its_own_tenant() {
let tenant = tenant_id(1);
let project_id = super::super::ids::ProjectId::new(Uuid7::from_parts(2, 0, 2).unwrap());
let credential = credential(&tenant, 3, "primary");
let scoped = ResourceVersion::new(
reference(ResourceKind::Alias, 4),
ResourceScope::Project {
tenant,
project: project_id,
},
Slug::parse("fast").unwrap(),
ResourceBody::Inline(CanonicalValue::Bool(true)),
)
.depending_on([credential.reference]);
let mut state = DesiredState::new();
state.insert(self::tenant(1, "acme")).unwrap();
state.insert(project(&tenant, 2, "core")).unwrap();
state.insert(credential).unwrap();
state.insert(scoped).unwrap();
state
.validate()
.expect("a project shares its tenant's scope");
}
#[test]
fn blob_references_must_be_declared_and_declarations_must_be_used() {
let catalog = blob_backed_catalog(5);
let blob = *catalog.body.blob().unwrap();
let mut undeclared = DesiredState::new();
undeclared.insert(catalog.clone()).unwrap();
assert_eq!(
undeclared.validate(),
Err(ValidationError::DanglingBlobReference {
from: catalog.reference,
digest: blob.digest
})
);
let mut orphaned = DesiredState::new();
orphaned.insert(tenant(1, "acme")).unwrap();
orphaned.declare_blob(blob);
assert_eq!(
orphaned.validate(),
Err(ValidationError::UnreferencedBlob {
digest: blob.digest
})
);
let mut state = DesiredState::new();
state.insert(catalog).unwrap();
state.declare_blob(blob);
state.declare_blob(blob);
assert_eq!(state.blobs().len(), 1);
state.validate().expect("declared and referenced");
}
#[test]
fn a_manifest_records_references_not_payloads() {
let candidate = candidate(state());
let manifest = manifest(&candidate);
assert_eq!(manifest.entries.len(), DESIRED_STATE_RESOURCES);
assert_eq!(manifest.serializer, SerializerVersion::default());
assert_eq!(manifest.mutation, candidate.mutation.id);
assert_eq!(manifest.checksum, candidate.state.checksum().unwrap());
assert_eq!(manifest.parent, None);
let references: Vec<_> = manifest.references().collect();
let mut sorted = references.clone();
sorted.sort();
assert_eq!(references, sorted);
for entry in &manifest.entries {
let resource = candidate.state.get(&entry.reference).expect("named");
assert_eq!(entry.content, resource.content_checksum().unwrap());
assert_eq!(entry.slug, resource.slug);
}
assert_eq!(manifest.blobs.len(), 1);
}
#[test]
fn an_invalid_candidate_produces_no_manifest() {
let mut state = DesiredState::new();
let missing = reference(ResourceKind::ProviderCredential, 99);
state.insert(tenant(1, "acme")).unwrap();
state
.insert(alias(&tenant_id(1), 2, "fast", &[missing]))
.unwrap();
let error = RevisionManifest::of(
revision_id(1),
None,
SystemTime::UNIX_EPOCH,
&candidate(state),
)
.expect_err("a dangling reference must not be publishable");
assert!(matches!(
error,
ValidationError::DanglingResourceReference { .. }
));
}
#[test]
fn a_revision_round_trips_through_its_manifest() {
let candidate = candidate(state());
let manifest = manifest(&candidate);
let loaded = LoadedRevision::assemble(manifest.clone(), candidate.state.clone())
.expect("the state the manifest describes");
assert_eq!(loaded.id(), manifest.id);
assert_eq!(loaded.manifest(), &manifest);
assert_eq!(loaded.state(), &candidate.state);
assert_eq!(
loaded.clone().into_state().checksum().unwrap(),
manifest.checksum
);
let rebuilt = RevisionManifest::of(
manifest.id,
manifest.parent,
manifest.created_at,
&RevisionCandidate {
state: loaded.into_state(),
..candidate
},
)
.unwrap();
assert_eq!(rebuilt, manifest);
}
#[test]
fn a_legacy_enabled_alias_targeting_a_disabled_enablement_hydrates() {
let mut state = state_with_models();
let target = state
.version_of(ResourceKind::ModelEnablement, resource_id(31))
.cloned()
.expect("the project enablement");
let disabled = ModelEnablementBody::read(&target)
.expect("an enablement body")
.transitioned(ModelLifecycle::Disabled)
.version_at(
target.slug.clone(),
target.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
state
.supersede(disabled.clone())
.expect("disable the target");
let alias = state
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the project alias");
let legacy_alias = ModelAliasBody::read(&alias)
.expect("an alias body")
.retargeted([AliasTarget::new(
disabled.reference.id,
disabled.reference.version,
)])
.version_at(alias.slug.clone(), alias.reference.version.next());
state
.supersede(legacy_alias)
.expect("write the historical alias shape");
assert!(
state.validate().is_err(),
"new candidates reject the legacy enabled-alias/disabled-target shape"
);
assert!(
candidate(state.clone()).validated_checksum().is_err(),
"ordinary publication remains strict"
);
assert!(
candidate(state.clone())
.validated_checksum_legacy_read()
.is_ok(),
"rollback may republish the retained legacy state"
);
let mut rollback = candidate(state.clone());
rollback.mutation.kind = MutationKind::Rollback;
rollback.audit.kind = MutationKind::Rollback;
RevisionManifest::of(revision_id(43), None, SystemTime::UNIX_EPOCH, &rollback)
.expect("store-side manifest validation permits rollback history");
let candidate = candidate(state.clone());
let mut entries = state
.resources()
.map(ManifestEntry::of)
.collect::<Result<Vec<_>, _>>()
.expect("manifest entries");
entries.sort_by_key(|entry| entry.reference);
let mut blobs: Vec<_> = state.blobs().copied().collect();
blobs.sort_by_key(|blob| blob.digest);
let manifest = RevisionManifest {
id: revision_id(42),
parent: None,
created_at: SystemTime::UNIX_EPOCH,
serializer: SerializerVersion::default(),
mutation: candidate.mutation.id,
entries,
blobs,
checksum: state.checksum().expect("state checksum"),
};
let loaded = LoadedRevision::assemble(manifest, state.clone())
.expect("legacy published revisions remain readable");
assert_eq!(loaded.state(), &state);
let resolved = Models::of(loaded.state()).expect("legacy model read");
assert_eq!(resolved.aliases().count(), 1);
assert_eq!(resolved.enablements().count(), 2);
}
#[test]
fn a_one_alias_repair_can_carry_untouched_legacy_aliases_forward() {
let mut base = state_with_models();
let target = base
.version_of(ResourceKind::ModelEnablement, resource_id(31))
.cloned()
.expect("the project enablement");
let disabled = ModelEnablementBody::read(&target)
.expect("an enablement body")
.transitioned(ModelLifecycle::Disabled)
.version_at(
target.slug.clone(),
target.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
base.supersede(disabled.clone())
.expect("disable the target");
let first = base
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the first alias");
let legacy_targets = [AliasTarget::new(
disabled.reference.id,
disabled.reference.version,
)];
let first_legacy = ModelAliasBody::read(&first)
.expect("an alias body")
.retargeted(legacy_targets);
base.supersede(first_legacy.version_at(first.slug.clone(), first.reference.version.next()))
.expect("write the first legacy alias");
let second_legacy = ModelAliasBody::new(
resource_id(33),
tenant_id(1),
super::super::fixtures::project_id(2),
super::super::models::WireFamily::OpenaiChat,
legacy_targets,
)
.version(Slug::parse("slow").expect("alias slug"));
base.insert(second_legacy)
.expect("write the second legacy alias");
assert!(
base.validate().is_err(),
"the stored shape is legacy-invalid"
);
let mut repaired = base.clone();
let first = repaired
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the first alias");
let repaired_body = ModelAliasBody::read(&first)
.expect("an alias body")
.transitioned(ModelLifecycle::Disabled)
.retargeted([]);
repaired
.supersede(repaired_body.version_at(first.slug, first.reference.version.next()))
.expect("repair one alias");
let mut repaired_candidate = candidate(repaired);
repaired_candidate.legacy_aliases =
legacy_alias_allowlist(&base, &repaired_candidate.state);
repaired_candidate
.validated_checksum_for_publication()
.expect("one-resource repair may carry the untouched legacy alias");
let mut authored = base.clone();
let second = authored
.version_of(ResourceKind::Alias, resource_id(33))
.cloned()
.expect("the second alias");
let authored_body = ModelAliasBody::read(&second).expect("an alias body");
authored
.supersede(authored_body.version_at(second.slug, second.reference.version.next()))
.expect("author a new alias version");
let mut authored_candidate = candidate(authored);
authored_candidate.legacy_aliases =
legacy_alias_allowlist(&base, &authored_candidate.state);
assert!(
authored_candidate
.validated_checksum_for_publication()
.is_err(),
"a newly authored offending alias remains refused"
);
}
#[test]
fn a_restacked_legacy_alias_may_follow_its_enablement_version() {
let mut base = state_with_models();
let target = base
.version_of(ResourceKind::ModelEnablement, resource_id(31))
.cloned()
.expect("the project enablement");
let disabled = ModelEnablementBody::read(&target)
.expect("an enablement body")
.transitioned(ModelLifecycle::Disabled)
.version_at(
target.slug.clone(),
target.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
base.supersede(disabled.clone())
.expect("disable the target");
let alias = base
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the project alias");
let legacy_alias = ModelAliasBody::read(&alias)
.expect("an alias body")
.retargeted([AliasTarget::new(
disabled.reference.id,
disabled.reference.version,
)]);
base.supersede(legacy_alias.version_at(alias.slug.clone(), alias.reference.version.next()))
.expect("write the legacy alias");
let mut restacked = base.clone();
let disabled = restacked
.version_of(ResourceKind::ModelEnablement, resource_id(31))
.cloned()
.expect("the disabled enablement");
let disabled_next = ModelEnablementBody::read(&disabled)
.expect("an enablement body")
.version_at(
disabled.slug.clone(),
disabled.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
restacked
.supersede(disabled_next.clone())
.expect("advance the disabled enablement");
let alias = restacked
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the legacy alias");
let restacked_alias = ModelAliasBody::read(&alias)
.expect("an alias body")
.retargeted([AliasTarget::new(
disabled_next.reference.id,
disabled_next.reference.version,
)])
.version_at(alias.slug.clone(), alias.reference.version.next());
restacked
.supersede(restacked_alias)
.expect("restack the dependent alias");
let allowed = legacy_alias_allowlist(&base, &restacked);
let restacked_reference = restacked
.version_of(ResourceKind::Alias, resource_id(32))
.expect("the restacked alias")
.reference;
assert!(allowed.contains(&restacked_reference));
let mut restacked_candidate = candidate(restacked);
restacked_candidate.legacy_aliases = allowed;
restacked_candidate
.validated_checksum_for_publication()
.expect("a restack-only target/version carry-forward remains allowed");
let mut authored = base.clone();
let target = authored
.version_of(ResourceKind::ModelEnablement, resource_id(31))
.cloned()
.expect("the disabled enablement");
let target_next = ModelEnablementBody::read(&target)
.expect("an enablement body")
.version_at(
target.slug.clone(),
target.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
authored
.supersede(target_next.clone())
.expect("advance the disabled enablement");
let alias = authored
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the legacy alias");
let base_default = base
.version_of(ResourceKind::ModelEnablement, resource_id(30))
.expect("the tenant default");
let authored_alias = ModelAliasBody::read(&alias)
.expect("an alias body")
.retargeted([
AliasTarget::new(base_default.reference.id, base_default.reference.version),
AliasTarget::new(target_next.reference.id, target_next.reference.version),
])
.version_at(alias.slug, alias.reference.version.next());
authored
.supersede(authored_alias)
.expect("author a changed alias");
let mut authored_candidate = candidate(authored.clone());
authored_candidate.legacy_aliases = legacy_alias_allowlist(&base, &authored);
assert!(
!authored_candidate
.legacy_aliases
.iter()
.any(|reference| reference.kind == ResourceKind::Alias),
"an authored target reorder is not a restack carry-forward"
);
assert!(
authored_candidate
.validated_checksum_for_publication()
.is_err(),
"an authored change to the offending alias remains refused"
);
}
#[test]
fn a_legacy_alias_with_another_disabled_target_explains_the_required_repair() {
let mut base = state_with_models();
let legacy_target = base
.version_of(ResourceKind::ModelEnablement, resource_id(31))
.cloned()
.expect("the project enablement");
let legacy_disabled = ModelEnablementBody::read(&legacy_target)
.expect("an enablement body")
.transitioned(ModelLifecycle::Disabled)
.version_at(
legacy_target.slug.clone(),
legacy_target.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
base.supersede(legacy_disabled.clone())
.expect("write the pre-existing disabled target");
let alias = base
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the project alias");
let default_target = base
.version_of(ResourceKind::ModelEnablement, resource_id(30))
.cloned()
.expect("the tenant default");
let legacy_alias = ModelAliasBody::read(&alias)
.expect("an alias body")
.retargeted([
AliasTarget::new(
legacy_disabled.reference.id,
legacy_disabled.reference.version,
),
AliasTarget::new(
default_target.reference.id,
default_target.reference.version,
),
])
.version_at(alias.slug.clone(), alias.reference.version.next());
base.supersede(legacy_alias)
.expect("write the pre-existing legacy alias");
let mut candidate_state = base.clone();
let default_target = candidate_state
.version_of(ResourceKind::ModelEnablement, resource_id(30))
.cloned()
.expect("the enabled target being disabled");
let disabled_default = ModelEnablementBody::read(&default_target)
.expect("an enablement body")
.transitioned(ModelLifecycle::Disabled)
.version_at(
default_target.slug.clone(),
default_target.reference.version.next(),
reference(ResourceKind::CatalogModel, 5),
);
candidate_state
.supersede(disabled_default)
.expect("disable the requested target");
let alias = candidate_state
.version_of(ResourceKind::Alias, resource_id(32))
.cloned()
.expect("the legacy alias");
let restacked_alias = ModelAliasBody::read(&alias)
.expect("an alias body")
.retargeted([AliasTarget::new(
legacy_disabled.reference.id,
legacy_disabled.reference.version,
)])
.version_at(alias.slug, alias.reference.version.next());
candidate_state
.supersede(restacked_alias)
.expect("restack the alias after removing one target");
let mut candidate = candidate(candidate_state.clone());
candidate.legacy_aliases = legacy_alias_allowlist(&base, &candidate_state);
assert!(
candidate.legacy_aliases.is_empty(),
"a changed target count cannot be treated as a carry-forward"
);
let error = candidate
.validated_checksum_for_publication()
.expect_err("the remaining disabled target must keep this unsafe change refused");
assert!(
error
.to_string()
.contains("repair or retire this alias before publishing"),
"the refusal should tell the operator how to unblock the requested change: {error}"
);
assert!(matches!(
error,
ValidationError::Model(ref model)
if matches!(**model, super::super::models::ModelError::DisabledTarget { .. })
));
}
#[test]
fn a_checksum_mismatch_is_reported_when_the_state_itself_still_adds_up() {
let candidate = candidate(state());
let mut manifest = manifest(&candidate);
manifest.checksum = Checksum::of(b"not the state");
let error = LoadedRevision::assemble(manifest.clone(), candidate.state.clone())
.expect_err("the recorded checksum must be enforced");
assert_eq!(
error,
IntegrityError::ChecksumMismatch {
expected: manifest.checksum,
actual: candidate.state.checksum().unwrap()
}
);
assert!(error.to_string().contains("hashes to"));
}
#[test]
fn a_rotted_resource_row_names_itself() {
let candidate = candidate(state());
let manifest = manifest(&candidate);
let mut state = DesiredState::new();
let mut rotted = None;
for resource in candidate.state.resources() {
let mut resource = resource.clone();
if resource.reference.kind == ResourceKind::Alias {
resource.body = ResourceBody::Inline(CanonicalValue::string("tampered"));
rotted = Some(resource.reference);
}
state.insert(resource).unwrap();
}
for blob in candidate.state.blobs() {
state.declare_blob(*blob);
}
let reference = rotted.expect("the fixture has an alias");
let error = LoadedRevision::assemble(manifest, state)
.expect_err("a tampered body must not hydrate");
assert!(
matches!(error, IntegrityError::ContentMismatch { reference: named, .. } if named == reference),
"{error}"
);
}
#[test]
fn a_manifest_and_a_state_must_name_the_same_resources() {
let candidate = candidate(state());
let manifest = manifest(&candidate);
let mut missing = manifest.clone();
let dropped = missing.entries.pop().expect("entries");
let error = LoadedRevision::assemble(missing, candidate.state.clone())
.expect_err("extra state must not hydrate");
assert_eq!(
error,
IntegrityError::UnexpectedResource {
reference: dropped.reference
}
);
let mut short = DesiredState::new();
let mut skipped = None;
for resource in candidate.state.resources() {
if resource.reference.kind == ResourceKind::Alias {
skipped = Some(resource.reference);
continue;
}
short.insert(resource.clone()).unwrap();
}
for blob in candidate.state.blobs() {
short.declare_blob(*blob);
}
let error = LoadedRevision::assemble(manifest.clone(), short)
.expect_err("a missing row must not hydrate");
assert_eq!(
error,
IntegrityError::MissingResource {
reference: skipped.expect("the fixture has an alias")
}
);
}
#[test]
fn a_renamed_row_is_an_entry_mismatch_not_a_silent_rename() {
let candidate = candidate(state());
let manifest = manifest(&candidate);
let mut state = DesiredState::new();
for resource in candidate.state.resources() {
let mut resource = resource.clone();
if resource.reference.kind == ResourceKind::Alias {
resource.slug = Slug::parse("renamed-underneath").unwrap();
}
state.insert(resource).unwrap();
}
for blob in candidate.state.blobs() {
state.declare_blob(*blob);
}
let error =
LoadedRevision::assemble(manifest, state).expect_err("a rename must not hydrate");
assert!(
matches!(error, IntegrityError::EntryMismatch { .. }),
"{error}"
);
}
#[test]
fn blob_declarations_must_agree_in_both_directions() {
let catalog = blob_backed_catalog(5);
let mut with_blob = DesiredState::new();
with_blob.insert(tenant(1, "acme")).unwrap();
with_blob.insert(catalog.clone()).unwrap();
with_blob.declare_blob(*catalog.body.blob().unwrap());
let declared = manifest(&candidate(with_blob));
let mut without = DesiredState::new();
without.insert(tenant(1, "acme")).unwrap();
let mut trimmed = declared.clone();
trimmed
.entries
.retain(|entry| without.get(&entry.reference).is_some());
trimmed.checksum = without.checksum().unwrap();
let error = LoadedRevision::assemble(trimmed, without)
.expect_err("a manifest blob the state does not declare must not hydrate");
assert_eq!(
error,
IntegrityError::MissingBlob {
digest: catalog.body.blob().unwrap().digest
}
);
let candidate = candidate(state());
let mut extra = manifest(&candidate);
extra.blobs.clear();
let error = LoadedRevision::assemble(extra, candidate.state.clone())
.expect_err("a state blob the manifest does not declare must not hydrate");
assert!(
matches!(error, IntegrityError::UnexpectedBlob { .. }),
"{error}"
);
}
#[test]
fn a_revision_written_by_another_serializer_is_not_silently_rehashed() {
let candidate = candidate(state());
let manifest = manifest(&candidate);
assert_eq!(manifest.serializer, SerializerVersion::V1);
assert_eq!(
LoadedRevision::assemble(manifest, candidate.state)
.map(|loaded| loaded.id())
.unwrap(),
revision_id(1)
);
}
#[test]
fn stored_state_that_is_not_valid_desired_state_is_an_integrity_error() {
let candidate = candidate(state());
let mut manifest = manifest(&candidate);
let mut state = candidate.state.clone();
let catalog = state
.resources()
.find(|resource| resource.body.blob().is_some())
.cloned()
.expect("the fixture has a blob-backed resource");
let mut rebuilt = DesiredState::new();
for resource in state.resources() {
rebuilt.insert(resource.clone()).unwrap();
}
state = rebuilt;
manifest.blobs.clear();
let error = LoadedRevision::assemble(manifest, state)
.expect_err("an undeclared blob must not hydrate");
assert_eq!(
error,
IntegrityError::Invalid(ValidationError::DanglingBlobReference {
from: catalog.reference,
digest: catalog.body.blob().unwrap().digest
})
);
}
#[test]
fn a_candidate_validates_before_it_reports_a_checksum() {
let valid = candidate(state());
assert_eq!(
valid.validated_checksum().unwrap(),
valid.state.checksum().unwrap()
);
assert_eq!(
candidate(DesiredState::new()).validated_checksum(),
Err(ValidationError::Empty)
);
}
#[test]
fn an_audit_event_recording_another_mutation_is_refused() {
let mut detached = candidate(state());
let elsewhere = MutationId::new(Uuid7::from_parts(7, 0, 7).unwrap());
detached.audit.mutation = elsewhere;
assert_eq!(
detached.validated_checksum(),
Err(ValidationError::AuditMutationMismatch {
audit: detached.audit.id,
recorded: elsewhere,
mutation: detached.mutation.id
})
);
}
#[test]
fn unrepresentable_state_is_a_validation_error_not_a_panic() {
let mut state = DesiredState::new();
state.insert(tenant(1, "acme")).unwrap();
state
.insert(ResourceVersion::new(
reference(ResourceKind::Alias, 2),
ResourceScope::Tenant(tenant_id(1)),
Slug::parse("fast").unwrap(),
ResourceBody::Inline(CanonicalValue::string("wire\tfamily")),
))
.unwrap();
assert!(matches!(
state.checksum(),
Err(CanonicalError::ControlCharacter { .. })
));
assert!(matches!(
candidate(state).validated_checksum(),
Err(ValidationError::Canonical(
CanonicalError::ControlCharacter { .. }
))
));
let _ = resource_id(1);
}
}