use crate::e2e::escape::{escape_elixir, sanitize_ident};
use crate::e2e::fixture::{Fixture, HttpFixture, ValidationErrorExpectation};
use std::fmt::Write as _;
use super::values::json_to_elixir;
use crate::e2e::codegen::client;
const FINCH_UNSUPPORTED_METHODS: &[&str] = &["TRACE", "CONNECT"];
const REQ_CONVENIENCE_METHODS: &[&str] = &["get", "post", "put", "patch", "delete", "head"];
pub(super) struct ElixirTestClientRenderer<'a> {
fixture_id: &'a str,
expected_status: u16,
}
impl<'a> client::TestClientRenderer for ElixirTestClientRenderer<'a> {
fn language_name(&self) -> &'static str {
"elixir"
}
fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
let escaped_description = description.replace('"', "\\\"");
let _ = writeln!(out, " describe \"{fn_name}\" do");
if skip_reason.is_some() {
let _ = writeln!(out, " @tag :skip");
}
let _ = writeln!(out, " test \"{escaped_description}\" do");
}
fn render_test_close(&self, out: &mut String) {
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
}
fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
let method = ctx.method.to_lowercase();
let mut opts: Vec<String> = vec!["finch: AlefE2EFinch".to_string()];
if let Some(body) = ctx.body {
let elixir_val = json_to_elixir(body);
opts.push(format!("json: {elixir_val}"));
}
if !ctx.headers.is_empty() {
let header_pairs: Vec<String> = ctx
.headers
.iter()
.map(|(k, v)| format!("{{\"{}\", \"{}\"}}", escape_elixir(k), escape_elixir(v)))
.collect();
opts.push(format!("headers: [{}]", header_pairs.join(", ")));
}
if !ctx.cookies.is_empty() {
let cookie_str = ctx
.cookies
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("; ");
opts.push(format!("headers: [{{\"cookie\", \"{}\"}}]", escape_elixir(&cookie_str)));
}
if !ctx.query_params.is_empty() {
let pairs: Vec<String> = ctx
.query_params
.iter()
.map(|(k, v)| {
let val_str = match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
format!("{{\"{}\", \"{}\"}}", escape_elixir(k), escape_elixir(&val_str))
})
.collect();
opts.push(format!("params: [{}]", pairs.join(", ")));
}
if (300..400).contains(&self.expected_status) {
opts.push("redirect: false".to_string());
}
let fixture_id = escape_elixir(self.fixture_id);
let sut_url_expr = "System.get_env(\"SUT_URL\") || mock_server_url()";
let url_expr = format!("({sut_url_expr}) <> \"/fixtures/{fixture_id}\"");
if REQ_CONVENIENCE_METHODS.contains(&method.as_str()) {
let opts_str = opts.join(", ");
let _ = writeln!(
out,
" {{:ok, response}} = Req.{method}(url: {url_expr}, {opts_str})"
);
} else {
opts.insert(0, format!("method: :{method}"));
opts.insert(1, format!("url: {url_expr}"));
let opts_str = opts.join(", ");
let _ = writeln!(out, " {{:ok, response}} = Req.request({opts_str})");
}
}
fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
let _ = writeln!(out, " assert {response_var}.status == {status}");
}
fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
let header_key = name.to_lowercase();
if header_key == "connection" {
return;
}
let key_lit = format!("\"{}\"", escape_elixir(&header_key));
let get_header_expr = format!(
"Enum.find_value({response_var}.headers, fn {{k, v}} -> if String.downcase(k) == {key_lit}, do: List.first(List.wrap(v)) end)"
);
match expected {
"<<present>>" => {
let _ = writeln!(out, " assert {get_header_expr} != nil");
}
"<<absent>>" => {
let _ = writeln!(out, " assert {get_header_expr} == nil");
}
"<<uuid>>" => {
let var = sanitize_ident(&header_key);
let _ = writeln!(out, " header_val_{var} = {get_header_expr}");
let _ = writeln!(
out,
" assert Regex.match?(~r/^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$/i, to_string(header_val_{var}))"
);
}
literal => {
let val_lit = format!("\"{}\"", escape_elixir(literal));
let _ = writeln!(out, " assert {get_header_expr} == {val_lit}");
}
}
}
fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
let elixir_val = json_to_elixir(expected);
match expected {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
let _ = writeln!(
out,
" body_decoded = if is_binary({response_var}.body), do: Jason.decode!({response_var}.body), else: {response_var}.body"
);
let _ = writeln!(out, " assert body_decoded == {elixir_val}");
}
_ => {
let _ = writeln!(out, " assert {response_var}.body == {elixir_val}");
}
}
}
fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
if let Some(obj) = expected.as_object() {
let _ = writeln!(
out,
" decoded_body = if is_binary({response_var}.body), do: Jason.decode!({response_var}.body), else: {response_var}.body"
);
for (key, val) in obj {
let key_lit = format!("\"{}\"", escape_elixir(key));
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert decoded_body[{key_lit}] == {elixir_val}");
}
}
}
fn render_assert_validation_errors(
&self,
out: &mut String,
response_var: &str,
errors: &[ValidationErrorExpectation],
) {
for err in errors {
let msg_lit = format!("\"{}\"", escape_elixir(&err.msg));
let _ = writeln!(
out,
" assert String.contains?(Jason.encode!({response_var}.body), {msg_lit})"
);
}
}
}
pub(super) fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
let method = http.request.method.to_uppercase();
if FINCH_UNSUPPORTED_METHODS.contains(&method.as_str()) {
let test_name = sanitize_ident(&fixture.id);
let test_label = fixture.id.replace('"', "\\\"");
let path = &http.request.path;
let _ = writeln!(out, " describe \"{test_name}\" do");
let _ = writeln!(out, " @tag :skip");
let _ = writeln!(out, " test \"{method} {path} - {test_label}\" do");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
return;
}
let renderer = ElixirTestClientRenderer {
fixture_id: &fixture.id,
expected_status: http.expected_response.status_code,
};
client::http_call::render_http_test(out, &renderer, fixture);
}