518 lines
22 KiB
Rust
518 lines
22 KiB
Rust
//! Catalog behavior: immutable revisions, candidate lifecycle, aliases,
|
|
//! game revision pin refs, and restart durability.
|
|
|
|
mod common;
|
|
use common::*;
|
|
use makepad_asset_store::{AssetServerCore, Budgets, CandidateState, ServerError};
|
|
use makepad_asset_data::{AssetAlias, AssetRevisionRef, BlobId};
|
|
|
|
#[test]
|
|
fn stage_requires_registered_asset_and_present_blobs() {
|
|
let (_root, core) = open_core("admission");
|
|
let glb = b"glb bytes".to_vec();
|
|
let thumb = b"thumb bytes".to_vec();
|
|
let manifest = prop_manifest(asset_id_n(1), &glb, &thumb);
|
|
let bytes = manifest.to_canonical_bytes().unwrap();
|
|
|
|
// Unregistered asset refuses.
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes, NOW).unwrap_err(),
|
|
ServerError::NotFound { what: "asset for revision" }
|
|
));
|
|
|
|
// Registered but blobs absent refuses (fail closed on dangling refs).
|
|
core.catalog().register_asset(&asset_id_n(1), "rik2", NOW).unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes, NOW).unwrap_err(),
|
|
ServerError::NotFound { what: "asset file blob" }
|
|
));
|
|
|
|
// With the GLB present the thumbnail is still missing.
|
|
core.put_blob(&glb, NOW).unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes, NOW).unwrap_err(),
|
|
ServerError::NotFound { what: "asset thumbnail blob" }
|
|
));
|
|
|
|
core.put_blob(&thumb, NOW).unwrap();
|
|
let rev = core.catalog().stage_asset_revision(&bytes, NOW).unwrap();
|
|
// The revision ID is the digest of the canonical manifest bytes.
|
|
assert_eq!(rev, manifest.revision().unwrap());
|
|
assert_eq!(
|
|
core.catalog().asset_candidate_state(&asset_id_n(1), &rev).unwrap(),
|
|
Some(CandidateState::Staged)
|
|
);
|
|
assert_eq!(
|
|
core.catalog().asset_revision_manifest(&rev).unwrap().unwrap(),
|
|
bytes
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_over_budget_refused() {
|
|
let root = test_root("manifest_budget");
|
|
let budgets = Budgets {
|
|
max_manifest_bytes: 16,
|
|
..Budgets::default_v1()
|
|
};
|
|
let core = AssetServerCore::open(&root, budgets).unwrap();
|
|
let manifest = prop_manifest(asset_id_n(1), b"g", b"t");
|
|
let bytes = manifest.to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes, NOW).unwrap_err(),
|
|
ServerError::OverBudget { what: "asset manifest bytes", .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn dependency_revisions_must_exist() {
|
|
let (_root, core) = open_core("deps");
|
|
let glb = b"glb".to_vec();
|
|
let thumb = b"thumb".to_vec();
|
|
core.put_blob(&glb, NOW).unwrap();
|
|
core.put_blob(&thumb, NOW).unwrap();
|
|
core.catalog().register_asset(&asset_id_n(1), "rik2", NOW).unwrap();
|
|
let mut manifest = prop_manifest(asset_id_n(1), &glb, &thumb);
|
|
manifest.dependencies = vec![AssetRevisionRef {
|
|
asset_id: asset_id_n(9),
|
|
revision: makepad_asset_data::AssetRevisionId::from_bytes([9; 32]),
|
|
}];
|
|
let bytes = manifest.to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes, NOW).unwrap_err(),
|
|
ServerError::NotFound { what: "asset dependency revision" }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_lifecycle_and_alias_rules() {
|
|
let (_root, core) = open_core("lifecycle");
|
|
let glb = b"lifecycle glb".to_vec();
|
|
let thumb = b"lifecycle thumb".to_vec();
|
|
core.put_blob(&glb, NOW).unwrap();
|
|
core.put_blob(&thumb, NOW).unwrap();
|
|
let id = asset_id_n(1);
|
|
core.catalog().register_asset(&id, "rik2", NOW).unwrap();
|
|
let manifest = prop_manifest(id, &glb, &thumb);
|
|
let bytes = manifest.to_canonical_bytes().unwrap();
|
|
let rev = core.catalog().stage_asset_revision(&bytes, NOW).unwrap();
|
|
let target = AssetRevisionRef { asset_id: id, revision: rev };
|
|
let alias = "rik2/props/box".parse().unwrap();
|
|
|
|
// Re-staging identical content while staged is idempotent.
|
|
assert_eq!(core.catalog().stage_asset_revision(&bytes, NOW + 1).unwrap(), rev);
|
|
|
|
// Aliases may only point at PUBLISHED revisions.
|
|
assert!(matches!(
|
|
core.catalog().set_asset_alias(&alias, &target, NOW).unwrap_err(),
|
|
ServerError::InvalidState { what: "alias target", .. }
|
|
));
|
|
|
|
core.catalog().publish_asset(&id, &rev, NOW + 2).unwrap();
|
|
// Publishing again is an idempotent repeat.
|
|
core.catalog().publish_asset(&id, &rev, NOW + 3).unwrap();
|
|
assert_eq!(
|
|
core.catalog().asset_candidate_state(&id, &rev).unwrap(),
|
|
Some(CandidateState::Published)
|
|
);
|
|
|
|
// Namespace mismatch between alias and asset refuses.
|
|
let foreign_alias = "other/props/box".parse().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().set_asset_alias(&foreign_alias, &target, NOW).unwrap_err(),
|
|
ServerError::Conflict { what: "alias namespace" }
|
|
));
|
|
|
|
core.catalog().set_asset_alias(&alias, &target, NOW + 4).unwrap();
|
|
assert_eq!(core.catalog().resolve_asset_alias(&alias).unwrap(), Some(target));
|
|
|
|
// Re-admitting content that already published is refused.
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes, NOW + 5).unwrap_err(),
|
|
ServerError::InvalidState { what: "candidate", state: "published" }
|
|
));
|
|
|
|
// Alias head retargets to a second published revision.
|
|
let glb2 = b"lifecycle glb v2".to_vec();
|
|
core.put_blob(&glb2, NOW + 6).unwrap();
|
|
let manifest2 = prop_manifest(id, &glb2, &thumb);
|
|
let bytes2 = manifest2.to_canonical_bytes().unwrap();
|
|
let rev2 = core.catalog().stage_asset_revision(&bytes2, NOW + 6).unwrap();
|
|
assert_ne!(rev, rev2);
|
|
core.catalog().publish_asset(&id, &rev2, NOW + 7).unwrap();
|
|
let target2 = AssetRevisionRef { asset_id: id, revision: rev2 };
|
|
core.catalog().set_asset_alias(&alias, &target2, NOW + 8).unwrap();
|
|
assert_eq!(core.catalog().resolve_asset_alias(&alias).unwrap(), Some(target2));
|
|
|
|
// Quarantine is reachable from published and is terminal; the alias head
|
|
// pointing at the pulled revision is torn down in the same transaction.
|
|
core.catalog().quarantine_asset(&id, &rev2, NOW + 9).unwrap();
|
|
assert_eq!(core.catalog().resolve_asset_alias(&alias).unwrap(), None);
|
|
core.catalog().quarantine_asset(&id, &rev2, NOW + 10).unwrap(); // idempotent
|
|
assert!(matches!(
|
|
core.catalog().publish_asset(&id, &rev2, NOW + 11).unwrap_err(),
|
|
ServerError::InvalidState { what: "candidate transition", state: "quarantined" }
|
|
));
|
|
assert!(matches!(
|
|
core.catalog().stage_asset_revision(&bytes2, NOW + 12).unwrap_err(),
|
|
ServerError::InvalidState { what: "candidate", state: "quarantined" }
|
|
));
|
|
// A quarantined revision can no longer become an alias target.
|
|
assert!(matches!(
|
|
core.catalog().set_asset_alias(&alias, &target2, NOW + 13).unwrap_err(),
|
|
ServerError::InvalidState { what: "alias target", .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn quarantine_tears_down_every_alias_head_on_the_revision() {
|
|
let (_root, core) = open_core("quarantine_alias");
|
|
let glb1 = b"qa glb v1".to_vec();
|
|
let glb2 = b"qa glb v2".to_vec();
|
|
let thumb = b"qa thumb".to_vec();
|
|
for blob in [&glb1, &glb2, &thumb] {
|
|
core.put_blob(blob, NOW).unwrap();
|
|
}
|
|
let id = asset_id_n(1);
|
|
core.catalog().register_asset(&id, "rik2", NOW).unwrap();
|
|
let bytes1 = prop_manifest(id, &glb1, &thumb).to_canonical_bytes().unwrap();
|
|
let bytes2 = prop_manifest(id, &glb2, &thumb).to_canonical_bytes().unwrap();
|
|
let rev1 = core.catalog().stage_asset_revision(&bytes1, NOW).unwrap();
|
|
let rev2 = core.catalog().stage_asset_revision(&bytes2, NOW).unwrap();
|
|
core.catalog().publish_asset(&id, &rev1, NOW).unwrap();
|
|
core.catalog().publish_asset(&id, &rev2, NOW).unwrap();
|
|
let t1 = AssetRevisionRef { asset_id: id, revision: rev1 };
|
|
let t2 = AssetRevisionRef { asset_id: id, revision: rev2 };
|
|
let a1: AssetAlias = "rik2/props/one".parse().unwrap();
|
|
let a2: AssetAlias = "rik2/props/two".parse().unwrap();
|
|
let a3: AssetAlias = "rik2/props/three".parse().unwrap();
|
|
core.catalog().set_asset_alias(&a1, &t1, NOW).unwrap();
|
|
core.catalog().set_asset_alias(&a2, &t1, NOW).unwrap();
|
|
core.catalog().set_asset_alias(&a3, &t2, NOW).unwrap();
|
|
|
|
// Quarantining rev1 drops BOTH heads pointing at it and only those.
|
|
core.catalog().quarantine_asset(&id, &rev1, NOW + 1).unwrap();
|
|
assert_eq!(core.catalog().resolve_asset_alias(&a1).unwrap(), None);
|
|
assert_eq!(core.catalog().resolve_asset_alias(&a2).unwrap(), None);
|
|
assert_eq!(core.catalog().resolve_asset_alias(&a3).unwrap(), Some(t2));
|
|
|
|
// clear_asset_alias is an idempotent head removal; a published target
|
|
// can be re-pointed afterwards.
|
|
assert!(core.catalog().clear_asset_alias(&a3).unwrap());
|
|
assert!(!core.catalog().clear_asset_alias(&a3).unwrap());
|
|
assert_eq!(core.catalog().resolve_asset_alias(&a3).unwrap(), None);
|
|
core.catalog().set_asset_alias(&a3, &t2, NOW + 2).unwrap();
|
|
assert_eq!(core.catalog().resolve_asset_alias(&a3).unwrap(), Some(t2));
|
|
}
|
|
|
|
#[test]
|
|
fn asset_registration_conflicts_refuse() {
|
|
let (_root, core) = open_core("register");
|
|
core.catalog().register_asset(&asset_id_n(1), "rik2", NOW).unwrap();
|
|
// Idempotent same-namespace repeat.
|
|
core.catalog().register_asset(&asset_id_n(1), "rik2", NOW + 1).unwrap();
|
|
assert!(matches!(
|
|
core.catalog().register_asset(&asset_id_n(1), "other", NOW + 2).unwrap_err(),
|
|
ServerError::Conflict { what: "asset namespace" }
|
|
));
|
|
assert!(matches!(
|
|
core.catalog().register_asset(&asset_id_n(2), "Bad Namespace", NOW).unwrap_err(),
|
|
ServerError::InvalidInput { .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn game_revision_pins_published_assets_only() {
|
|
let (_root, core) = open_core("game");
|
|
let now = NOW;
|
|
let (id_a, rev_a) = publish_prop(&core, "rik2", 1, b"glb a", b"thumb a", now);
|
|
// Second asset stays staged only.
|
|
let glb_b = b"glb b".to_vec();
|
|
let thumb_b = b"thumb b".to_vec();
|
|
core.put_blob(&glb_b, now).unwrap();
|
|
core.put_blob(&thumb_b, now).unwrap();
|
|
core.catalog().register_asset(&asset_id_n(2), "rik2", now).unwrap();
|
|
let manifest_b = prop_manifest(asset_id_n(2), &glb_b, &thumb_b);
|
|
let rev_b = core
|
|
.catalog()
|
|
.stage_asset_revision(&manifest_b.to_canonical_bytes().unwrap(), now)
|
|
.unwrap();
|
|
|
|
let gid = game_id_n(7);
|
|
core.catalog().register_game(&gid, "rik2", now).unwrap();
|
|
let ref_a = AssetRevisionRef { asset_id: id_a, revision: rev_a };
|
|
let ref_b = AssetRevisionRef { asset_id: asset_id_n(2), revision: rev_b };
|
|
let lock = lock_for(gid, &[("rik2/props/a", ref_a), ("rik2/props/b", ref_b)]);
|
|
let splash = b"splash source".to_vec();
|
|
let toml = b"manifest toml".to_vec();
|
|
let gthumb = b"game thumb".to_vec();
|
|
for blob in [&splash, &toml, &lock, >humb] {
|
|
core.put_blob(blob, now).unwrap();
|
|
}
|
|
let gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
let gm_bytes = gm.to_canonical_bytes().unwrap();
|
|
|
|
// One pinned revision is not published -> refuse.
|
|
assert!(matches!(
|
|
core.catalog().stage_game_revision(&gm_bytes, &lock, now).unwrap_err(),
|
|
ServerError::InvalidState { what: "pinned asset revision", .. }
|
|
));
|
|
|
|
core.catalog().publish_asset(&asset_id_n(2), &rev_b, now).unwrap();
|
|
let grev = core.catalog().stage_game_revision(&gm_bytes, &lock, now).unwrap();
|
|
assert_eq!(grev, gm.revision().unwrap());
|
|
|
|
// The refs table pins exactly the closure.
|
|
let mut expected = vec![ref_a, ref_b];
|
|
expected.sort();
|
|
assert_eq!(core.catalog().game_revision_refs(&grev).unwrap(), expected);
|
|
|
|
// Game alias requires a published game revision.
|
|
let galias = "rik2/games/test-game".parse().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().set_game_alias(&galias, &gid, &grev, now).unwrap_err(),
|
|
ServerError::InvalidState { what: "game alias target", .. }
|
|
));
|
|
core.catalog().publish_game(&gid, &grev, now).unwrap();
|
|
core.catalog().set_game_alias(&galias, &gid, &grev, now).unwrap();
|
|
assert_eq!(
|
|
core.catalog().resolve_game_alias(&galias).unwrap(),
|
|
Some((gid, grev))
|
|
);
|
|
|
|
// Game alias heads clear idempotently and re-point while published.
|
|
assert!(core.catalog().clear_game_alias(&galias).unwrap());
|
|
assert!(!core.catalog().clear_game_alias(&galias).unwrap());
|
|
core.catalog().set_game_alias(&galias, &gid, &grev, now).unwrap();
|
|
|
|
// Quarantine the game revision: terminal there too, and the alias head
|
|
// pointing at the pulled revision is gone.
|
|
core.catalog().quarantine_game(&gid, &grev, now).unwrap();
|
|
assert_eq!(core.catalog().resolve_game_alias(&galias).unwrap(), None);
|
|
assert!(matches!(
|
|
core.catalog().publish_game(&gid, &grev, now).unwrap_err(),
|
|
ServerError::InvalidState { .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn game_lock_binding_is_verified() {
|
|
let (_root, core) = open_core("game_lock");
|
|
let now = NOW;
|
|
let (id_a, rev_a) = publish_prop(&core, "rik2", 1, b"glb a", b"thumb a", now);
|
|
let gid = game_id_n(7);
|
|
core.catalog().register_game(&gid, "rik2", now).unwrap();
|
|
let ref_a = AssetRevisionRef { asset_id: id_a, revision: rev_a };
|
|
let lock = lock_for(gid, &[("rik2/props/a", ref_a)]);
|
|
let other_lock = lock_for(game_id_n(8), &[("rik2/props/a", ref_a)]);
|
|
let splash = b"s".to_vec();
|
|
let toml = b"t".to_vec();
|
|
let gthumb = b"th".to_vec();
|
|
for blob in [&splash, &toml, &lock, &other_lock, >humb] {
|
|
core.put_blob(blob, now).unwrap();
|
|
}
|
|
|
|
// Manifest names one lock digest but different lock bytes arrive.
|
|
let gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
let gm_bytes = gm.to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_game_revision(&gm_bytes, &other_lock, now).unwrap_err(),
|
|
ServerError::DigestMismatch { what: "game lock blob", .. }
|
|
));
|
|
|
|
// Lock whose game_id disagrees with the manifest refuses.
|
|
let gm_cross = game_manifest(gid, &splash, &toml, &other_lock, >humb);
|
|
let gm_cross_bytes = gm_cross.to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_game_revision(&gm_cross_bytes, &other_lock, now).unwrap_err(),
|
|
ServerError::Conflict { what: "lock game_id" }
|
|
));
|
|
|
|
// Missing lock blob record refuses even when digests agree.
|
|
let unrecorded_lock = lock_for(gid, &[("rik2/props/x", ref_a)]);
|
|
let gm2 = game_manifest(gid, &splash, &toml, &unrecorded_lock, >humb);
|
|
let gm2_bytes = gm2.to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
core.catalog().stage_game_revision(&gm2_bytes, &unrecorded_lock, now).unwrap_err(),
|
|
ServerError::NotFound { what: "game lock blob" }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn asset_declared_byte_lengths_must_match_recorded_sizes() {
|
|
let (_root, core) = open_core("size_lies");
|
|
let glb = b"glb payload".to_vec();
|
|
let thumb = b"thumb payload".to_vec();
|
|
core.put_blob(&glb, NOW).unwrap();
|
|
core.put_blob(&thumb, NOW).unwrap();
|
|
core.catalog().register_asset(&asset_id_n(1), "rik2", NOW).unwrap();
|
|
|
|
// File length lie. metrics.total_bytes tracks the lie so the manifest
|
|
// stays contract-valid: the SERVER's size-vs-CAS check must refuse it.
|
|
let mut lying = prop_manifest(asset_id_n(1), &glb, &thumb);
|
|
lying.files[0].byte_len += 1;
|
|
lying.metrics.total_bytes += 1;
|
|
let err = core
|
|
.catalog()
|
|
.stage_asset_revision(&lying.to_canonical_bytes().unwrap(), NOW)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::SizeMismatch { what: "asset file blob size", .. }), "{err}");
|
|
|
|
// Thumbnail length lie.
|
|
let mut lying = prop_manifest(asset_id_n(1), &glb, &thumb);
|
|
lying.thumbnail.as_mut().unwrap().byte_len -= 1;
|
|
lying.metrics.total_bytes -= 1;
|
|
let err = core
|
|
.catalog()
|
|
.stage_asset_revision(&lying.to_canonical_bytes().unwrap(), NOW)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::SizeMismatch { what: "asset thumbnail blob size", .. }), "{err}");
|
|
|
|
// The truthful manifest still stages.
|
|
let honest = prop_manifest(asset_id_n(1), &glb, &thumb);
|
|
core.catalog()
|
|
.stage_asset_revision(&honest.to_canonical_bytes().unwrap(), NOW)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn game_declared_byte_lengths_must_match_recorded_sizes() {
|
|
let (_root, core) = open_core("game_size_lies");
|
|
let now = NOW;
|
|
let (id_a, rev_a) = publish_prop(&core, "rik2", 1, b"glb a", b"thumb a", now);
|
|
let gid = game_id_n(7);
|
|
core.catalog().register_game(&gid, "rik2", now).unwrap();
|
|
let ref_a = AssetRevisionRef { asset_id: id_a, revision: rev_a };
|
|
let lock = lock_for(gid, &[("rik2/props/a", ref_a)]);
|
|
let splash = b"splash source".to_vec();
|
|
let toml = b"manifest toml".to_vec();
|
|
let gthumb = b"game thumb".to_vec();
|
|
for blob in [&splash, &toml, &lock, >humb] {
|
|
core.put_blob(blob, now).unwrap();
|
|
}
|
|
|
|
// Splash length lie.
|
|
let mut gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
gm.splash_byte_len += 1;
|
|
let err = core
|
|
.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::SizeMismatch { what: "game splash blob size", .. }), "{err}");
|
|
|
|
// Thumbnail length lie.
|
|
let mut gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
gm.thumbnail.byte_len += 1;
|
|
let err = core
|
|
.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::SizeMismatch { what: "game thumbnail blob size", .. }), "{err}");
|
|
|
|
// Lock record size lie: this lock's digest carries a corrupt catalog
|
|
// size, so even the true bytes (matching digest) must refuse admission.
|
|
let lock2 = lock_for(gid, &[("rik2/props/b", ref_a)]);
|
|
core.catalog()
|
|
.record_blob(&BlobId::hash_of(&lock2), lock2.len() as u64 + 5, now)
|
|
.unwrap();
|
|
let gm2 = game_manifest(gid, &splash, &toml, &lock2, >humb);
|
|
let err = core
|
|
.catalog()
|
|
.stage_game_revision(&gm2.to_canonical_bytes().unwrap(), &lock2, now)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::SizeMismatch { what: "game lock blob size", .. }), "{err}");
|
|
|
|
// The truthful manifest still stages.
|
|
let gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
core.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn game_catalog_snapshot_pin_is_checked() {
|
|
let (_root, core) = open_core("game_snapshot");
|
|
let now = NOW;
|
|
let (id_a, rev_a) = publish_prop(&core, "rik2", 1, b"glb a", b"thumb a", now);
|
|
let gid = game_id_n(7);
|
|
core.catalog().register_game(&gid, "rik2", now).unwrap();
|
|
let ref_a = AssetRevisionRef { asset_id: id_a, revision: rev_a };
|
|
let lock = lock_for(gid, &[("rik2/props/a", ref_a)]);
|
|
let splash = b"splash source".to_vec();
|
|
let toml = b"manifest toml".to_vec();
|
|
let gthumb = b"game thumb".to_vec();
|
|
for blob in [&splash, &toml, &lock, >humb] {
|
|
core.put_blob(blob, now).unwrap();
|
|
}
|
|
let snapshot = b"catalog snapshot document".to_vec();
|
|
|
|
// Pinned but unrecorded snapshot refuses.
|
|
let mut gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
gm.catalog_snapshot = Some(BlobId::hash_of(&snapshot));
|
|
let err = core
|
|
.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::NotFound { what: "game catalog snapshot blob" }), "{err}");
|
|
|
|
// Empty snapshot refuses.
|
|
let empty = BlobId::hash_of(b"");
|
|
core.catalog().record_blob(&empty, 0, now).unwrap();
|
|
let mut gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
gm.catalog_snapshot = Some(empty);
|
|
let err = core
|
|
.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::InvalidInput { what: "game catalog snapshot empty" }), "{err}");
|
|
|
|
// Snapshot beyond the canonical-document bound refuses; the recorded
|
|
// size is what admission judges, so record an over-bound size directly.
|
|
let big = BlobId::hash_of(b"pretend huge snapshot");
|
|
core.catalog().record_blob(&big, 1024 * 1024 + 1, now).unwrap();
|
|
let mut gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
gm.catalog_snapshot = Some(big);
|
|
let err = core
|
|
.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap_err();
|
|
assert!(matches!(err, ServerError::OverBudget { what: "game catalog snapshot bytes", .. }), "{err}");
|
|
|
|
// A real recorded snapshot stages, and the revision is the manifest hash.
|
|
core.put_blob(&snapshot, now).unwrap();
|
|
let mut gm = game_manifest(gid, &splash, &toml, &lock, >humb);
|
|
gm.catalog_snapshot = Some(BlobId::hash_of(&snapshot));
|
|
let grev = core
|
|
.catalog()
|
|
.stage_game_revision(&gm.to_canonical_bytes().unwrap(), &lock, now)
|
|
.unwrap();
|
|
assert_eq!(grev, gm.revision().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn catalog_survives_reopen() {
|
|
let root = test_root("durable");
|
|
let alias: makepad_asset_data::AssetAlias = "rik2/props/box".parse().unwrap();
|
|
let (id, rev);
|
|
{
|
|
let core = AssetServerCore::open(&root, Budgets::default_v1()).unwrap();
|
|
let pair = publish_prop(&core, "rik2", 1, b"durable glb", b"durable thumb", NOW);
|
|
id = pair.0;
|
|
rev = pair.1;
|
|
core.catalog()
|
|
.set_asset_alias(&alias, &AssetRevisionRef { asset_id: id, revision: rev }, NOW)
|
|
.unwrap();
|
|
}
|
|
let core = AssetServerCore::open(&root, Budgets::default_v1()).unwrap();
|
|
core.recover(NOW + 100).unwrap();
|
|
assert_eq!(
|
|
core.catalog().resolve_asset_alias(&alias).unwrap(),
|
|
Some(AssetRevisionRef { asset_id: id, revision: rev })
|
|
);
|
|
assert_eq!(
|
|
core.catalog().asset_candidate_state(&id, &rev).unwrap(),
|
|
Some(CandidateState::Published)
|
|
);
|
|
let manifest_bytes = core.catalog().asset_revision_manifest(&rev).unwrap().unwrap();
|
|
assert_eq!(BlobId::hash_of(&manifest_bytes).as_bytes(), rev.as_bytes());
|
|
}
|