use super::unit_named;
use crate::parse_str;
use crate::quadlet::generate_at;
#[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_at(&file, "proj", std::path::Path::new("/srv/app"));
assert!(!out
.units
.iter()
.any(|u| u.filename == "proj-extnet.network"));
assert!(!out.units.iter().any(|u| u.filename == "proj-extvol.volume"));
let c = &unit_named(&out, "proj-web.container").contents;
assert!(c.contains("Network=extnet"));
assert!(!c.contains("Network=proj-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_at(&file, "p", std::path::Path::new("/srv/app"));
let c = &unit_named(&out, "p-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_at(&file, "p", std::path::Path::new("/srv/app"));
let net = &unit_named(&out, "p-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, "p-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_at(&file, "p", std::path::Path::new("/srv/app"));
let net = &unit_named(&out, "p-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_at(&file, "p", std::path::Path::new("/srv/app"));
let vol = &unit_named(&out, "p-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_at(&file, "p", std::path::Path::new("/srv/app"));
let c = &unit_named(&out, "p-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_at(&file, "proj", std::path::Path::new("/srv/app"));
let web = &unit_named(&out, "proj-web.container").contents;
assert!(web.contains("After=proj-db_1.service"), "in:\n{web}");
assert!(web.contains("Requires=proj-db_1.service"), "in:\n{web}");
assert!(!web.contains("db:1.service"), "in:\n{web}");
unit_named(&out, "proj-db_1.container");
}
#[test]
fn network_mode_service_maps_to_dot_container() {
let yaml = "services:\n s:\n image: x\n network_mode: \"service:db\"\n";
let file = parse_str(yaml).unwrap();
let out = generate_at(&file, "p", std::path::Path::new("/srv/app"));
let c = &unit_named(&out, "p-s.container").contents;
assert!(
c.contains("Network=p-db.container"),
"service:db must map to Network=p-db.container; got:\n{c}"
);
}
#[test]
fn network_mode_container_maps_to_join_form() {
let yaml = "services:\n s:\n image: x\n network_mode: \"container:sidecar\"\n";
let file = parse_str(yaml).unwrap();
let out = generate_at(&file, "p", std::path::Path::new("/srv/app"));
let c = &unit_named(&out, "p-s.container").contents;
assert!(
c.contains("Network=container:sidecar"),
"container:sidecar must map to the join form; got:\n{c}"
);
assert!(
!c.contains("sidecar.container"),
"container: must not emit a .container unit dependency; got:\n{c}"
);
}
#[test]
fn duplicate_network_aliases_are_emitted_once() {
let yaml = "services:\n s:\n image: x\n networks:\n front:\n aliases: [dup, dup, uniq]\nnetworks:\n front:\n";
let file = parse_str(yaml).unwrap();
let out = generate_at(&file, "p", std::path::Path::new("/srv/app"));
let c = &unit_named(&out, "p-s.container").contents;
assert_eq!(
c.matches("NetworkAlias=dup").count(),
1,
"duplicate alias must be emitted once; got:\n{c}"
);
assert_eq!(c.matches("NetworkAlias=uniq").count(), 1, "in:\n{c}");
}
#[test]
fn ipam_options_warn_because_quadlet_cannot_emit_them() {
let yaml = "networks:\n net:\n ipam:\n options:\n foo: bar\nservices:\n s:\n image: x\n networks: [net]\n";
let file = parse_str(yaml).unwrap();
let out = generate_at(&file, "p", std::path::Path::new("/srv/app"));
assert!(
out.warnings.iter().any(|w| w.contains("ipam.options")),
"expected an ipam.options warning; got: {:?}",
out.warnings
);
}
#[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_at(&file, "p", std::path::Path::new("/srv/app"));
let c = &unit_named(&out, "p-s.container").contents;
assert!(c.contains("Network=p-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_at(&file, "p", std::path::Path::new("/srv/app"));
let net = &unit_named(&out, "p-net.network").contents;
let vol = &unit_named(&out, "p-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, "p-s.container").contents;
assert!(c.contains("[Install]") && c.contains("WantedBy=default.target"));
}