use super::*;
#[test]
fn loads_all_embedded_manifests() {
let catalog = SectorCatalog::new();
assert_eq!(catalog.len(), 11);
}
#[test]
fn exactly_three_sectors_are_in_force() {
let catalog = SectorCatalog::new();
let mut in_force: Vec<&str> = catalog.in_force().iter().map(|d| d.key.as_str()).collect();
in_force.sort_unstable();
assert_eq!(in_force, vec!["battery", "electronics", "unsold-goods"]);
}
#[test]
fn provisional_sectors_are_flagged_not_dropped() {
let catalog = SectorCatalog::new();
assert_eq!(catalog.provisional().len(), 8);
assert!(!catalog.is_in_force("textile"));
assert!(!catalog.is_in_force("steel"));
}
#[test]
fn battery_descriptor_is_complete() {
let catalog = SectorCatalog::new();
let battery = catalog.get("battery").expect("battery in catalog");
assert_eq!(battery.status, RegulatoryStatus::InForce);
assert_eq!(battery.dpp_applies_from.as_deref(), Some("2027-02-18"));
assert_eq!(battery.retention_years, 10);
assert!(battery.schema_versions.contains(&"2.0.0".to_string()));
assert!(battery.schema_versions.contains(&"2.1.0".to_string()));
assert!(battery.schema_versions.contains(&"2.2.0".to_string()));
assert!(battery.schema_versions.contains(&"2.3.0".to_string()));
assert!(battery.schema_versions.contains(&"2.4.0".to_string()));
assert_eq!(battery.current_schema_version, "2.4.0");
assert_eq!(battery.plugin.as_deref(), Some("sector-battery"));
}
#[test]
fn resolve_schema_version_new_vs_existing() {
let catalog = SectorCatalog::new();
assert_eq!(
catalog.resolve_schema_version("battery", None).as_deref(),
Some("2.4.0")
);
assert_eq!(
catalog
.resolve_schema_version("battery", Some("1.0.0"))
.as_deref(),
Some("1.0.0")
);
assert_eq!(catalog.resolve_schema_version("unknown", None), None);
}
#[test]
fn in_force_gating_is_status_driven() {
let catalog = SectorCatalog::new();
assert!(catalog.is_in_force("battery"));
assert!(catalog.is_in_force("unsold-goods"));
assert!(catalog.is_in_force("electronics"));
assert!(!catalog.is_in_force("detergent")); assert!(!catalog.is_in_force("nonexistent"));
}
#[test]
fn allows_determination_matches_status() {
assert!(RegulatoryStatus::InForce.allows_determination());
assert!(!RegulatoryStatus::Provisional.allows_determination());
}
#[test]
fn register_runtime_sector() {
let mut catalog = SectorCatalog::new();
let descriptor = SectorDescriptor {
key: "plastics".into(),
title: "Plastics".into(),
status: RegulatoryStatus::Provisional,
regime: Regime::Espr,
legal_basis: vec!["ESPR Working Plan".into()],
dpp_applies_from: None,
retention_years: 10,
schema_versions: vec!["1.0.0".into()],
current_schema_version: "1.0.0".into(),
product_categories: vec![],
disclosure: std::collections::HashMap::new(),
plugin: None,
notes: None,
};
assert!(catalog.register(descriptor.clone()).is_ok());
assert_eq!(catalog.len(), 12);
assert!(matches!(
catalog.register(descriptor),
Err(CatalogError::AlreadyExists(_))
));
}
fn provisional_descriptor(current: &str, versions: Vec<String>) -> SectorDescriptor {
SectorDescriptor {
key: "plastics".into(),
title: "Plastics".into(),
status: RegulatoryStatus::Provisional,
regime: Regime::Espr,
legal_basis: vec!["ESPR Working Plan".into()],
dpp_applies_from: None,
retention_years: 10,
schema_versions: versions,
current_schema_version: current.into(),
product_categories: vec![],
disclosure: std::collections::HashMap::new(),
plugin: None,
notes: None,
}
}
#[test]
fn register_rejects_invalid_current_schema_version() {
let mut catalog = SectorCatalog::new();
let descriptor = provisional_descriptor("not-semver", vec!["not-semver".into()]);
assert!(matches!(
catalog.register(descriptor),
Err(CatalogError::InvalidSchemaVersion { .. })
));
assert_eq!(catalog.len(), 11);
}
#[test]
fn register_rejects_current_version_not_in_list() {
let mut catalog = SectorCatalog::new();
let descriptor = provisional_descriptor("2.0.0", vec!["1.0.0".into()]);
assert!(matches!(
catalog.register(descriptor),
Err(CatalogError::CurrentVersionNotListed { .. })
));
assert_eq!(catalog.len(), 11);
}
#[test]
fn sector_enum_and_catalog_agree() {
use crate::domain::sector::Sector;
let catalog = SectorCatalog::new();
let typed = [
Sector::Battery,
Sector::Textile,
Sector::UnsoldGoods,
Sector::Steel,
Sector::Electronics,
Sector::Construction,
Sector::Tyre,
Sector::Toy,
Sector::Aluminium,
Sector::Furniture,
Sector::Detergent,
];
for sector in &typed {
let key = sector.catalog_key();
assert!(
catalog.get(key).is_some(),
"Sector::{sector:?} (key '{key}') has no embedded catalog entry"
);
}
let typed_keys: std::collections::HashSet<&str> =
typed.iter().map(Sector::catalog_key).collect();
for key in catalog.keys() {
assert!(
typed_keys.contains(key),
"catalog key '{key}' has no corresponding typed Sector variant"
);
}
}
#[test]
fn every_sector_declares_a_retention_period() {
let catalog = SectorCatalog::new();
for descriptor in catalog.all() {
let key = descriptor.key.as_str();
assert_eq!(
catalog.retention_years(key),
Some(descriptor.retention_years),
"retention_years() disagrees with the descriptor for '{key}'"
);
assert!(
descriptor.retention_years > 0,
"sector '{key}' declares no retention period"
);
}
assert_eq!(catalog.retention_years("packaging"), None);
}
#[test]
fn unsold_goods_cites_espr_article_25() {
let catalog = SectorCatalog::new();
let textile = catalog.get("unsold-goods").expect("unsold-goods present");
let basis = textile.legal_basis.join(" ");
assert!(
basis.contains("Article 25"),
"unsold-goods legal basis must cite ESPR Article 25, got: {basis}"
);
assert!(
!basis.contains("Article 22"),
"the incorrect Article 22 citation must be gone, got: {basis}"
);
}
#[test]
fn descriptor_round_trips_camel_case() {
let catalog = SectorCatalog::new();
let battery = catalog.get("battery").unwrap();
let json = serde_json::to_value(battery).unwrap();
assert_eq!(json["dppAppliesFrom"], "2027-02-18");
assert_eq!(json["status"], "in_force");
let back: SectorDescriptor = serde_json::from_value(json).unwrap();
assert_eq!(back.key, "battery");
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn disclosure_keys_match_schema_properties() {
use crate::schemas::VersionedSchemaRegistry;
let catalog = SectorCatalog::new();
let registry = VersionedSchemaRegistry::new();
for descriptor in catalog.all() {
if descriptor.disclosure.is_empty() {
continue;
}
let version: semver::Version =
descriptor
.current_schema_version
.parse()
.unwrap_or_else(|_| {
panic!(
"sector '{}' currentSchemaVersion '{}' is not valid semver",
descriptor.key, descriptor.current_schema_version
)
});
let schema_json = registry.get(&descriptor.key, &version).unwrap_or_else(|| {
panic!(
"schema not found for sector '{}' v{}",
descriptor.key, descriptor.current_schema_version
)
});
let schema: serde_json::Value =
serde_json::from_str(schema_json).expect("embedded schema must be valid JSON");
let properties = schema
.get("properties")
.and_then(|p| p.as_object())
.unwrap_or_else(|| {
panic!(
"schema for sector '{}' has no top-level 'properties' object",
descriptor.key
)
});
for key in descriptor.disclosure.keys() {
assert!(
properties.contains_key(key),
"disclosure key '{}' in sector '{}' does not match any property in schema v{} \
(properties: {:?}). Either rename the key to match the serialised field name, \
or remove it — a mismatched key silently fails to gate the field.",
key,
descriptor.key,
descriptor.current_schema_version,
properties.keys().collect::<Vec<_>>()
);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn catalog_agrees_with_schema_registry() {
use crate::schemas::VersionedSchemaRegistry;
let catalog = SectorCatalog::new();
let registry = VersionedSchemaRegistry::new();
for d in catalog.all() {
let reg_versions: Vec<String> = registry
.versions_for(&d.key)
.iter()
.map(|v| v.to_string())
.collect();
for v in &d.schema_versions {
assert!(
reg_versions.contains(v),
"catalog sector '{}' declares schema {v} not embedded in the registry (registry has {reg_versions:?})",
d.key
);
}
assert!(
d.schema_versions.contains(&d.current_schema_version),
"catalog sector '{}' currentSchemaVersion {} is not in its schemaVersions {:?}",
d.key,
d.current_schema_version,
d.schema_versions
);
}
for sector in registry.sectors() {
assert!(
catalog.get(sector).is_some(),
"schema registry has sector '{sector}' with no catalog entry"
);
}
for d in catalog.in_force() {
assert!(
d.plugin.is_some(),
"in-force sector '{}' must declare a plugin binding",
d.key
);
}
}
#[test]
fn every_manifest_declares_a_regime() {
for d in SectorCatalog::new().all().iter() {
if d.status == RegulatoryStatus::Watch {
continue;
}
assert_ne!(
d.regime,
Regime::None,
"sector '{}' has status {:?} but no regime; Regime::None is reserved for Watch sectors with no DPP instrument",
d.key,
d.status
);
}
}
#[test]
fn a_watch_sector_may_declare_no_regime() {
let watch = r#"{
"key": "w", "title": "W", "status": "watch", "regime": "none",
"legalBasis": [], "retentionYears": 10,
"schemaVersions": ["1.0.0"], "currentSchemaVersion": "1.0.0"
}"#;
let d: SectorDescriptor = serde_json::from_str(watch).expect("watch + none must parse");
assert_eq!(d.regime, Regime::None);
assert_eq!(d.status, RegulatoryStatus::Watch);
}
#[test]
fn manifest_without_regime_is_rejected() {
let no_regime = r#"{
"key": "x", "title": "X", "status": "provisional",
"legalBasis": [], "retentionYears": 10,
"schemaVersions": ["1.0.0"], "currentSchemaVersion": "1.0.0"
}"#;
assert!(serde_json::from_str::<SectorDescriptor>(no_regime).is_err());
}
#[test]
fn most_sectors_are_not_espr() {
let catalog = SectorCatalog::new();
let non_espr = catalog
.all()
.iter()
.filter(|d| d.regime != Regime::Espr)
.count();
assert_eq!(
non_espr, 5,
"expected 5 non-ESPR sectors (battery, toy, detergent, construction, electronics)"
);
}
#[test]
fn regime_does_not_affect_determination_gating() {
let catalog = SectorCatalog::new();
for d in catalog
.all()
.iter()
.filter(|d| d.status == RegulatoryStatus::Provisional)
{
assert!(
!d.status.allows_determination(),
"provisional sector '{}' (regime {:?}) must not allow determinations",
d.key,
d.regime
);
}
}
#[test]
fn watch_never_allows_determination() {
assert!(!RegulatoryStatus::Watch.allows_determination());
}
#[test]
fn in_force_with_future_passport_date_still_determines() {
let catalog = SectorCatalog::new();
let battery = catalog.get("battery").expect("battery in catalog");
assert_eq!(battery.status, RegulatoryStatus::InForce);
assert_eq!(battery.dpp_applies_from.as_deref(), Some("2027-02-18"));
assert!(battery.status.allows_determination());
}
#[test]
fn every_manifest_round_trips() {
for d in SectorCatalog::new().all().iter() {
let json = serde_json::to_string(d).expect("serialise");
let back: SectorDescriptor = serde_json::from_str(&json).expect("deserialise");
assert_eq!(
serde_json::to_value(&back).expect("re-serialise"),
serde_json::to_value(d).expect("serialise"),
"round-trip changed sector '{}'",
d.key
);
}
}
const CATEGORY_ENUM_PROPERTY: &[(&str, &str)] = &[
("aluminium", "productionRoute"),
("battery", "batteryType"),
("construction", "productFamily"),
("detergent", "productType"),
("electronics", "productCategory"),
("furniture", "productType"),
("steel", "productCategory"),
("tyre", "tyreClass"),
("unsold-goods", "productCategory"),
];
#[test]
fn product_categories_are_legal_values_of_their_schema_enum() {
use crate::schemas::VersionedSchemaRegistry;
let catalog = SectorCatalog::new();
let registry = VersionedSchemaRegistry::new();
for (sector_key, property) in CATEGORY_ENUM_PROPERTY {
let descriptor = catalog
.get(sector_key)
.unwrap_or_else(|| panic!("sector '{sector_key}' is in the table but not the catalog"));
let version: semver::Version = descriptor
.current_schema_version
.parse()
.expect("currentSchemaVersion is valid semver");
let schema_json = registry
.get(sector_key, &version)
.unwrap_or_else(|| panic!("no schema for '{sector_key}' v{version}"));
let schema: serde_json::Value =
serde_json::from_str(schema_json).expect("schema is valid JSON");
let allowed: Vec<&str> = schema["properties"][property]["enum"]
.as_array()
.unwrap_or_else(|| {
panic!("'{sector_key}' schema property '{property}' has no enum — stale table row")
})
.iter()
.filter_map(serde_json::Value::as_str)
.collect();
for category in &descriptor.product_categories {
assert!(
allowed.contains(&category.as_str()),
"sector '{sector_key}' lists product category '{category}', which is not a legal \
value of schema property '{property}' ({allowed:?})"
);
}
}
}