use super::*;
use serde_json::{json, Value};
fn labels() -> Vec<Entry> {
[
(
"gatehouse.lifecycle.authority",
"container-lifecycle/mutate",
),
(
"gatehouse.lifecycle.invocation",
"11111111111111111111111111111111",
),
("gatehouse.lifecycle.project", "project"),
("gatehouse.lifecycle.resource", "resource"),
]
.into_iter()
.map(|(k, v)| Entry {
key: k.into(),
value: v.into(),
})
.collect()
}
fn target() -> Target {
Target {
container_port: 1234,
object_id: "a".repeat(64),
name: "test-container".into(),
labels: labels(),
}
}
fn request(operation: Operation) -> Request {
let mut r = Request {
protocol: PROTOCOL.into(),
binding: Binding {
installation_sha256: "b".repeat(64),
executable_sha256: "c".repeat(64),
config_sha256: "d".repeat(64),
engine_binding: EngineBinding {
engine: "docker".into(),
transport: "unix-http".into(),
socket_path: "/run/test.sock".into(),
socket_uid: 1000,
principal_uid: 1000,
rootless: true,
api_version: "1.47".into(),
server_version: "27".into(),
daemon_id: "test-daemon".into(),
scope: "test".into(),
},
project_id: "project".into(),
resource_id: "resource".into(),
lease_invocation: "1".repeat(32),
invocation: "2".repeat(32),
generation: 1,
operation_key: "0".repeat(64),
},
operation,
};
r.binding.operation_key = r.operation_key().unwrap();
r
}
fn object(state: ContainerState) -> Object {
let t = target();
Object {
object_id: t.object_id,
name: t.name,
labels: t.labels,
state,
ports: if state == ContainerState::Running {
vec![Port {
container_port: 1234,
protocol: "tcp".into(),
host_ip: "127.0.0.1".into(),
host_port: 49152,
}]
} else {
vec![]
},
image_id: format!("sha256:{}", "e".repeat(64)),
}
}
fn observations() -> Observations {
let o = Observation {
api_version: "1.47".into(),
server_version: "27".into(),
daemon_id: "test-daemon".into(),
};
Observations {
observed_before: o.clone(),
observed_after: o,
}
}
fn response(r: &Request, outcome: Outcome) -> Response {
Response {
protocol: r.protocol.clone(),
binding: r.binding.clone(),
operation: r.operation.clone(),
outcome,
observations: Some(observations()),
}
}
fn bytes<T: Serialize>(t: &T) -> Vec<u8> {
canonical::encode(&serde_json::to_value(t).unwrap()).unwrap()
}
fn absent() -> Outcome {
let t = target();
Outcome::Absent {
evidence: Absence {
object_id: t.object_id.clone(),
route: format!("/v1.47/containers/{}/json", t.object_id),
http_status: 404,
error_message_sha256: format!(
"{:x}",
Sha256::digest(format!("No such container: {}", t.object_id).as_bytes())
),
},
}
}
#[test]
fn all_operations_have_schema_valid_roundtrip() {
for op in [
Operation::ResolveImage {
image: "repo:tag".into(),
},
Operation::Create {
name: target().name,
image_id: object(ContainerState::Created).image_id,
labels: labels(),
environment: vec![],
container_port: 1234,
},
Operation::Status { target: target() },
Operation::Start { target: target() },
Operation::Stop {
target: target(),
grace_seconds: 10,
},
Operation::Remove { target: target() },
Operation::HealthExec {
target: target(),
argv: vec!["check".into()],
timeout_ms: 500,
},
] {
let r = request(op);
let b = r.canonical_bytes().unwrap();
assert_eq!(parse_protected_request(&b, &b).unwrap(), r);
}
}
#[test]
fn full_request_vector_is_stable() {
let r = request(Operation::Status { target: target() });
let expected = include_bytes!("status-request.json");
assert_eq!(r.canonical_bytes().unwrap(), expected);
assert_eq!(parse_protected_request(expected, expected).unwrap(), r);
}
#[test]
fn protected_request_rejects_digest_echo_stale_generation_and_different_target() {
let r = request(Operation::Status { target: target() });
let trusted = r.canonical_bytes().unwrap();
let mut stale = r.clone();
stale.binding.generation += 1;
assert!(parse_request(&bytes(&stale)).is_err());
stale.binding.operation_key = stale.operation_key().unwrap();
assert!(parse_protected_request(&bytes(&stale), &trusted).is_err());
let mut modified = r.clone();
modified.binding.invocation = "3".repeat(32);
modified.binding.operation_key = modified.operation_key().unwrap();
assert!(parse_protected_request(&bytes(&modified), &trusted).is_err());
let mut with_newline = trusted.clone();
with_newline.push(b'\n');
assert!(parse_protected_request(&with_newline, &trusted).is_err());
}
#[test]
fn nested_duplicate_unknown_fields_and_missing_keys_rejected() {
let r = request(Operation::Status { target: target() });
let b = r.canonical_bytes().unwrap();
let s = String::from_utf8(b).unwrap();
assert!(parse_request(
s.replace("\"generation\":1", "\"generation\":1,\"generation\":1")
.as_bytes()
)
.is_err());
let v = serde_json::to_value(r).unwrap();
for path in ["binding", "operation"] {
let mut bad = v.clone();
bad[path]["extra"] = json!(true);
assert!(parse_request(&bytes(&bad)).is_err());
}
let mut bad = v;
bad["binding"]
.as_object_mut()
.unwrap()
.remove("operation_key");
assert!(parse_request(&bytes(&bad)).is_err());
}
#[test]
fn valid_outcomes_and_only_exact_operation_pairs() {
let pairs = [
(
Operation::ResolveImage {
image: "repo:tag".into(),
},
Outcome::Image {
image_id: object(ContainerState::Created).image_id,
repo_digests: vec![],
},
),
(
Operation::Create {
name: target().name,
image_id: object(ContainerState::Created).image_id,
labels: labels(),
environment: vec![],
container_port: 1234,
},
Outcome::Created {
object: object(ContainerState::Created),
},
),
(
Operation::Status { target: target() },
Outcome::Status {
object: object(ContainerState::Running),
},
),
(
Operation::Start { target: target() },
Outcome::Started {
object: object(ContainerState::Running),
},
),
(
Operation::Stop {
target: target(),
grace_seconds: 10,
},
Outcome::Stopped {
object: object(ContainerState::Exited),
},
),
(Operation::Remove { target: target() }, Outcome::Removed),
(
Operation::HealthExec {
target: target(),
argv: vec!["check".into()],
timeout_ms: 100,
},
Outcome::Health {
exec_id: "f".repeat(64),
exit_code: 7,
object: object(ContainerState::Running),
},
),
];
for (i, (op, outcome)) in pairs.iter().enumerate() {
let r = request(op.clone());
assert!(parse_response(&bytes(&response(&r, outcome.clone())), &r).is_ok());
for (j, (_, other)) in pairs.iter().enumerate() {
if i != j {
assert!(
response(&r, other.clone()).validate(&r).is_err(),
"pair {i}/{j}"
);
}
}
}
}
#[test]
fn absence_requires_exact_route_message_identity_observations_and_status_operation() {
let r = request(Operation::Status { target: target() });
let ok = response(&r, absent());
assert!(parse_response(&bytes(&ok), &r).is_ok());
let value = serde_json::to_value(&ok).unwrap();
for (field, replacement) in [
("route", json!("/v1.47/info")),
("object_id", json!("b".repeat(64))),
("http_status", json!(500)),
("error_message_sha256", json!("a".repeat(64))),
] {
let mut bad = value.clone();
bad["outcome"]["evidence"][field] = replacement;
assert!(parse_response(&bytes(&bad), &r).is_err());
}
let remove = request(Operation::Remove { target: target() });
assert!(response(&remove, absent()).validate(&remove).is_err());
let mut wrong = ok.clone();
wrong
.observations
.as_mut()
.unwrap()
.observed_after
.daemon_id = "other".into();
assert!(wrong.validate(&r).is_err());
wrong = ok;
wrong.observations = None;
assert!(wrong.validate(&r).is_err());
}
#[test]
fn failures_require_explicit_null_observations_and_do_not_claim_success() {
let r = request(Operation::Remove { target: target() });
for outcome in [
Outcome::Unknown {
reason: Reason::Transport,
},
Outcome::RejectedBeforeEffect {
reason: Reason::InvalidInput,
},
] {
let mut out = response(&r, outcome);
assert!(out.validate(&r).is_err());
out.observations = None;
assert!(parse_response(&bytes(&out), &r).is_ok());
let mut v = serde_json::to_value(out).unwrap();
v.as_object_mut().unwrap().remove("observations");
assert!(parse_response(&bytes(&v), &r).is_err());
}
}
#[test]
fn remove_claim_rejects_extra_fields() {
let r = request(Operation::Remove { target: target() });
let mut v = serde_json::to_value(response(&r, Outcome::Removed)).unwrap();
v["outcome"]["absence"] = json!(true);
assert!(parse_response(&bytes(&v), &r).is_err());
}
#[test]
fn target_labels_image_pin_and_state_mismatches_rejected() {
let r = request(Operation::Start { target: target() });
for obj in [
object(ContainerState::Exited),
Object {
object_id: "b".repeat(64),
..object(ContainerState::Running)
},
Object {
labels: vec![],
..object(ContainerState::Running)
},
] {
assert!(response(&r, Outcome::Started { object: obj })
.validate(&r)
.is_err());
}
let pin = format!("repo@sha256:{}", "f".repeat(64));
let r = request(Operation::ResolveImage { image: pin.clone() });
assert!(response(
&r,
Outcome::Image {
image_id: object(ContainerState::Created).image_id.clone(),
repo_digests: vec![]
}
)
.validate(&r)
.is_err());
assert!(response(
&r,
Outcome::Image {
image_id: object(ContainerState::Created).image_id,
repo_digests: vec![pin]
}
)
.validate(&r)
.is_ok());
}
#[test]
fn health_invalid_bounds_and_noninteger_exit_rejected() {
for op in [
Operation::HealthExec {
target: target(),
argv: vec![],
timeout_ms: 100,
},
Operation::HealthExec {
target: target(),
argv: vec!["check".into()],
timeout_ms: 30001,
},
Operation::Stop {
target: target(),
grace_seconds: 0,
},
] {
assert!(request(op).validate().is_err());
}
let r = request(Operation::HealthExec {
target: target(),
argv: vec!["check".into()],
timeout_ms: 100,
});
let mut v = serde_json::to_value(response(
&r,
Outcome::Health {
exec_id: "f".repeat(64),
exit_code: 0,
object: object(ContainerState::Running),
},
))
.unwrap();
v["outcome"]["exit_code"] = Value::Null;
assert!(parse_response(&bytes(&v), &r).is_err());
}
#[test]
fn ports_are_bound_to_requested_service_not_merely_loopback() {
let r = request(Operation::Status { target: target() });
let good = object(ContainerState::Running);
assert!(response(
&r,
Outcome::Status {
object: good.clone()
}
)
.validate(&r)
.is_ok());
for port in [
Port {
container_port: 9999,
..good.ports[0].clone()
},
Port {
host_ip: "0.0.0.0".into(),
..good.ports[0].clone()
},
Port {
host_port: 0,
..good.ports[0].clone()
},
Port {
protocol: "udp".into(),
..good.ports[0].clone()
},
] {
let bad = Object {
ports: vec![port],
..good.clone()
};
assert!(response(&r, Outcome::Status { object: bad })
.validate(&r)
.is_err());
}
let duplicate = Object {
ports: vec![good.ports[0].clone(); 2],
..good.clone()
};
assert!(response(&r, Outcome::Status { object: duplicate })
.validate(&r)
.is_err());
let empty = Object {
ports: vec![],
..good
};
assert!(response(
&r,
Outcome::Status {
object: empty.clone()
}
)
.validate(&r)
.is_ok());
let start = request(Operation::Start { target: target() });
assert!(response(
&start,
Outcome::Started {
object: empty.clone()
}
)
.validate(&start)
.is_err());
let health = request(Operation::HealthExec {
target: target(),
argv: vec!["check".into()],
timeout_ms: 100,
});
assert!(response(
&health,
Outcome::Health {
exec_id: "f".repeat(64),
exit_code: 0,
object: empty
}
)
.validate(&health)
.is_err());
}
#[test]
fn zero_target_port_is_not_authority_and_port_changes_change_digest() {
let original = request(Operation::Status { target: target() });
let mut changed = target();
changed.container_port = 9999;
let changed = request(Operation::Status { target: changed });
assert_ne!(
original.binding.operation_key,
changed.binding.operation_key
);
assert!(parse_protected_request(
&changed.canonical_bytes().unwrap(),
&original.canonical_bytes().unwrap()
)
.is_err());
let mut zero = target();
zero.container_port = 0;
assert!(request(Operation::Status { target: zero })
.validate()
.is_err());
}