use super::*;
use crate::{
passport::{ManufacturerInfo, Passport},
status::PassportStatus,
};
use chrono::Utc;
fn make_published_passport() -> Passport {
Passport {
product_name: "Test".into(),
manufacturer: ManufacturerInfo {
name: "ACME".into(),
address: "Berlin".into(),
country: None,
did_web_url: None,
registered_trade_name: None,
electronic_address: None,
},
status: PassportStatus::Published,
qr_code_url: Some("https://id.odal-node.io/01/09506000134352".into()),
jws_signature: Some("eyJ0eXAiOiJKV1QifQ.payload.sig".into()),
published_at: Some(Utc::now()),
placed_on_market_date: None,
schema_version: "1.1.0".into(),
retention_locked: true,
operator_identifier: Some("did:web:acme.example.com".into()),
product_group_data: Some(crate::product_group::ProductGroupData::Textile(Box::new(
crate::test_support::sample_textile_data(),
))),
facility: Some(crate::passport::FacilitySnapshot {
scheme: "national".into(),
value: "FAC-DE-001".into(),
name: "Acme Plant".into(),
country: "DE".into(),
address: None,
}),
..crate::test_support::sample_passport()
}
}
fn acme() -> RegisteringOperator<'static> {
RegisteringOperator {
legal_name: "Acme GmbH",
country: "DE",
identifier_scheme: "did",
}
}
#[test]
fn from_published_passport_maps_all_fields() {
let passport = make_published_passport();
let req = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect("the fixture passport is complete");
assert_eq!(req.passport_id, passport.id);
assert_eq!(req.operator_identifier, "did:web:acme.example.com");
assert_eq!(req.facility_identifier, "FAC-DE-001");
assert_eq!(
req.facility.as_ref().map(|f| f.name.as_str()),
Some("Acme Plant")
);
assert_eq!(
req.facility.as_ref().map(|f| f.country.as_str()),
Some("DE")
);
assert_eq!(req.product_category, "textile");
assert_eq!(
req.data_carrier_uri,
"https://id.odal-node.io/01/09506000134352"
);
assert_eq!(req.schema_version, "1.1.0");
assert!(req.jws_signature.is_some());
assert!(req.published_at.is_some());
assert_eq!(req.country_code, "DE");
assert_eq!(req.operator_name, "Acme GmbH");
assert_ne!(
req.operator_name, passport.manufacturer.name,
"operator and manufacturer are distinct legal persons"
);
assert_eq!(req.granularity, RegistrationGranularity::Item);
}
#[test]
fn an_incomplete_passport_is_refused_with_every_missing_field_named() {
let mut passport = make_published_passport();
passport.operator_identifier = None;
passport.facility = None;
passport.qr_code_url = None;
let refused = RegistrationRequest::from_published_passport(
&passport,
RegisteringOperator {
legal_name: "",
country: "",
identifier_scheme: "",
},
RegistrationGranularity::Item,
)
.expect_err("a passport carrying none of the three cannot be registered");
let fields: Vec<&str> = refused.errors.iter().map(|e| e.field.as_str()).collect();
assert_eq!(
fields,
["/operatorIdentifier", "/facility", "/qrCodeUrl"],
"every missing field must be reported, not the first"
);
}
#[test]
fn a_passport_missing_only_its_carrier_url_reports_only_that() {
let mut passport = make_published_passport();
passport.qr_code_url = None;
let refused = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect_err("the carrier URI is what a registration resolves to");
assert_eq!(refused.errors.len(), 1);
assert_eq!(refused.errors[0].field, "/qrCodeUrl");
}
#[test]
fn registry_status_serde_round_trip() {
let statuses = vec![
RegistryStatus::Pending,
RegistryStatus::Registered,
RegistryStatus::Rejected,
RegistryStatus::SuspendedByAuthority,
RegistryStatus::Deactivated,
];
for status in statuses {
let json = serde_json::to_string(&status).unwrap();
let back: RegistryStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status, back);
}
}
#[test]
fn the_model_identifier_reaches_the_registration() {
use crate::product_group::ProductGroupData;
let mut passport = make_published_passport();
passport.product_group_data = Some(ProductGroupData::Battery(Box::new(
crate::product_group::BatteryData {
battery_model_id: Some("BM-4815".into()),
..crate::test_support::sample_battery_data()
},
)));
let req = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect("the fixture passport is complete");
assert_eq!(req.model_id.as_deref(), Some("BM-4815"));
}
#[test]
fn a_product_group_without_a_model_identifier_reports_none() {
let passport = make_published_passport();
assert!(
RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect("the fixture passport is complete")
.model_id
.is_none()
);
}
#[test]
fn every_clause_5_scheme_reaches_the_request() {
use crate::identifier::ProductIdentifier;
use crate::product_group::ProductGroupData;
for identifier in [
ProductIdentifier::gs1(crate::identifier::Gtin::parse("09506000134352").unwrap()),
ProductIdentifier::identification_link("https://id.acme.example.com/p/1").unwrap(),
ProductIdentifier::did("did:web:acme.example.com:p:1").unwrap(),
] {
let mut passport = make_published_passport();
let mut textile = crate::test_support::sample_textile_data();
textile.product_identifier = identifier.clone();
passport.product_group_data = Some(ProductGroupData::Textile(Box::new(textile)));
let req = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect("a passport that identifies itself can be registered");
assert_eq!(
req.product_identifier.as_ref(),
Some(&identifier),
"the identifier must travel, not be re-derived from the carrier"
);
}
}
#[test]
fn a_product_group_that_identifies_nothing_is_refused() {
use crate::product_group::ProductGroupData;
let mut passport = make_published_passport();
passport.product_group_data = Some(ProductGroupData::UnsoldGoods(
crate::test_support::sample_unsold_goods_report(),
));
let refused = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect_err("a discard disclosure is not a product registration");
assert_eq!(refused.errors.len(), 1);
assert_eq!(
refused.errors[0].field,
"/productGroupData/productIdentifier"
);
}
fn untyped_passport(product_identifier: serde_json::Value) -> Passport {
use crate::product_group::{ProductGroup, ProductGroupData};
let mut passport = make_published_passport();
passport.product_group = ProductGroup::Other("photovoltaic".into());
passport.product_group_data = ProductGroupData::other(serde_json::json!({
"productGroup": "photovoltaic",
"productIdentifier": product_identifier,
}));
passport
}
#[test]
fn an_untyped_product_group_registers_on_the_identifier_it_carries() {
let passport = untyped_passport(serde_json::json!({
"scheme": "gs1",
"gtin": "09506000134352",
}));
let req = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect("a valid identifier registers whether or not the group is typed");
let identifier = req.product_identifier.expect("carried");
assert_eq!(identifier.as_str(), "09506000134352");
}
#[test]
fn an_untyped_product_group_with_a_malformed_identifier_is_refused() {
let passport = untyped_passport(serde_json::json!({
"scheme": "gs1",
"gtin": "09506000134351",
}));
let refused = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect_err("a bad check digit is no identifier");
assert_eq!(
refused.errors[0].field,
"/productGroupData/productIdentifier"
);
}
#[test]
fn a_present_but_blank_field_is_as_absent_as_a_missing_one() {
let mut passport = make_published_passport();
passport.operator_identifier = Some(String::new());
passport.qr_code_url = Some(" ".into());
if let Some(facility) = passport.facility.as_mut() {
facility.value = String::new();
}
let refused = RegistrationRequest::from_published_passport(
&passport,
acme(),
RegistrationGranularity::Item,
)
.expect_err("blank is not a value the passport carried");
let fields: Vec<&str> = refused.errors.iter().map(|e| e.field.as_str()).collect();
assert_eq!(fields, ["/operatorIdentifier", "/facility", "/qrCodeUrl"]);
}