use crate::e2e::escape::{escape_csharp, sanitize_ident};
use crate::e2e::fixture::{Fixture, HttpFixture, ValidationErrorExpectation};
use heck::ToUpperCamelCase;
use crate::e2e::codegen::client;
struct CSharpTestClientRenderer;
fn to_csharp_http_method(method: &str) -> String {
let lower = method.to_ascii_lowercase();
let mut chars = lower.chars();
match chars.next() {
Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(),
None => String::new(),
}
}
const CSHARP_RESTRICTED_REQUEST_HEADERS: &[&str] = &[
"content-length",
"host",
"connection",
"expect",
"transfer-encoding",
"upgrade",
"content-type",
"content-encoding",
"content-language",
"content-location",
"content-md5",
"content-range",
"content-disposition",
];
fn is_csharp_content_header(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"content-type"
| "content-length"
| "content-encoding"
| "content-language"
| "content-location"
| "content-md5"
| "content-range"
| "content-disposition"
| "expires"
| "last-modified"
| "allow"
)
}
impl client::TestClientRenderer for CSharpTestClientRenderer {
fn language_name(&self) -> &'static str {
"csharp"
}
fn sanitize_test_name(&self, id: &str) -> String {
id.to_upper_camel_case()
}
fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
let escaped_reason = skip_reason.map(escape_csharp);
let rendered = crate::e2e::template_env::render(
"csharp/http_test_open.jinja",
minijinja::context! {
fn_name => fn_name,
description => description,
skip_reason => escaped_reason,
},
);
out.push_str(&rendered);
}
fn render_test_close(&self, out: &mut String) {
let rendered = crate::e2e::template_env::render("csharp/http_test_close.jinja", minijinja::context! {});
out.push_str(&rendered);
}
fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
let method = to_csharp_http_method(ctx.method);
let path_param_names = extract_path_param_names(ctx.path);
out.push_str(" var baseUrl = Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") ?? \"http://localhost:8080\";\n");
for param_name in &path_param_names {
out.push_str(&format!(" var {param_name} = \"\";\n"));
}
out.push_str(
" using var handler = new System.Net.Http.HttpClientHandler { AllowAutoRedirect = false };\n",
);
out.push_str(" using var client = new System.Net.Http.HttpClient(handler);\n");
out.push_str(&format!(" var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.{method}, $\"{{baseUrl}}{}\");\n", ctx.path));
if let Some(body) = ctx.body {
let content_type = ctx.content_type.unwrap_or("application/json");
let json_str = serde_json::to_string(body).unwrap_or_default();
let escaped = escape_csharp(&json_str);
if content_type.contains("multipart/form-data") && content_type.contains("boundary=") {
let boundary_pos = content_type.find("boundary=").unwrap_or(0);
let boundary_value = &content_type[boundary_pos + 9..];
out.push_str(" var multipartBytes = System.Text.Encoding.UTF8.GetBytes(\"");
out.push_str(&escaped);
out.push_str("\");\n");
out.push_str(" var multipartContent = new System.Net.Http.ByteArrayContent(multipartBytes);\n");
out.push_str(" var mediaType = new System.Net.Http.Headers.MediaTypeHeaderValue(\"multipart/form-data\");\n");
out.push_str(&format!(" mediaType.Parameters.Add(new System.Net.Http.Headers.NameValueHeaderValue(\"boundary\", \"{boundary_value}\"));\n"));
out.push_str(" multipartContent.Headers.ContentType = mediaType;\n");
out.push_str(" request.Content = multipartContent;\n");
} else {
out.push_str(&format!(" request.Content = new System.Net.Http.StringContent(\"{escaped}\", System.Text.Encoding.UTF8, \"{content_type}\");\n"));
}
}
for (name, value) in ctx.headers {
if CSHARP_RESTRICTED_REQUEST_HEADERS.contains(&name.to_lowercase().as_str()) {
continue;
}
let escaped_name = escape_csharp(name);
let escaped_value = escape_csharp(value);
out.push_str(&format!(
" request.Headers.Add(\"{escaped_name}\", \"{escaped_value}\");\n"
));
}
if !ctx.cookies.is_empty() {
let mut pairs: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
pairs.sort();
let cookie_header = escape_csharp(&pairs.join("; "));
out.push_str(&format!(
" request.Headers.Add(\"Cookie\", \"{cookie_header}\");\n"
));
}
out.push_str(" var response = await client.SendAsync(request);\n");
}
fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
out.push_str(&format!(" Assert.Equal({status}, (int)response.StatusCode);\n"));
}
fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
let target = if is_csharp_content_header(name) {
"response.Content.Headers"
} else {
"response.Headers"
};
let escaped_name = escape_csharp(name);
match expected {
"<<present>>" => {
out.push_str(&format!(" Assert.True({target}.Contains(\"{escaped_name}\"), \"expected header {escaped_name} to be present\");\n"));
}
"<<absent>>" => {
out.push_str(&format!(" Assert.False({target}.Contains(\"{escaped_name}\"), \"expected header {escaped_name} to be absent\");\n"));
}
"<<uuid>>" => {
out.push_str(&format!(" Assert.True({target}.TryGetValues(\"{escaped_name}\", out var _uuidHdr) && System.Text.RegularExpressions.Regex.IsMatch(string.Join(\", \", _uuidHdr), @\"^[0-9a-fA-F]{{8}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{4}}-[0-9a-fA-F]{{12}}$\"), \"header {escaped_name} is not a UUID\");\n"));
}
literal => {
let var_name = format!("hdr{}", sanitize_ident(name));
let escaped_value = escape_csharp(literal);
out.push_str(&format!(" Assert.True({target}.TryGetValues(\"{escaped_name}\", out var {var_name}) && {var_name}.Any(v => v.Contains(\"{escaped_value}\")), \"header {escaped_name} mismatch\");\n"));
}
}
}
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_csharp(&json_str);
out.push_str(" var bodyText = await response.Content.ReadAsStringAsync();\n");
out.push_str(" var body = JsonDocument.Parse(bodyText).RootElement;\n");
out.push_str(&format!(
" var expectedBody = JsonDocument.Parse(\"{escaped}\").RootElement;\n"
));
out.push_str(" Assert.Equal(expectedBody.GetRawText(), body.GetRawText());\n");
}
serde_json::Value::String(s) => {
let escaped = escape_csharp(s);
out.push_str(" var bodyText = await response.Content.ReadAsStringAsync();\n");
out.push_str(&format!(" Assert.Equal(\"{escaped}\", bodyText.Trim());\n"));
}
other => {
let escaped = escape_csharp(&other.to_string());
out.push_str(" var bodyText = await response.Content.ReadAsStringAsync();\n");
out.push_str(&format!(" Assert.Equal(\"{escaped}\", bodyText.Trim());\n"));
}
}
}
fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
if let Some(obj) = expected.as_object() {
out.push_str(" var partialBodyText = await response.Content.ReadAsStringAsync();\n");
out.push_str(" var partialBody = JsonDocument.Parse(partialBodyText).RootElement;\n");
for (key, val) in obj {
let escaped_key = escape_csharp(key);
let json_str = serde_json::to_string(val).unwrap_or_default();
let escaped_val = escape_csharp(&json_str);
let var_name = format!("expected{}", key.to_upper_camel_case());
out.push_str(&format!(
" var {var_name} = JsonDocument.Parse(\"{escaped_val}\").RootElement;\n"
));
out.push_str(&format!(" Assert.True(partialBody.TryGetProperty(\"{escaped_key}\", out var _partialProp{var_name}) && _partialProp{var_name}.GetRawText() == {var_name}.GetRawText(), \"partial body field '{escaped_key}' mismatch\");\n"));
}
}
}
fn render_assert_validation_errors(
&self,
out: &mut String,
_response_var: &str,
errors: &[ValidationErrorExpectation],
) {
out.push_str(" var validationBodyText = await response.Content.ReadAsStringAsync();\n");
for err in errors {
let escaped_msg = escape_csharp(&err.msg);
out.push_str(&format!(
" Assert.Contains(\"{escaped_msg}\", validationBodyText);\n"
));
}
}
}
pub(super) fn render_http_test_method(out: &mut String, fixture: &Fixture, _http: &HttpFixture) {
client::http_call::render_http_test(out, &CSharpTestClientRenderer, fixture);
}
fn extract_path_param_names(path: &str) -> Vec<String> {
let mut params = Vec::new();
let mut in_param = false;
let mut current_param = String::new();
for ch in path.chars() {
match ch {
'{' => {
in_param = true;
current_param.clear();
}
'}' => {
if in_param && !current_param.is_empty() {
let param_name = current_param.split(':').next().unwrap_or("").to_string();
if !param_name.is_empty() {
params.push(param_name);
}
}
in_param = false;
current_param.clear();
}
_ if in_param => {
current_param.push(ch);
}
_ => {}
}
}
params
}