use indoc::indoc;
use test_util::prelude::sim_assert_eq;
use super::*;
#[test]
#[expect(
clippy::too_many_lines,
reason = "the complete fixture scenario is clearest as one contiguous test"
)]
fn grouped_selector_receiver_is_optional_but_present_scalars_fail() {
let src = indoc! {r"
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
grouped: {{ (.Values.grouped.receiver).leaf | quote }}
{{- if .Values.strict.enabled }}
strict: {{ .Values.strict.receiver.leaf | quote }}
{{- end }}
"};
let values_yaml = indoc! {"
grouped: {}
strict:
enabled: false
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
let grouped_receiver_present = serde_json::json!({
"not": {
"anyOf": [
{
"not": {
"properties": {
"grouped": {
"properties": { "receiver": {} },
"required": ["receiver"],
"type": "object",
},
},
"required": ["grouped"],
"type": "object",
},
},
{
"properties": {
"grouped": {
"properties": { "receiver": { "enum": [null] } },
"required": ["receiver"],
"type": "object",
},
},
"required": ["grouped"],
"type": "object",
},
],
},
});
let strict_enabled = serde_json::json!({
"properties": {
"strict": {
"properties": { "enabled": { "$ref": "#/$defs/helm-truthy" } },
"required": ["enabled"],
"type": "object",
},
},
"required": ["strict"],
"type": "object",
});
let mut properties = serde_json::Map::new();
properties.insert(
"grouped".to_string(),
serde_json::json!({
"additionalProperties": {},
"properties": {
"receiver": {
"additionalProperties": {},
"properties": { "leaf": {} },
},
},
"type": "object",
}),
);
properties.insert(
"strict".to_string(),
serde_json::json!({
"additionalProperties": {},
"allOf": [{
"if": { "allOf": [
{
"properties": { "enabled": { "$ref": "#/$defs/helm-truthy" } },
"required": ["enabled"],
"type": "object",
},
{ "anyOf": [
{ "not": { "properties": { "receiver": {} },
"required": ["receiver"], "type": "object" } },
{ "properties": { "receiver": { "enum": [null] } },
"required": ["receiver"], "type": "object" },
] },
] },
"then": false,
}],
"properties": {
"enabled": {
"anyOf": [
{ "not": { "$ref": "#/$defs/helm-truthy" } },
{ "type": "boolean" },
],
},
"receiver": {
"additionalProperties": {},
"properties": { "leaf": {} },
},
},
"type": "object",
}),
);
let all_of = vec![
serde_json::json!({
"if": grouped_receiver_present,
"then": root_property_schema(
"grouped",
serde_json::json!({
"additionalProperties": {},
"properties": {
"receiver": { "anyOf": [{ "type": "object" }] },
},
}),
),
}),
serde_json::json!({
"if": strict_enabled,
"then": root_property_schema(
"strict",
serde_json::json!({
"additionalProperties": {},
"properties": {
"receiver": { "anyOf": [{ "type": "object" }] },
},
}),
),
}),
navigated_host_clause(&["grouped"]),
navigated_host_clause(&["strict"]),
];
for instance in [
serde_json::json!({ "grouped": {}, "strict": { "enabled": false } }),
serde_json::json!({ "grouped": { "receiver": null }, "strict": { "enabled": false } }),
serde_json::json!({ "grouped": { "receiver": {} }, "strict": { "enabled": false } }),
serde_json::json!({ "grouped": {}, "strict": { "enabled": false, "receiver": "skipped" } }),
serde_json::json!({
"grouped": {},
"strict": { "enabled": true, "receiver": {} }
}),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"absent/null grouped receivers and object receivers render: instance={instance}; schema={schema}"
);
}
for instance in [
serde_json::json!({
"grouped": { "receiver": "not-an-object" },
"strict": { "enabled": false }
}),
serde_json::json!({ "grouped": {}, "strict": { "enabled": true } }),
] {
assert!(
!schema_accepts_instance(&schema, &instance),
"present scalar grouped receivers and missing strict receivers fail: instance={instance}; schema={schema}"
);
}
sim_assert_eq!(
have: &schema,
want: &expected_values_schema(properties, all_of, true)
);
}
#[test]
fn present_key_guard_keeps_scalar_provider_schema_at_leaf() {
let src = indoc! {r#"
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
{{- if hasKey .Values.global "hostUsers" }}
hostUsers: {{ .Values.global.hostUsers }}
{{- end }}
containers:
- name: test
image: test
"#};
let schema = schema_for_values_yaml(parse_ir(src), Some("global: {}\n"));
for instance in [
serde_json::json!({ "global": {} }),
serde_json::json!({ "global": { "hostUsers": true } }),
serde_json::json!({ "global": { "hostUsers": false } }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"absent and boolean hostUsers values render: instance={instance}; schema={schema}"
);
}
assert!(
schema_accepts_instance(
&schema,
&serde_json::json!({ "global": { "hostUsers": "false" } })
),
"an unquoted Boolean string reparses to the provider's Boolean field: {schema}"
);
assert!(
!schema_accepts_instance(
&schema,
&serde_json::json!({ "global": { "hostUsers": "audit" } })
),
"a non-Boolean string cannot satisfy the provider field: {schema}"
);
}
#[test]
fn synthetic_member_parent_does_not_seed_unreferenced_values_siblings() {
let src = indoc! {r"
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
port: {{ .Values.master.containerPorts.redis | quote }}
"};
let values_yaml = indoc! {"
master:
containerPorts:
redis: 6379
unrelated:
imported: false
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
assert!(
schema
.pointer("/properties/master/properties/unrelated")
.is_none(),
"a requirement-only parent must not seed an unconsumed sibling: {schema}"
);
assert!(
schema
.pointer("/properties/master/properties/containerPorts/properties/redis")
.is_some(),
"the genuinely consumed descendant must remain represented: {schema}"
);
}
#[test]
fn member_local_guard_does_not_leak_its_string_contract() {
let src = indoc! {r"
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
output: |-
{{- range $item := .Values.items }}
{{- if $item.enabled }}
{{ tpl $item.template $ }}
{{- end }}
{{- end }}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some("items: []\n"));
let open_member = serde_json::json!({
"additionalProperties": {},
"properties": { "enabled": {} },
});
let object_member = serde_json::json!({
"additionalProperties": {},
"properties": { "enabled": {} },
"type": "object",
});
let mut properties = serde_json::Map::new();
properties.insert(
"items".to_string(),
serde_json::json!({
"anyOf": [
{ "items": open_member, "type": "array" },
{ "items": object_member.clone(), "type": "array" },
{ "type": "integer" },
{ "type": "null" },
{ "additionalProperties": object_member.clone(), "type": "object" },
]
}),
);
let all_of = vec![serde_json::json!({
"additionalProperties": {},
"properties": {
"items": {
"anyOf": [
{ "items": object_member.clone(), "type": "array" },
{
"additionalProperties": object_member,
"type": "object",
},
{ "maximum": 0, "type": "integer" },
{ "type": "null" },
]
}
},
})];
sim_assert_eq!(
have: &schema,
want: &expected_values_schema(properties, all_of, false)
);
for instance in [
serde_json::json!({ "items": [{ "enabled": false, "template": 7 }] }),
serde_json::json!({ "items": [{ "enabled": true, "template": "body" }] }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"dead member consumers and live strings remain valid: instance={instance}; schema={schema}"
);
}
}
#[test]
fn nested_with_chain_range_keeps_falsy_ancestors_valid() {
let src = indoc! {r"
apiVersion: apps/v1
kind: Deployment
metadata:
name: d
spec:
template:
spec:
{{- with .Values.affinity }}
affinity:
{{- with .podAffinity }}
podAffinity:
{{- with .preferredDuringSchedulingIgnoredDuringExecution }}
preferredDuringSchedulingIgnoredDuringExecution:
{{- range . }}
- weight: {{ .weight }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
"};
let schema = schema_for_values_yaml(
parse_ir(src),
Some(indoc! {"
affinity: {}
"}),
);
for instance in [
serde_json::json!({ "affinity": false }),
serde_json::json!({ "affinity": 0 }),
serde_json::json!({ "affinity": "" }),
serde_json::json!({ "affinity": {} }),
serde_json::json!({ "affinity": {
"podAffinity": { "preferredDuringSchedulingIgnoredDuringExecution": [{ "weight": 1 }] }
} }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"falsy ancestors are skipped by the with chain and valid lists render: instance={instance}; schema={schema}"
);
}
assert!(
!schema_accepts_instance(
&schema,
&serde_json::json!({ "affinity": {
"podAffinity": { "preferredDuringSchedulingIgnoredDuringExecution": "audit" }
} }),
),
"a live truthy non-iterable still fails the range: {schema}"
);
}
#[test]
fn nested_member_range_keeps_map_lane_in_member_arm() {
let src = indoc! {r#"
{{- if typeIs "[]interface {}" .Values.locations }}
{{- range .Values.locations }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .name | default "d" }}
{{- with .annotations }}
annotations:
{{- range $key, $value := . }}
{{- $key | nindent 4 }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
"#};
let values_yaml = indoc! {"
locations:
- name:
annotations: {}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
for instance in [
serde_json::json!({ "locations": [{ "name": "d", "annotations": {} }] }),
serde_json::json!({ "locations": [{ "name": "d", "annotations": { "a": "b" } }] }),
serde_json::json!({ "locations": "ignored" }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"map-form annotations render and non-lists skip the typeIs branch: instance={instance}; schema={schema}"
);
}
assert!(
!schema_accepts_instance(&schema, &serde_json::json!({ "locations": [7] })),
"a scalar item fails the member reads inside the range: {schema}"
);
}
#[test]
fn header_member_read_requires_an_object_host_beside_body_dispatch() {
let src = indoc! {r#"
{{- if and .Values.webhook.create .Values.webhook.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: test
spec:
{{- if hasKey .Values.webhook.podDisruptionBudget "maxUnavailable" }}
maxUnavailable: {{ .Values.webhook.podDisruptionBudget.maxUnavailable }}
{{- else if hasKey .Values.webhook.podDisruptionBudget "minAvailable" }}
minAvailable: {{ .Values.webhook.podDisruptionBudget.minAvailable }}
{{- end }}
{{- end }}
"#};
let schema = schema_for_values_yaml(
parse_ir(src),
Some(indoc! {"
webhook:
create: true
podDisruptionBudget:
enabled: false
minAvailable: 1
"}),
);
for (instance, want) in [
(
serde_json::json!({ "webhook": { "create": true, "podDisruptionBudget": 7 } }),
false,
),
(
serde_json::json!({ "webhook": { "create": true, "podDisruptionBudget": [1] } }),
false,
),
(
serde_json::json!({ "webhook": { "podDisruptionBudget": { "enabled": false } } }),
true,
),
] {
assert!(
schema_accepts_instance(&schema, &instance) == want,
"instance={instance}; schema={schema}"
);
}
}
#[test]
fn nil_safe_grouped_receiver_with_declared_default_admits_null() {
let src = indoc! {r"
apiVersion: v1
kind: ConfigMap
metadata:
name: test
{{- with ((.Values.global).labels) }}
labels:
{{- toYaml . | nindent 4 }}
{{- end }}
data: {}
"};
let values_yaml = indoc! {"
global:
labels: {}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
sim_assert_eq!(
have: schema.pointer("/properties/global/type") == Some(&serde_json::json!("object")),
want: false,
"declared-default base must not pin bare `type: object`: {schema}",
);
for (instance, want) in [
(serde_json::json!({ "global": null }), true),
(serde_json::json!({}), true),
(serde_json::json!({ "global": {} }), true),
(
serde_json::json!({ "global": { "labels": { "a": "b" } } }),
true,
),
(serde_json::json!({ "global": 42 }), false),
(serde_json::json!({ "global": "oops" }), false),
(serde_json::json!({ "global": false }), false),
] {
assert!(
schema_accepts_instance(&schema, &instance) == want,
"instance={instance}; want={want}; schema={schema}"
);
}
}
#[test]
fn navigated_hosts_must_exist_in_the_coalesced_document() {
let src = indoc! {r"
{{- if .Values.apiService.create }}
apiVersion: v1
kind: Service
metadata:
name: x
{{- end }}
---
{{- if .Values.rbac.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: y
{{- end }}
---
{{- if (.Values.nilSafe).enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: n
{{- end }}
---
{{- with .Values.gated }}
{{- if .enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: g
{{- end }}
{{- end }}
"};
let values_yaml = indoc! {"
apiService:
create: true
rbac:
serviceAccount:
create: true
nilSafe: {}
gated: {}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
let composed = serde_json::json!({
"apiService": { "create": true },
"rbac": { "serviceAccount": { "create": true } },
"nilSafe": {},
"gated": {},
});
let without = |path: &[&str]| {
let mut instance = composed.clone();
let mut node = &mut instance;
let Some((leaf, parents)) = path.split_last() else {
return instance;
};
for segment in parents {
node = &mut node[*segment];
}
if let Some(object) = node.as_object_mut() {
object.remove(*leaf);
}
instance
};
for (instance, want, label) in [
(composed.clone(), true, "the coalesced defaults render"),
(
without(&["apiService"]),
false,
"a deleted top-level host aborts the header read",
),
(
without(&["rbac"]),
false,
"a deleted host ancestor aborts the chained read",
),
(
without(&["rbac", "serviceAccount"]),
false,
"a deleted nested host aborts, default-supplied or not",
),
(
without(&["nilSafe"]),
true,
"a nil-safe grouped receiver renders when deleted",
),
(
without(&["gated"]),
true,
"a `with`-scoped host renders when deleted",
),
(
serde_json::json!({ "apiService": {}, "rbac": { "serviceAccount": { "create": true } },
"nilSafe": {}, "gated": {} }),
true,
"an empty host map reads its member as nil",
),
] {
assert!(
schema_accepts_instance(&schema, &instance) == want,
"navigated host presence ({label}): instance={instance}; want={want}; schema={schema}"
);
}
}
#[test]
fn overlaid_range_members_keep_their_member_contracts() {
let src = indoc! {r#"
{{- $services := .Values.additionalServices }}
{{- $services = set $services "default" (omit .Values.service "additionalServices") }}
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
{{- range $name, $service := $services }}
{{- if ne $service.enabled false }}
{{ $name }}: live
{{- end }}
{{- end }}
"#};
let values_yaml = indoc! {"
additionalServices: {}
service:
enabled: true
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
for (overrides, want) in [
(
serde_json::json!({ "additionalServices": { "audit": { "enabled": true } } }),
true,
),
(serde_json::json!({ "additionalServices": {} }), true),
(
serde_json::json!({ "additionalServices": { "audit": false } }),
false,
),
(
serde_json::json!({ "additionalServices": { "audit": "x" } }),
false,
),
(
serde_json::json!({ "additionalServices": { "audit": 7 } }),
false,
),
(
serde_json::json!({ "additionalServices": { "audit": [] } }),
false,
),
] {
let instance = composed_instance(values_yaml, overrides);
assert!(
schema_accepts_instance(&schema, &instance) == want,
"the overlaid range binds its member contracts: \
instance={instance}; want={want}; schema={schema}"
);
}
}