Construct example calls from fetched structured identity and scope_ref. Add exhaustive public DTO coverage and a transactional external-style provider over TCP to exercise root/self scopes, equal names in distinct scopes, optional and stale refs, deletion/republication, visibility, validator precedence and non-retry of unknown outcomes. Validation: cargo test -p wip-public-api-example (9 DTO/example tests, 4 real TCP conformance tests); Nix clippy --all-targets -D warnings.
301 lines
9.3 KiB
Rust
301 lines
9.3 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use wip_host::CallErrorKind;
|
|
use wip_interface::{Entry, JsonMember, JsonValue, WipType};
|
|
use wip_protocol::{INTERFACE_FORMAT_V1, InterfaceDescriptor, Object, ProtocolErrorCode, Value};
|
|
use wip_public_api_example::{CatalogDefinition, PublishOutcome, PublishRequest, Visibility, run};
|
|
|
|
#[test]
|
|
fn public_crates_support_the_complete_qualified_end_to_end_scenario() {
|
|
let report = run().unwrap();
|
|
|
|
assert_eq!(report.object_name, "article-service");
|
|
assert_eq!(
|
|
report.object_reference.as_deref(),
|
|
Some("article-service-instance")
|
|
);
|
|
assert_eq!(
|
|
report.object_validator.as_deref(),
|
|
Some(b"object-v1".as_slice())
|
|
);
|
|
assert_eq!(
|
|
report.published_interfaces,
|
|
[
|
|
report.catalog_interface_reference.clone(),
|
|
report.audit_interface_reference.clone(),
|
|
],
|
|
"interfaces must retain explicit publication order"
|
|
);
|
|
assert_ne!(
|
|
report.catalog_interface_reference, report.audit_interface_reference,
|
|
"interfaces form an ordered set of distinct scope/name references"
|
|
);
|
|
assert_eq!(report.catalog_interface_reference.scope, "/article-service");
|
|
assert_eq!(report.catalog_interface_reference.name, "catalog");
|
|
assert_eq!(report.audit_interface_reference.scope, "/article-service");
|
|
assert_eq!(report.audit_interface_reference.name, "audit");
|
|
assert_eq!(report.catalog_descriptor_format, INTERFACE_FORMAT_V1);
|
|
assert_eq!(report.audit_descriptor_format, INTERFACE_FORMAT_V1);
|
|
assert_eq!(
|
|
report.catalog_scope_ref.as_deref(),
|
|
Some("article-service-instance")
|
|
);
|
|
assert_eq!(report.audit_scope_ref, report.catalog_scope_ref);
|
|
assert_eq!(report.scope_mismatch, ProtocolErrorCode::InterfaceMismatch);
|
|
assert_eq!(
|
|
report.published,
|
|
PublishOutcome::Published("article-1".into())
|
|
);
|
|
assert_eq!(
|
|
report.rejected,
|
|
PublishOutcome::Rejected("a title is required".into())
|
|
);
|
|
assert_eq!(report.catalog_status, "catalog-ready");
|
|
assert_eq!(report.audit_status, "audit-ready");
|
|
}
|
|
|
|
#[test]
|
|
fn domain_host_permission_and_validator_failures_remain_distinct() {
|
|
let report = run().unwrap();
|
|
|
|
assert!(matches!(report.rejected, PublishOutcome::Rejected(_)));
|
|
assert_eq!(report.host_failure, CallErrorKind::Internal);
|
|
assert_eq!(report.protocol_failure.code, ProtocolErrorCode::Internal);
|
|
assert_eq!(
|
|
report.unknown_protocol_failure.code,
|
|
ProtocolErrorCode::OperationOutcomeUnknown
|
|
);
|
|
assert_eq!(
|
|
report.permission_failure,
|
|
ProtocolErrorCode::PermissionDenied
|
|
);
|
|
assert_eq!(
|
|
report.validator_failure,
|
|
ProtocolErrorCode::ValidatorMismatch,
|
|
"validator recovery signals must not collapse to InvalidRequest"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interface_mismatch_is_not_interface_not_found() {
|
|
let report = run().unwrap();
|
|
|
|
assert_eq!(
|
|
report.interface_mismatch,
|
|
ProtocolErrorCode::InterfaceMismatch
|
|
);
|
|
assert_eq!(
|
|
report.interface_not_found,
|
|
ProtocolErrorCode::InterfaceNotFound
|
|
);
|
|
assert_ne!(report.interface_mismatch, report.interface_not_found);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_path_keeps_canonical_failure_precedence() {
|
|
let report = run().unwrap();
|
|
|
|
assert_eq!(report.precedence_failure, ProtocolErrorCode::InvalidRequest);
|
|
assert_ne!(report.precedence_failure, report.validator_failure);
|
|
assert_ne!(report.precedence_failure, report.interface_mismatch);
|
|
}
|
|
|
|
#[test]
|
|
fn object_fields_keep_path_context_and_optional_ref_separate() {
|
|
// Listing every field makes this test fail to compile if a mandatory
|
|
// ObjectId is reintroduced into the canonical object DTO.
|
|
let object = Object {
|
|
name: "article".into(),
|
|
description: None,
|
|
interfaces: vec![root_reference("catalog-ref"), root_reference("audit-ref")],
|
|
r#ref: None,
|
|
validator: None,
|
|
};
|
|
|
|
object.validate_at_path("/library/article").unwrap();
|
|
assert_eq!(object.name, "article");
|
|
assert_eq!(object.r#ref, None);
|
|
assert_eq!(
|
|
object.interfaces,
|
|
[root_reference("catalog-ref"), root_reference("audit-ref")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn interface_reference_is_observed_separately_from_descriptor_fields() {
|
|
// An exact destructuring pattern is compile-time coverage that the
|
|
// descriptor has no trait-name or opaque-reference `name` field.
|
|
let InterfaceDescriptor {
|
|
format,
|
|
documentation,
|
|
types,
|
|
operations,
|
|
} = CatalogDefinition::descriptor();
|
|
|
|
assert_eq!(format, INTERFACE_FORMAT_V1);
|
|
assert!(documentation.is_some());
|
|
assert!(!types.is_empty());
|
|
assert_eq!(
|
|
operations
|
|
.iter()
|
|
.map(|operation| operation.name.as_str())
|
|
.collect::<Vec<_>>(),
|
|
[
|
|
"publish",
|
|
"status",
|
|
"backend_status",
|
|
"uncertain_backend_status",
|
|
"restricted_status",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn application_record_enum_union_entry_and_json_use_schema_neutral_values() {
|
|
let request = PublishRequest {
|
|
title: "Canonical values".into(),
|
|
body: None,
|
|
source: Entry::new("/drafts/canonical-values").unwrap(),
|
|
visibility: Visibility::Public,
|
|
metadata: JsonValue::Object(vec![
|
|
JsonMember {
|
|
name: "tags".into(),
|
|
value: JsonValue::Array(vec![JsonValue::String("wip".into()), JsonValue::Null]),
|
|
},
|
|
JsonMember {
|
|
name: "ready".into(),
|
|
value: JsonValue::Boolean(true),
|
|
},
|
|
]),
|
|
};
|
|
|
|
let encoded = request.encode().unwrap();
|
|
assert_schema_neutral(&encoded);
|
|
let Value::Record(fields) = encoded else {
|
|
panic!("derived DTO record must use Value::Record")
|
|
};
|
|
assert_eq!(
|
|
fields.get("title"),
|
|
Some(&Value::String("Canonical values".into()))
|
|
);
|
|
assert!(!fields.contains_key("body"));
|
|
assert_eq!(
|
|
fields.get("source"),
|
|
Some(&Value::String("/drafts/canonical-values".into()))
|
|
);
|
|
assert_eq!(
|
|
fields.get("visibility"),
|
|
Some(&Value::String("Public".into()))
|
|
);
|
|
assert_eq!(
|
|
fields.get("metadata"),
|
|
Some(&Value::Record(BTreeMap::from([
|
|
("ready".into(), Value::Boolean(true)),
|
|
(
|
|
"tags".into(),
|
|
Value::List(vec![Value::String("wip".into()), Value::Unit])
|
|
),
|
|
])))
|
|
);
|
|
|
|
let outcome = PublishOutcome::Rejected("policy".into()).encode().unwrap();
|
|
assert_schema_neutral(&outcome);
|
|
assert_eq!(
|
|
outcome,
|
|
Value::Record(BTreeMap::from([
|
|
("$case".into(), Value::String("Rejected".into())),
|
|
("value".into(), Value::String("policy".into())),
|
|
]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn entry_is_a_validated_canonical_absolute_path_string() {
|
|
let entry = Entry::new("/articles/one").unwrap();
|
|
assert_eq!(
|
|
entry.encode().unwrap(),
|
|
Value::String("/articles/one".into())
|
|
);
|
|
|
|
for invalid in [
|
|
"articles/one",
|
|
"/articles/",
|
|
"/articles//one",
|
|
"/articles/./one",
|
|
"/articles/../one",
|
|
] {
|
|
assert!(
|
|
Entry::new(invalid).is_err(),
|
|
"accepted invalid path {invalid}"
|
|
);
|
|
assert!(
|
|
Entry::decode(Value::String(invalid.into())).is_err(),
|
|
"decoded invalid path {invalid}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn assert_schema_neutral(value: &Value) {
|
|
// This exhaustive match is a compile-time regression guard: reintroducing
|
|
// semantic Entry/Enum/Union/Json variants makes the consumer non-exhaustive.
|
|
match value {
|
|
Value::Unit
|
|
| Value::Boolean(_)
|
|
| Value::Integer(_)
|
|
| Value::Number(_)
|
|
| Value::String(_)
|
|
| Value::Bytes(_) => {}
|
|
Value::Record(fields) => {
|
|
for value in fields.values() {
|
|
assert_schema_neutral(value);
|
|
}
|
|
}
|
|
Value::List(values) => {
|
|
for value in values {
|
|
assert_schema_neutral(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scoped_dtos_keep_identity_binding_and_validators_separate() {
|
|
// Exact literals/destructuring guard the public Rust contract, including
|
|
// omission of refs rather than requiring a fabricated identity.
|
|
let reference = wip_protocol::InterfaceReference {
|
|
scope: "/article-service".into(),
|
|
name: "catalog::read#raw".into(),
|
|
};
|
|
let request = wip_protocol::FetchInterfaceRequest {
|
|
interface: reference.clone(),
|
|
};
|
|
request.validate().unwrap();
|
|
let wip_protocol::FetchInterfaceResponse {
|
|
interface,
|
|
scope_ref,
|
|
descriptor,
|
|
validator,
|
|
} = wip_protocol::FetchInterfaceResponse {
|
|
interface: reference.clone(),
|
|
scope_ref: None,
|
|
descriptor: CatalogDefinition::descriptor(),
|
|
validator: None,
|
|
};
|
|
let target = wip_protocol::InterfaceTarget {
|
|
reference: interface,
|
|
scope_ref,
|
|
validator,
|
|
};
|
|
assert_eq!(target.reference, reference);
|
|
assert!(target.scope_ref.is_none());
|
|
assert!(target.validator.is_none());
|
|
assert_eq!(descriptor.format, INTERFACE_FORMAT_V1);
|
|
}
|
|
|
|
fn root_reference(name: &str) -> wip_protocol::InterfaceReference {
|
|
wip_protocol::InterfaceReference {
|
|
scope: "/".into(),
|
|
name: name.into(),
|
|
}
|
|
}
|