use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GctfDocument {
pub file_path: String,
pub sections: Vec<Section>,
pub metadata: DocumentMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_document: Option<Box<GctfDocument>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DocumentMetadata {
pub source: Option<String>,
pub mtime: Option<i64>,
pub parsed_at: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct FileMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub links: Vec<String>,
}
impl FileMeta {
pub fn is_empty(&self) -> bool {
self.name.is_none()
&& self.summary.is_none()
&& self.tags.is_empty()
&& self.owner.is_none()
&& self.links.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Section {
pub section_type: SectionType,
pub content: SectionContent,
pub inline_options: InlineOptions,
pub raw_content: String,
pub start_line: usize,
pub end_line: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SectionContent {
Single(String),
Json(serde_json::Value),
JsonLines(Vec<serde_json::Value>),
KeyValues(HashMap<String, String>),
Extract(HashMap<String, String>),
Assertions(Vec<String>),
Meta(FileMeta),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SectionType {
Address,
Endpoint,
Request,
Response,
Error,
RequestHeaders,
Asserts,
Proto,
Tls,
Options,
Extract,
Meta,
}
impl SectionType {
pub fn is_terminal(&self) -> bool {
matches!(
self,
SectionType::Response | SectionType::Error | SectionType::Asserts
)
}
pub fn as_str(&self) -> &'static str {
match self {
SectionType::Address => "ADDRESS",
SectionType::Endpoint => "ENDPOINT",
SectionType::Request => "REQUEST",
SectionType::Response => "RESPONSE",
SectionType::Error => "ERROR",
SectionType::RequestHeaders => "REQUEST_HEADERS",
SectionType::Asserts => "ASSERTS",
SectionType::Proto => "PROTO",
SectionType::Tls => "TLS",
SectionType::Options => "OPTIONS",
SectionType::Extract => "EXTRACT",
SectionType::Meta => "META",
}
}
pub fn from_keyword(s: &str) -> Option<SectionType> {
match s.trim() {
"ADDRESS" => Some(SectionType::Address),
"ENDPOINT" => Some(SectionType::Endpoint),
"REQUEST" => Some(SectionType::Request),
"RESPONSE" => Some(SectionType::Response),
"ERROR" => Some(SectionType::Error),
"REQUEST_HEADERS" | "HEADERS" => Some(SectionType::RequestHeaders),
"ASSERTS" => Some(SectionType::Asserts),
"PROTO" => Some(SectionType::Proto),
"TLS" => Some(SectionType::Tls),
"OPTIONS" => Some(SectionType::Options),
"EXTRACT" => Some(SectionType::Extract),
"META" => Some(SectionType::Meta),
_ => None,
}
}
pub fn is_multiple_allowed(&self) -> bool {
matches!(
self,
SectionType::Request
| SectionType::Response
| SectionType::Asserts
| SectionType::Extract
)
}
pub fn is_file_level(&self) -> bool {
matches!(self, SectionType::Meta)
}
pub fn supports_inline_options(&self) -> bool {
matches!(self, SectionType::Response | SectionType::Error)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct InlineOptions {
pub with_asserts: bool,
pub partial: bool,
pub tolerance: Option<f64>,
pub redact: Vec<String>,
pub unordered_arrays: bool,
}
impl InlineOptions {
pub fn to_header_tokens(&self) -> Vec<String> {
let mut parts = Vec::new();
if self.partial {
parts.push("partial".to_string());
}
if let Some(tolerance) = self.tolerance {
parts.push(format!("tolerance={}", tolerance));
}
if !self.redact.is_empty() {
let mut sorted_redact = self.redact.clone();
sorted_redact.sort();
let quoted = sorted_redact
.iter()
.map(|field| format!("\"{}\"", field))
.collect::<Vec<_>>()
.join(",");
parts.push(format!("redact=[{}]", quoted));
}
if self.unordered_arrays {
parts.push("unordered_arrays".to_string());
}
if self.with_asserts {
parts.push("with_asserts".to_string());
}
parts
}
pub fn is_empty(&self) -> bool {
!self.with_asserts
&& !self.partial
&& self.tolerance.is_none()
&& self.redact.is_empty()
&& !self.unordered_arrays
}
}
impl Section {
pub fn format_header(&self) -> String {
let section = self.section_type.as_str();
if self.section_type.supports_inline_options() {
let parts = self.inline_options.to_header_tokens();
if parts.is_empty() {
format!("--- {} ---", section)
} else {
format!("--- {} {} ---", section, parts.join(" "))
}
} else {
format!("--- {} ---", section)
}
}
pub fn header_keyword_from_source<'a>(&self, source: &'a str) -> Option<&'a str> {
let header_line = source.lines().nth(self.start_line)?.trim();
let inner = header_line.strip_prefix("---")?.strip_suffix("---")?.trim();
inner.split_whitespace().next()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SectionHeader {
pub section_type: SectionType,
pub options: HashMap<String, String>,
}
pub struct DocumentChainIter<'a> {
current: Option<&'a GctfDocument>,
}
impl<'a> Iterator for DocumentChainIter<'a> {
type Item = &'a GctfDocument;
fn next(&mut self) -> Option<Self::Item> {
self.current.take().inspect(|doc| {
self.current = doc.next_document.as_deref();
})
}
}
impl GctfDocument {
pub fn new(file_path: String) -> Self {
Self {
file_path,
sections: Vec::new(),
metadata: DocumentMetadata {
source: None,
mtime: None,
parsed_at: crate::time::now_timestamp(),
},
next_document: None,
}
}
pub fn iter_chain(&self) -> DocumentChainIter<'_> {
DocumentChainIter {
current: Some(self),
}
}
pub fn document_count(&self) -> usize {
self.iter_chain().count()
}
pub fn is_single_document(&self) -> bool {
self.next_document.is_none()
}
pub fn get_document(&self, index: usize) -> Option<&GctfDocument> {
self.iter_chain().nth(index)
}
pub fn sections_by_type(&self, section_type: SectionType) -> Vec<&Section> {
self.sections
.iter()
.filter(|s| s.section_type == section_type)
.collect()
}
pub fn first_section(&self, section_type: SectionType) -> Option<&Section> {
self.sections
.iter()
.find(|s| s.section_type == section_type)
}
pub fn get_address(&self, env_address: Option<&str>) -> Option<String> {
if let Some(section) = self.first_section(SectionType::Address)
&& let SectionContent::Single(addr) = §ion.content
{
return Some(addr.clone());
}
env_address.map(|s| s.to_string())
}
pub fn get_endpoint(&self) -> Option<String> {
if let Some(section) = self.first_section(SectionType::Endpoint)
&& let SectionContent::Single(endpoint) = §ion.content
{
return Some(endpoint.clone());
}
None
}
pub fn parse_endpoint(&self) -> Option<(String, String, String)> {
let endpoint = self.get_endpoint()?;
let parts: Vec<&str> = endpoint.split('/').collect();
if parts.len() == 2 {
let full_service = parts[0];
let service_parts: Vec<&str> = full_service.split('.').collect();
if service_parts.len() >= 2 {
let package = service_parts[..service_parts.len() - 1].join(".");
let service = service_parts[service_parts.len() - 1].to_string();
let method = parts[1].to_string();
return Some((package, service, method));
} else if service_parts.len() == 1 {
let package = String::new();
let service = service_parts[0].to_string();
let method = parts[1].to_string();
return Some((package, service, method));
}
}
None
}
pub fn get_requests(&self) -> Vec<serde_json::Value> {
self.sections_by_type(SectionType::Request)
.into_iter()
.filter_map(|s| {
if let SectionContent::Json(json) = &s.content {
Some(json.clone())
} else {
None
}
})
.collect()
}
pub fn get_assertions(&self) -> Vec<Vec<String>> {
self.sections_by_type(SectionType::Asserts)
.into_iter()
.filter_map(|s| {
if let SectionContent::Assertions(asserts) = &s.content {
Some(asserts.clone())
} else {
None
}
})
.collect()
}
pub fn get_request_headers(&self) -> Option<HashMap<String, String>> {
if let Some(section) = self.first_section(SectionType::RequestHeaders)
&& let SectionContent::KeyValues(headers) = §ion.content
{
return Some(headers.clone());
}
None
}
pub fn get_tls_config(&self) -> Option<HashMap<String, String>> {
if let Some(section) = self.first_section(SectionType::Tls)
&& let SectionContent::KeyValues(config) = §ion.content
{
return Some(config.clone());
}
None
}
pub fn get_options(&self) -> Option<HashMap<String, String>> {
if let Some(section) = self.first_section(SectionType::Options)
&& let SectionContent::KeyValues(config) = §ion.content
{
return Some(config.clone());
}
None
}
pub fn get_tls_config_with_defaults(
&self,
defaults: &HashMap<String, String>,
) -> Option<HashMap<String, String>> {
let mut merged = defaults.clone();
if let Some(section) = self.first_section(SectionType::Tls)
&& let SectionContent::KeyValues(config) = §ion.content
{
for (key, value) in config {
merged.insert(key.clone(), value.clone());
}
}
if merged.is_empty() {
None
} else {
Some(merged)
}
}
pub fn get_proto_config(&self) -> Option<HashMap<String, String>> {
if let Some(section) = self.first_section(SectionType::Proto)
&& let SectionContent::KeyValues(config) = §ion.content
{
return Some(config.clone());
}
None
}
pub fn has_response_error_conflict(&self) -> bool {
self.first_section(SectionType::Response).is_some()
&& self.first_section(SectionType::Error).is_some()
}
pub fn section_uses_deprecated_headers_alias(&self, section: &Section) -> bool {
if section.section_type != SectionType::RequestHeaders {
return false;
}
self.metadata
.source
.as_deref()
.and_then(|source| section.header_keyword_from_source(source))
.is_some_and(|keyword| keyword.eq_ignore_ascii_case("HEADERS"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_section_type_from_str() {
assert_eq!(
SectionType::from_keyword("ADDRESS"),
Some(SectionType::Address)
);
assert_eq!(
SectionType::from_keyword("ENDPOINT"),
Some(SectionType::Endpoint)
);
assert_eq!(SectionType::from_keyword("INVALID"), None);
}
#[test]
fn test_section_type_multiple_allowed() {
assert!(SectionType::Request.is_multiple_allowed());
assert!(SectionType::Response.is_multiple_allowed());
assert!(SectionType::Asserts.is_multiple_allowed());
assert!(!SectionType::Address.is_multiple_allowed());
assert!(!SectionType::Endpoint.is_multiple_allowed());
}
#[test]
fn test_section_type_supports_inline_options() {
assert!(SectionType::Response.supports_inline_options());
assert!(SectionType::Error.supports_inline_options());
assert!(!SectionType::Request.supports_inline_options());
assert!(!SectionType::Address.supports_inline_options());
}
#[test]
fn test_section_type_as_str() {
assert_eq!(SectionType::Address.as_str(), "ADDRESS");
assert_eq!(SectionType::Endpoint.as_str(), "ENDPOINT");
assert_eq!(SectionType::Request.as_str(), "REQUEST");
assert_eq!(SectionType::Response.as_str(), "RESPONSE");
assert_eq!(SectionType::Error.as_str(), "ERROR");
assert_eq!(SectionType::RequestHeaders.as_str(), "REQUEST_HEADERS");
assert_eq!(SectionType::Asserts.as_str(), "ASSERTS");
assert_eq!(SectionType::Proto.as_str(), "PROTO");
assert_eq!(SectionType::Tls.as_str(), "TLS");
assert_eq!(SectionType::Options.as_str(), "OPTIONS");
assert_eq!(SectionType::Extract.as_str(), "EXTRACT");
}
#[test]
fn test_section_type_from_keyword_aliases() {
assert_eq!(
SectionType::from_keyword("HEADERS"),
Some(SectionType::RequestHeaders)
);
assert_eq!(
SectionType::from_keyword("REQUEST_HEADERS"),
Some(SectionType::RequestHeaders)
);
}
#[test]
fn test_section_type_from_keyword_case_insensitive() {
assert_eq!(SectionType::from_keyword("address"), None);
assert_eq!(
SectionType::from_keyword(" ADDRESS "),
Some(SectionType::Address)
);
}
#[test]
fn test_section_type_is_terminal() {
assert!(SectionType::Response.is_terminal());
assert!(SectionType::Error.is_terminal());
assert!(SectionType::Asserts.is_terminal());
assert!(!SectionType::Request.is_terminal());
assert!(!SectionType::Endpoint.is_terminal());
assert!(!SectionType::Extract.is_terminal());
assert!(!SectionType::Address.is_terminal());
}
#[test]
fn test_gctf_document_new() {
let doc = GctfDocument::new("test.gctf".to_string());
assert_eq!(doc.file_path, "test.gctf");
assert!(doc.sections.is_empty());
assert!(doc.metadata.source.is_none());
assert!(doc.metadata.mtime.is_none());
}
#[test]
fn test_gctf_document_sections_by_type() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Request,
content: SectionContent::Json(json!({"key": "value1"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
doc.sections.push(Section {
section_type: SectionType::Request,
content: SectionContent::Json(json!({"key": "value2"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 3,
end_line: 4,
});
doc.sections.push(Section {
section_type: SectionType::Response,
content: SectionContent::Json(json!({"result": "ok"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 5,
end_line: 6,
});
let requests = doc.sections_by_type(SectionType::Request);
assert_eq!(requests.len(), 2);
let responses = doc.sections_by_type(SectionType::Response);
assert_eq!(responses.len(), 1);
let errors = doc.sections_by_type(SectionType::Error);
assert_eq!(errors.len(), 0);
}
#[test]
fn test_gctf_document_first_section() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Request,
content: SectionContent::Json(json!({"key": "value"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
let first_request = doc.first_section(SectionType::Request);
assert!(first_request.is_some());
let first_error = doc.first_section(SectionType::Error);
assert!(first_error.is_none());
}
#[test]
fn test_gctf_document_get_address() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Address,
content: SectionContent::Single("localhost:4770".to_string()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 1,
});
assert_eq!(doc.get_address(None), Some("localhost:4770".to_string()));
assert_eq!(
doc.get_address(Some("env:5000")),
Some("localhost:4770".to_string())
);
let doc2 = GctfDocument::new("test.gctf".to_string());
assert_eq!(
doc2.get_address(Some("env:5000")),
Some("env:5000".to_string())
);
assert_eq!(doc2.get_address(None), None);
}
#[test]
fn test_gctf_document_get_endpoint() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Endpoint,
content: SectionContent::Single("my.Service/Method".to_string()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 1,
});
assert_eq!(doc.get_endpoint(), Some("my.Service/Method".to_string()));
let doc2 = GctfDocument::new("test.gctf".to_string());
assert_eq!(doc2.get_endpoint(), None);
}
#[test]
fn test_gctf_document_parse_endpoint() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Endpoint,
content: SectionContent::Single("package.Service/Method".to_string()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 1,
});
let (package, service, method) = doc.parse_endpoint().unwrap();
assert_eq!(package, "package");
assert_eq!(service, "Service");
assert_eq!(method, "Method");
}
#[test]
fn test_gctf_document_parse_endpoint_no_package() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Endpoint,
content: SectionContent::Single("Service/Method".to_string()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 1,
});
let (package, service, method) = doc.parse_endpoint().unwrap();
assert_eq!(package, "");
assert_eq!(service, "Service");
assert_eq!(method, "Method");
}
#[test]
fn test_gctf_document_parse_endpoint_invalid() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Endpoint,
content: SectionContent::Single("invalid".to_string()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 1,
});
assert!(doc.parse_endpoint().is_none());
}
#[test]
fn test_gctf_document_get_requests() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Request,
content: SectionContent::Json(json!({"key": "value1"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
doc.sections.push(Section {
section_type: SectionType::Request,
content: SectionContent::Json(json!({"key": "value2"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 3,
end_line: 4,
});
let requests = doc.get_requests();
assert_eq!(requests.len(), 2);
assert_eq!(requests[0], json!({"key": "value1"}));
assert_eq!(requests[1], json!({"key": "value2"}));
}
#[test]
fn test_gctf_document_get_assertions() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.sections.push(Section {
section_type: SectionType::Asserts,
content: SectionContent::Assertions(vec![".id == 1".to_string()]),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
doc.sections.push(Section {
section_type: SectionType::Asserts,
content: SectionContent::Assertions(vec![".name == \"test\"".to_string()]),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 3,
end_line: 4,
});
let assertions = doc.get_assertions();
assert_eq!(assertions.len(), 2);
assert_eq!(assertions[0], vec![".id == 1"]);
assert_eq!(assertions[1], vec![".name == \"test\""]);
}
#[test]
fn test_gctf_document_get_request_headers() {
let mut doc = GctfDocument::new("test.gctf".to_string());
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer token".to_string());
doc.sections.push(Section {
section_type: SectionType::RequestHeaders,
content: SectionContent::KeyValues(headers.clone()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
let result = doc.get_request_headers().unwrap();
assert_eq!(
result.get("Authorization"),
Some(&"Bearer token".to_string())
);
}
#[test]
fn test_gctf_document_get_tls_config() {
let mut doc = GctfDocument::new("test.gctf".to_string());
let mut config = HashMap::new();
config.insert("ca_cert".to_string(), "/path/to/ca.pem".to_string());
doc.sections.push(Section {
section_type: SectionType::Tls,
content: SectionContent::KeyValues(config.clone()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
let result = doc.get_tls_config().unwrap();
assert_eq!(result.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
}
#[test]
fn test_gctf_document_get_options() {
let mut doc = GctfDocument::new("test.gctf".to_string());
let mut options = HashMap::new();
options.insert("dry_run".to_string(), "true".to_string());
options.insert("timeout".to_string(), "10".to_string());
doc.sections.push(Section {
section_type: SectionType::Options,
content: SectionContent::KeyValues(options.clone()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
let result = doc.get_options().unwrap();
assert_eq!(result.get("dry_run"), Some(&"true".to_string()));
assert_eq!(result.get("timeout"), Some(&"10".to_string()));
}
#[test]
fn test_gctf_document_get_tls_config_with_defaults_env_only() {
let doc = GctfDocument::new("test.gctf".to_string());
let mut defaults = HashMap::new();
defaults.insert("server_name".to_string(), "example.com".to_string());
let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
}
#[test]
fn test_gctf_document_get_tls_config_with_defaults_section_overrides() {
let mut doc = GctfDocument::new("test.gctf".to_string());
let mut config = HashMap::new();
config.insert("insecure".to_string(), "true".to_string());
doc.sections.push(Section {
section_type: SectionType::Tls,
content: SectionContent::KeyValues(config),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
let mut defaults = HashMap::new();
defaults.insert("insecure".to_string(), "false".to_string());
defaults.insert("server_name".to_string(), "example.com".to_string());
let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
assert_eq!(result.get("insecure"), Some(&"true".to_string()));
assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
}
#[test]
fn test_gctf_document_get_proto_config() {
let mut doc = GctfDocument::new("test.gctf".to_string());
let mut config = HashMap::new();
config.insert("files".to_string(), "service.proto".to_string());
doc.sections.push(Section {
section_type: SectionType::Proto,
content: SectionContent::KeyValues(config.clone()),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
let result = doc.get_proto_config().unwrap();
assert_eq!(result.get("files"), Some(&"service.proto".to_string()));
}
#[test]
fn test_gctf_document_has_response_error_conflict() {
let mut doc = GctfDocument::new("test.gctf".to_string());
assert!(!doc.has_response_error_conflict());
doc.sections.push(Section {
section_type: SectionType::Response,
content: SectionContent::Json(json!({"result": "ok"})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 1,
end_line: 2,
});
assert!(!doc.has_response_error_conflict());
doc.sections.push(Section {
section_type: SectionType::Error,
content: SectionContent::Json(json!({"code": 5})),
inline_options: InlineOptions::default(),
raw_content: "".to_string(),
start_line: 3,
end_line: 4,
});
assert!(doc.has_response_error_conflict());
}
#[test]
fn test_inline_options_default() {
let options = InlineOptions::default();
assert!(!options.with_asserts);
assert!(!options.partial);
assert!(options.tolerance.is_none());
assert!(options.redact.is_empty());
assert!(!options.unordered_arrays);
}
#[test]
fn test_section_format_header_with_inline_options() {
let section = Section {
section_type: SectionType::Response,
content: SectionContent::Json(serde_json::json!({"ok": true})),
inline_options: InlineOptions {
with_asserts: true,
partial: true,
tolerance: Some(0.1),
redact: vec!["token".to_string()],
unordered_arrays: true,
},
raw_content: "".to_string(),
start_line: 0,
end_line: 0,
};
let header = section.format_header();
assert_eq!(
header,
"--- RESPONSE partial tolerance=0.1 redact=[\"token\"] unordered_arrays with_asserts ---"
);
}
#[test]
fn test_section_content_debug() {
let content = SectionContent::Single("test".to_string());
let debug_str = format!("{:?}", content);
assert!(debug_str.contains("Single"));
}
#[test]
fn test_section_header_keyword_from_source() {
let section = Section {
section_type: SectionType::Response,
content: SectionContent::Json(serde_json::json!({"ok": true})),
inline_options: InlineOptions::default(),
raw_content: "{\"ok\":true}".to_string(),
start_line: 0,
end_line: 2,
};
let source = "--- RESPONSE with_asserts=true ---\n{\"ok\":true}\n";
assert_eq!(section.header_keyword_from_source(source), Some("RESPONSE"));
}
#[test]
fn test_document_detects_deprecated_headers_alias() {
let mut doc = GctfDocument::new("test.gctf".to_string());
doc.metadata.source = Some("--- HEADERS ---\nAuthorization: Bearer t\n".to_string());
doc.sections.push(Section {
section_type: SectionType::RequestHeaders,
content: SectionContent::KeyValues(HashMap::from([(
"Authorization".to_string(),
"Bearer t".to_string(),
)])),
inline_options: InlineOptions::default(),
raw_content: "Authorization: Bearer t".to_string(),
start_line: 0,
end_line: 2,
});
assert!(doc.section_uses_deprecated_headers_alias(&doc.sections[0]));
}
#[test]
fn test_gctf_document_debug() {
let doc = GctfDocument::new("test.gctf".to_string());
let debug_str = format!("{:?}", doc);
assert!(debug_str.contains("test.gctf"));
}
#[test]
fn test_document_chain_single() {
let doc = GctfDocument::new("test.gctf".to_string());
assert!(doc.is_single_document());
assert_eq!(doc.document_count(), 1);
}
#[test]
fn test_document_chain_two_docs() {
let mut doc1 = GctfDocument::new("test.gctf".to_string());
let doc2 = GctfDocument::new("test.gctf".to_string());
doc1.next_document = Some(Box::new(doc2));
assert!(!doc1.is_single_document());
assert_eq!(doc1.document_count(), 2);
}
#[test]
fn test_document_chain_three_docs() {
let mut doc3 = GctfDocument::new("test.gctf".to_string());
doc3.file_path = "doc3".to_string();
let mut doc2 = GctfDocument::new("test.gctf".to_string());
doc2.file_path = "doc2".to_string();
doc2.next_document = Some(Box::new(doc3));
let mut doc1 = GctfDocument::new("test.gctf".to_string());
doc1.file_path = "doc1".to_string();
doc1.next_document = Some(Box::new(doc2));
assert_eq!(doc1.document_count(), 3);
let docs: Vec<_> = doc1.iter_chain().collect();
assert_eq!(docs.len(), 3);
assert_eq!(docs[0].file_path, "doc1");
assert_eq!(docs[1].file_path, "doc2");
assert_eq!(docs[2].file_path, "doc3");
}
#[test]
fn test_document_chain_get_document() {
let mut doc2 = GctfDocument::new("test.gctf".to_string());
doc2.file_path = "doc2".to_string();
let mut doc1 = GctfDocument::new("test.gctf".to_string());
doc1.file_path = "doc1".to_string();
doc1.next_document = Some(Box::new(doc2));
assert_eq!(doc1.get_document(0).unwrap().file_path, "doc1");
assert_eq!(doc1.get_document(1).unwrap().file_path, "doc2");
assert!(doc1.get_document(2).is_none());
}
#[test]
fn test_document_chain_iter_on_last() {
let doc = GctfDocument::new("test.gctf".to_string());
let docs: Vec<_> = doc.iter_chain().collect();
assert_eq!(docs.len(), 1);
assert_eq!(docs[0].file_path, "test.gctf");
}
}