use crate::e2e::codegen::client;
use crate::e2e::escape::{escape_kotlin, sanitize_ident};
use crate::e2e::fixture::{Fixture, HttpFixture, ValidationErrorExpectation};
use heck::ToUpperCamelCase;
use std::fmt::Write as FmtWrite;
pub(super) fn url_encode_path(path: &str) -> String {
path.chars()
.map(|c| match c {
'|' => "%7C".to_string(),
'<' => "%3C".to_string(),
'>' => "%3E".to_string(),
'"' => "%22".to_string(),
'#' => "%23".to_string(),
'%' => "%25".to_string(),
c if c.is_ascii_alphanumeric() || "/-_.~?=&:@!".contains(c) => c.to_string(),
c => format!("%{:02X}", c as u8),
})
.collect()
}
pub(super) struct KotlinTestClientRenderer;
impl client::TestClientRenderer for KotlinTestClientRenderer {
fn language_name(&self) -> &'static str {
"kotlin"
}
fn sanitize_test_name(&self, id: &str) -> String {
sanitize_ident(id).to_upper_camel_case()
}
fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
let _ = writeln!(out, " @Test");
let _ = writeln!(out, " fun test{fn_name}() {{");
let _ = writeln!(out, " // {description}");
if let Some(reason) = skip_reason {
let escaped = escape_kotlin(reason);
let _ = writeln!(
out,
" org.junit.jupiter.api.Assumptions.assumeTrue(false, \"{escaped}\")"
);
}
}
fn render_test_close(&self, out: &mut String) {
let _ = writeln!(out, " }}");
}
fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
let method = ctx.method.to_uppercase();
let fixture_path = ctx.path;
const JAVA_RESTRICTED_HEADERS: &[&str] = &["connection", "content-length", "expect", "host", "upgrade"];
let _ = writeln!(
out,
" val baseUrl = System.getenv(\"SUT_URL\") ?: \"http://127.0.0.1:8007\""
);
let encoded_path = url_encode_path(fixture_path);
let _ = writeln!(out, " val uri = java.net.URI.create(\"$baseUrl{encoded_path}\")");
let body_publisher = if let Some(body) = ctx.body {
let json = serde_json::to_string(body).unwrap_or_default();
let escaped = escape_kotlin(&json);
format!("java.net.http.HttpRequest.BodyPublishers.ofString(\"{escaped}\")")
} else {
"java.net.http.HttpRequest.BodyPublishers.noBody()".to_string()
};
let _ = writeln!(out, " val builder = java.net.http.HttpRequest.newBuilder(uri)");
let _ = writeln!(out, " .method(\"{method}\", {body_publisher})");
if ctx.body.is_some() {
let content_type = ctx.content_type.unwrap_or("application/json");
let _ = writeln!(out, " .header(\"Content-Type\", \"{content_type}\")");
}
let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
header_pairs.sort_by_key(|(k, _)| k.as_str());
for (name, value) in &header_pairs {
if JAVA_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
continue;
}
let escaped_name = escape_kotlin(name);
let escaped_value = escape_kotlin(value);
let _ = writeln!(out, " .header(\"{escaped_name}\", \"{escaped_value}\")");
}
if !ctx.cookies.is_empty() {
let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
cookie_pairs.sort_by_key(|(k, _)| k.as_str());
let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
let cookie_header = escape_kotlin(&cookie_str.join("; "));
let _ = writeln!(out, " .header(\"Cookie\", \"{cookie_header}\")");
}
let _ = writeln!(
out,
" val {} = java.net.http.HttpClient.newHttpClient()",
ctx.response_var
);
let _ = writeln!(
out,
" .send(builder.build(), java.net.http.HttpResponse.BodyHandlers.ofString())"
);
}
fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
let _ = writeln!(
out,
" assertEquals({status}, {response_var}.statusCode(), \"status code mismatch\")"
);
}
fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
let escaped_name = escape_kotlin(name);
match expected {
"<<present>>" => {
let _ = writeln!(
out,
" assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").isPresent, \"header {escaped_name} should be present\")"
);
}
"<<absent>>" => {
let _ = writeln!(
out,
" assertFalse({response_var}.headers().firstValue(\"{escaped_name}\").isPresent, \"header {escaped_name} should be absent\")"
);
}
"<<uuid>>" => {
let _ = writeln!(
out,
" assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").orElse(\"\").matches(Regex(\"[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}\")), \"header {escaped_name} should be a UUID\")"
);
}
exact => {
let escaped_value = escape_kotlin(exact);
let _ = writeln!(
out,
" assertTrue({response_var}.headers().firstValue(\"{escaped_name}\").orElse(\"\").contains(\"{escaped_value}\"), \"header {escaped_name} mismatch\")"
);
}
}
}
fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
match expected {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
let json_str = serde_json::to_string(expected).unwrap_or_default();
let escaped = escape_kotlin(&json_str);
let _ = writeln!(out, " val bodyJson = MAPPER.readTree({response_var}.body())");
let _ = writeln!(out, " val expectedJson = MAPPER.readTree(\"{escaped}\")");
let _ = writeln!(out, " assertEquals(expectedJson, bodyJson, \"body mismatch\")");
}
serde_json::Value::String(s) => {
let escaped = escape_kotlin(s);
let _ = writeln!(
out,
" assertEquals(\"{escaped}\", {response_var}.body().trim(), \"body mismatch\")"
);
}
other => {
let escaped = escape_kotlin(&other.to_string());
let _ = writeln!(
out,
" assertEquals(\"{escaped}\", {response_var}.body().trim(), \"body mismatch\")"
);
}
}
}
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, " val _partialTree = MAPPER.readTree({response_var}.body())");
for (key, val) in obj {
let escaped_key = escape_kotlin(key);
match val {
serde_json::Value::String(s) => {
let escaped_val = escape_kotlin(s);
let _ = writeln!(
out,
" assertEquals(\"{escaped_val}\", _partialTree.path(\"{escaped_key}\").asText(), \"partial body field '{escaped_key}' mismatch\")"
);
}
serde_json::Value::Bool(b) => {
let _ = writeln!(
out,
" assertEquals({b}, _partialTree.path(\"{escaped_key}\").asBoolean(), \"partial body field '{escaped_key}' mismatch\")"
);
}
serde_json::Value::Number(n) => {
let _ = writeln!(
out,
" assertEquals({n}, _partialTree.path(\"{escaped_key}\").numberValue(), \"partial body field '{escaped_key}' mismatch\")"
);
}
other => {
let json_str = serde_json::to_string(other).unwrap_or_default();
let escaped_val = escape_kotlin(&json_str);
let _ = writeln!(
out,
" assertEquals(MAPPER.readTree(\"{escaped_val}\"), _partialTree.path(\"{escaped_key}\"), \"partial body field '{escaped_key}' mismatch\")"
);
}
}
}
}
}
fn render_assert_validation_errors(
&self,
out: &mut String,
response_var: &str,
errors: &[ValidationErrorExpectation],
) {
let _ = writeln!(out, " val _veTree = MAPPER.readTree({response_var}.body())");
let _ = writeln!(out, " val _veErrors = _veTree.path(\"errors\")");
for ve in errors {
let escaped_msg = escape_kotlin(&ve.msg);
let _ = writeln!(
out,
" assertTrue((0 until _veErrors.size()).any {{ _veErrors.get(it).path(\"msg\").asText().contains(\"{escaped_msg}\") }}, \"expected validation error containing: {escaped_msg}\")"
);
}
}
}
pub(super) fn render_http_test_method(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
if http.expected_response.status_code == 101 {
let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
let description = &fixture.description;
let _ = writeln!(out, " @Test");
let _ = writeln!(out, " fun test{method_name}() {{");
let _ = writeln!(out, " // {description}");
let _ = writeln!(
out,
" org.junit.jupiter.api.Assumptions.assumeTrue(false, \"Skipped: Java HttpClient cannot handle 101 Switching Protocols responses\")"
);
let _ = writeln!(out, " }}");
return;
}
client::http_call::render_http_test(out, &KotlinTestClientRenderer, fixture);
}