use super::unit_named;
use crate::parse_str;
use crate::quadlet::{generate, QuadletOutput, QuadletUnit};
#[test]
fn duplicate_filename_detects_collision() {
let mk = |n: &str| QuadletUnit {
filename: n.to_string(),
contents: String::new(),
};
let mut out = QuadletOutput {
units: vec![mk("web.container"), mk("db.volume")],
..Default::default()
};
assert_eq!(out.duplicate_filename(), None);
out.units.push(mk("web.container"));
assert_eq!(out.duplicate_filename(), Some("web.container"));
}
#[test]
fn generates_container_network_and_volume_units() {
let yaml = r#"
services:
web:
image: nginx:1.27
container_name: web
ports:
- "8080:80"
environment:
B_KEY: two
A_KEY: one
volumes:
- data:/var/lib/data
networks:
- frontend
restart: unless-stopped
depends_on:
- db
db:
image: postgres:16
volumes:
data:
networks:
frontend:
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
let web = unit_named(&out, "web.container");
assert!(web.contents.contains("Image=nginx:1.27"));
assert!(web.contents.contains("ContainerName=web"));
assert!(web.contents.contains("PublishPort=8080:80"));
let a = web.contents.find("Environment=A_KEY=one").unwrap();
let b = web.contents.find("Environment=B_KEY=two").unwrap();
assert!(a < b, "environment keys must be sorted");
assert!(web.contents.contains("Volume=data.volume:/var/lib/data"));
assert!(web.contents.contains("Network=frontend.network"));
assert!(web.contents.contains("Restart=always"));
assert!(web.contents.contains("After=db.service"));
assert!(web.contents.contains("WantedBy=default.target"));
unit_named(&out, "db.container");
assert!(unit_named(&out, "data.volume")
.contents
.contains("VolumeName=proj_data"));
assert!(unit_named(&out, "frontend.network")
.contents
.contains("NetworkName=proj_frontend"));
}
#[test]
fn warns_about_unmapped_build_field() {
let yaml = r#"
services:
app:
build: .
image: app:latest
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
assert!(
out.warnings.iter().any(|w| w.contains("build")),
"a set build field must produce a warning"
);
}
#[test]
fn bind_path_volume_is_passed_through() {
let yaml = r#"
services:
web:
image: nginx
volumes:
- ./html:/usr/share/nginx/html:ro
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
let web = unit_named(&out, "web.container");
assert!(web
.contents
.contains("Volume=./html:/usr/share/nginx/html:ro"));
}
#[test]
fn long_form_volume_with_named_source_and_readonly() {
let yaml = r#"
services:
db:
image: postgres
volumes:
- type: volume
source: pgdata
target: /var/lib/postgresql/data
read_only: true
volumes:
pgdata:
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
let c = &unit_named(&out, "db.container").contents;
assert!(c.contains("Volume=pgdata.volume:/var/lib/postgresql/data:ro"));
}
#[test]
fn warns_for_every_unmapped_field() {
let yaml = r#"
services:
s:
image: x
network_mode: "bridge:custom"
profiles: [debug]
volumes_from:
- other
deploy:
replicas: 3
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let joined = out.warnings.join("\n");
for needle in ["network_mode", "profiles", "volumes_from", "scale/replicas"] {
assert!(joined.contains(needle), "expected warning for {needle}");
}
}
#[test]
fn hostile_service_name_cannot_escape_output_directory() {
let yaml = "services:\n ? \"../../evil\"\n : { image: x }\n";
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
let unit = &out.units[0];
assert!(
!unit.filename.contains('/') && !unit.filename.contains('\\'),
"unit file name must be a single safe component, got {}",
unit.filename
);
assert!(unit.filename.ends_with(".container"));
}
#[test]
fn newline_in_value_cannot_inject_unit_directives() {
let yaml =
"services:\n web:\n image: x\n environment:\n EVIL: \"a\\nExecStartPre=/bin/rm -rf /\"\n";
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
let c = &unit_named(&out, "web.container").contents;
assert!(
!c.lines().any(|l| l.starts_with("ExecStartPre")),
"a newline in a value must not inject a directive line:\n{c}"
);
}
#[test]
fn external_network_and_volume_emit_no_unit_and_use_bare_name() {
let yaml = r#"
services:
web:
image: nginx
networks:
- extnet
volumes:
- extvol:/data
networks:
extnet:
external: true
volumes:
extvol:
external: true
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
assert!(!out.units.iter().any(|u| u.filename == "extnet.network"));
assert!(!out.units.iter().any(|u| u.filename == "extvol.volume"));
let c = &unit_named(&out, "web.container").contents;
assert!(c.contains("Network=extnet"));
assert!(!c.contains("Network=extnet.network"));
assert!(c.contains("Volume=extvol:/data"));
assert!(!c.contains("extvol.volume"));
}
#[test]
fn long_form_bind_selinux_and_propagation_preserved() {
let yaml = r#"
services:
s:
image: x
volumes:
- type: bind
source: /host/data
target: /data
bind:
selinux: z
propagation: rshared
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let c = &unit_named(&out, "s.container").contents;
assert!(
c.contains("Volume=/host/data:/data:z,rshared"),
"selinux/propagation must be preserved; got:\n{c}"
);
}
#[test]
fn network_and_volume_units_carry_config_keys() {
let yaml = r#"
services:
s:
image: x
networks:
net:
driver: bridge
internal: true
enable_ipv6: true
labels:
tier: net
volumes:
vol:
driver: local
labels:
tier: vol
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let net = &unit_named(&out, "net.network").contents;
assert!(net.contains("Driver=bridge"));
assert!(net.contains("Internal=true"));
assert!(net.contains("IPv6=true"));
assert!(net.contains("Label=tier=net"));
let vol = &unit_named(&out, "vol.volume").contents;
assert!(vol.contains("Driver=local"));
assert!(vol.contains("Label=tier=vol"));
}
#[test]
fn network_unit_emits_ipam_pools_options_and_custom_name() {
let yaml = r#"
services:
s:
image: x
networks:
net:
name: custom-net
driver_opts:
mtu: "9000"
com.docker.network.bridge.name: br0
ipam:
driver: host-local
config:
- subnet: 10.7.0.0/16
gateway: 10.7.0.1
ip_range: 10.7.0.128/25
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let net = &unit_named(&out, "net.network").contents;
assert!(net.contains("NetworkName=custom-net"), "in:\n{net}");
assert!(!net.contains("NetworkName=p_net"), "in:\n{net}");
assert!(net.contains("IPAMDriver=host-local"), "in:\n{net}");
assert!(net.contains("Subnet=10.7.0.0/16"), "in:\n{net}");
assert!(net.contains("Gateway=10.7.0.1"), "in:\n{net}");
assert!(net.contains("IPRange=10.7.0.128/25"), "in:\n{net}");
assert!(net.contains("Options=mtu=9000"), "in:\n{net}");
assert!(
net.contains("Options=com.docker.network.bridge.name=br0"),
"in:\n{net}"
);
}
#[test]
fn volume_unit_emits_options_and_custom_name() {
let yaml = r#"
services:
s:
image: x
volumes:
vol:
name: custom-vol
driver: local
driver_opts:
type: nfs
device: ":/exports"
o: "addr=10.0.0.1,rw"
custom: extra
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let vol = &unit_named(&out, "vol.volume").contents;
assert!(vol.contains("VolumeName=custom-vol"), "in:\n{vol}");
assert!(!vol.contains("VolumeName=p_vol"), "in:\n{vol}");
assert!(vol.contains("Type=nfs"), "in:\n{vol}");
assert!(vol.contains("Device=:/exports"), "in:\n{vol}");
assert!(vol.contains("Options=addr=10.0.0.1,rw"), "in:\n{vol}");
assert!(vol.contains("PodmanArgs=--opt custom=extra"), "in:\n{vol}");
}
#[test]
fn healthcheck_start_interval_is_warned_and_omitted() {
let yaml = r#"
services:
s:
image: x
healthcheck:
test: ["CMD", "true"]
interval: 5s
start_interval: 2s
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let c = &unit_named(&out, "s.container").contents;
assert!(c.contains("HealthInterval=5s"), "in:\n{c}");
assert!(
!c.contains("HealthStartupInterval"),
"start_interval must not emit HealthStartupInterval=; got:\n{c}"
);
let joined = out.warnings.join("\n");
assert!(
joined.contains("start_interval"),
"start_interval must warn; got:\n{joined}"
);
}
#[test]
fn dependency_unit_names_are_sanitized_in_ordering() {
let yaml = r#"
services:
web:
image: nginx
depends_on:
- "db:1"
? "db:1"
: { image: postgres }
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "proj");
let web = &unit_named(&out, "web.container").contents;
assert!(web.contains("After=db_1.service"), "in:\n{web}");
assert!(web.contains("Requires=db_1.service"), "in:\n{web}");
assert!(!web.contains("db:1.service"), "in:\n{web}");
unit_named(&out, "db_1.container");
}
#[test]
fn network_mode_service_and_container_map_to_dot_container() {
for (mode, target) in [("service:db", "db"), ("container:sidecar", "sidecar")] {
let yaml = format!("services:\n s:\n image: x\n network_mode: \"{mode}\"\n");
let file = parse_str(&yaml).unwrap();
let out = generate(&file, "p");
let c = &unit_named(&out, "s.container").contents;
assert!(
c.contains(&format!("Network={target}.container")),
"{mode} must map to Network={target}.container; got:\n{c}"
);
}
}
#[test]
fn network_mode_service_target_is_sanitized() {
let yaml = "services:\n s:\n image: x\n network_mode: \"service:web:1\"\n";
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let c = &unit_named(&out, "s.container").contents;
assert!(c.contains("Network=web_1.container"), "in:\n{c}");
}
#[test]
fn volume_and_network_units_have_no_install_section() {
let yaml = r#"
services:
s:
image: x
networks:
net:
volumes:
vol:
"#;
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let net = &unit_named(&out, "net.network").contents;
let vol = &unit_named(&out, "vol.volume").contents;
assert!(
!net.contains("[Install]") && !net.contains("WantedBy"),
".network must carry no [Install]; got:\n{net}"
);
assert!(
!vol.contains("[Install]") && !vol.contains("WantedBy"),
".volume must carry no [Install]; got:\n{vol}"
);
let c = &unit_named(&out, "s.container").contents;
assert!(c.contains("[Install]") && c.contains("WantedBy=default.target"));
}
#[test]
fn privileged_maps_to_podman_arg() {
let yaml = "services:\n s:\n image: x\n privileged: true\n";
let file = parse_str(yaml).unwrap();
let out = generate(&file, "p");
let c = &unit_named(&out, "s.container").contents;
assert!(c.contains("PodmanArgs=--privileged"), "in:\n{c}");
assert!(
!out.warnings.iter().any(|w| w.contains("privileged")),
"privileged must be mapped, not warned; got: {:?}",
out.warnings
);
}