use anyhow::Context;
use crate::Result;
use crate::suite::resolve::{resolve_path_from_repo_root, write_file};
use crate::suite::runtime::{resolve_gql_url, resolve_graphql_bearer_token};
use crate::suite::schema::SuiteCleanupStep;
use super::template::{parse_vars_map, render_template};
use super::{CleanupContext, append_log, log_failure_with_error, read_json_file};
pub(super) fn graphql_cleanup_step(
ctx: &mut CleanupContext<'_>,
response_json: &serde_json::Value,
step: &SuiteCleanupStep,
step_index: usize,
) -> Result<bool> {
let op = step.op.trim();
if op.is_empty() {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) runner failed: step[{step_index}] op=<missing>"),
)?;
return Ok(false);
}
let op_abs = resolve_path_from_repo_root(ctx.repo_root, op);
if !op_abs.is_file() {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) runner failed: step[{step_index}] op not found: {op}"),
)?;
return Ok(false);
}
let cleanup_config_dir = if !step.config_dir.trim().is_empty() {
step.config_dir.trim()
} else if !ctx.gql_config_dir.trim().is_empty() {
ctx.gql_config_dir.trim()
} else {
ctx.suite_defaults.graphql.config_dir.trim()
};
let cleanup_jwt = if !step.jwt.trim().is_empty() {
step.jwt.trim().to_string()
} else if !ctx.gql_jwt.trim().is_empty() {
ctx.gql_jwt.trim().to_string()
} else if !ctx.rest_token.trim().is_empty() {
ctx.rest_token.trim().to_string()
} else {
ctx.suite_defaults.graphql.jwt.trim().to_string()
};
let cleanup_url_override = if !step.url.trim().is_empty() {
step.url.trim()
} else if !ctx.gql_url.trim().is_empty() {
ctx.gql_url.trim()
} else {
""
};
let cleanup_env = if !step.env.trim().is_empty() {
step.env.trim()
} else {
ctx.effective_env
};
let endpoint_url = match resolve_gql_url(
ctx.repo_root,
cleanup_config_dir,
cleanup_url_override,
cleanup_env,
ctx.suite_defaults,
ctx.env_gql_url,
) {
Ok(v) => v,
Err(err) => {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) runner failed: step[{step_index}] {err}"),
)?;
return Ok(false);
}
};
let allow_errors = step.allow_errors;
let expect_jq = if !step.expect_jq.trim().is_empty() {
step.expect_jq.trim().to_string()
} else {
step.expect
.as_ref()
.map(|e| e.jq.trim().to_string())
.unwrap_or_default()
};
if allow_errors && expect_jq.trim().is_empty() {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) expect.jq missing: step[{step_index}]"),
)?;
return Ok(false);
}
let mut access_token = ctx.access_token_for_case.trim().to_string();
if let Some(auth) = ctx.auth_manager.as_deref_mut()
&& !cleanup_jwt.trim().is_empty()
{
match auth.ensure_token(
&cleanup_jwt,
ctx.repo_root,
ctx.suite_defaults,
ctx.env_rest_url,
ctx.env_gql_url,
) {
Ok(t) => access_token = t,
Err(err) => {
if access_token.is_empty() {
append_log(
ctx.main_stderr_file,
&format!(
"cleanup(graphql) auth failed: step[{step_index}] profile={cleanup_jwt}"
),
)?;
append_log(ctx.main_stderr_file, &err)?;
return Ok(false);
}
}
}
}
let setup_dir_abs = resolve_path_from_repo_root(ctx.repo_root, cleanup_config_dir);
let mut auth_stderr: Vec<u8> = Vec::new();
let bearer_token = if !access_token.is_empty() {
Some(access_token)
} else {
match resolve_graphql_bearer_token(
&setup_dir_abs,
&endpoint_url,
&op_abs,
&cleanup_jwt,
&mut auth_stderr,
) {
Ok(v) => v,
Err(err) => {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) runner failed: step[{step_index}] {err}"),
)?;
return Ok(false);
}
}
};
let vars_json: Option<serde_json::Value> = if !step.vars_jq.trim().is_empty() {
let out = crate::jq::query(response_json, step.vars_jq.trim()).ok();
let out = out.unwrap_or_default();
if out.len() != 1 || !out[0].is_object() {
append_log(
ctx.main_stderr_file,
&format!(
"cleanup(graphql) varsJq failed: step[{step_index}] varsJq={}",
step.vars_jq.trim()
),
)?;
return Ok(false);
}
Some(out[0].clone())
} else if !step.vars_template.trim().is_empty() {
let template_path = resolve_path_from_repo_root(ctx.repo_root, step.vars_template.trim());
if !template_path.is_file() {
append_log(
ctx.main_stderr_file,
&format!(
"cleanup(graphql) varsTemplate render failed: step[{step_index}] template={}",
step.vars_template.trim()
),
)?;
return Ok(false);
}
let template_text = std::fs::read_to_string(&template_path)
.with_context(|| format!("read varsTemplate: {}", template_path.display()))?;
let vars_map = parse_vars_map(step.vars.as_ref())?;
let rendered = match render_template(&template_text, response_json, &vars_map) {
Ok(v) => v,
Err(_) => {
append_log(
ctx.main_stderr_file,
&format!(
"cleanup(graphql) varsTemplate render failed: step[{step_index}] template={}",
step.vars_template.trim()
),
)?;
return Ok(false);
}
};
let v: serde_json::Value = serde_json::from_str(&rendered)
.map_err(|_| anyhow::anyhow!("cleanup(graphql) varsTemplate rendered invalid JSON"))?;
if !v.is_object() {
append_log(
ctx.main_stderr_file,
&format!(
"cleanup(graphql) varsTemplate render failed: step[{step_index}] template={}",
step.vars_template.trim()
),
)?;
return Ok(false);
}
Some(v)
} else if let Some(v) = step.vars.as_ref() {
if v.is_null() {
None
} else if let Some(path) = v.as_str() {
let abs = resolve_path_from_repo_root(ctx.repo_root, path);
if !abs.is_file() {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) vars not found: step[{step_index}] {path}"),
)?;
return Ok(false);
}
Some(read_json_file(&abs)?)
} else {
append_log(
ctx.main_stderr_file,
"cleanup(graphql) vars must be a file path string",
)?;
return Ok(false);
}
} else {
None
};
let cleanup_stdout_file = ctx.run_dir.join(format!(
"{}.cleanup.{step_index}.response.json",
ctx.safe_id
));
let cleanup_stderr_file = ctx
.run_dir
.join(format!("{}.cleanup.{step_index}.stderr.log", ctx.safe_id));
write_file(&cleanup_stdout_file, b"")?;
write_file(&cleanup_stderr_file, b"")?;
if !auth_stderr.is_empty() {
write_file(&cleanup_stderr_file, &auth_stderr)?;
}
let op_file = crate::graphql::schema::GraphqlOperationFile::load(&op_abs)
.context("load graphql cleanup op")?;
let executed = match crate::graphql::runner::execute_graphql_request(
&endpoint_url,
bearer_token.as_deref(),
&op_file.operation,
vars_json.as_ref(),
) {
Ok(v) => v,
Err(err) => {
log_failure_with_error(
ctx.main_stderr_file,
&format!("cleanup(graphql) runner failed: step[{step_index}] rc=1 op={op}"),
&err,
)?;
return Ok(false);
}
};
write_file(&cleanup_stdout_file, &executed.response.body)?;
let response_json2: serde_json::Value = match serde_json::from_slice(&executed.response.body) {
Ok(v) => v,
Err(_) => {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) runner failed: step[{step_index}] invalid JSON"),
)?;
return Ok(false);
}
};
let errors_present = response_json2
.get("errors")
.and_then(|v| v.as_array())
.map(|a| !a.is_empty())
.unwrap_or(false);
if errors_present && !allow_errors {
append_log(
ctx.main_stderr_file,
&format!("cleanup(graphql) errors present: step[{step_index}] op={op}"),
)?;
return Ok(false);
}
if !expect_jq.trim().is_empty() {
let ok = crate::jq::eval_exit_status(&response_json2, expect_jq.trim()).unwrap_or(false);
if !ok {
append_log(
ctx.main_stderr_file,
&format!(
"cleanup(graphql) expect.jq failed: step[{step_index}] {}",
expect_jq.trim()
),
)?;
return Ok(false);
}
}
Ok(true)
}