use assert_cmd::Command;
use predicates::prelude::*;
use std::io::Write;
const OAS3: &str = "tests/fixtures/petstore_oas3.json";
const OAS2: &str = "tests/fixtures/petstore_oas2.json";
const OAS3_SCHEMA_REFS: &str = "tests/fixtures/schema_refs_oas3.json";
const OAS3_MULTI_AUTH: &str = "tests/fixtures/multi_auth_oas3.json";
const OAS2_MULTI_AUTH: &str = "tests/fixtures/multi_auth_oas2.json";
const OAS3_EXAMPLES: &str = "tests/fixtures/examples_oas3.json";
const OAS3_REF_BODY: &str = "tests/fixtures/ref_request_body_oas3.json";
const REF_PARAMETER: &str = "tests/fixtures/ref_parameter.json";
const REF_PATH_ITEM: &str = "tests/fixtures/ref_path_item.json";
const TYPE_ARRAY: &str = "tests/fixtures/type_array_nullable.json";
const MISSING_RESPONSES: &str = "tests/fixtures/missing_responses.json";
const OVERRIDE_PARAM: &str = "tests/fixtures/override_param.json";
const UNKNOWN_TAG: &str = "tests/fixtures/unknown_tag.json";
fn vimanam() -> Command {
Command::cargo_bin("vimanam").unwrap()
}
#[test]
fn version_flag_reports_crate_version() {
vimanam()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn summary_lists_services_and_operations() {
vimanam()
.arg(OAS3)
.assert()
.success()
.stdout(predicate::str::contains("# Petstore API"))
.stdout(predicate::str::contains("- Pets"))
.stdout(predicate::str::contains("- Store"))
.stdout(predicate::str::contains("* ListPets"));
}
#[test]
fn basic_detail_writes_endpoint_sections() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic"])
.assert()
.success()
.stdout(predicate::str::contains("### Pets_ListPets"))
.stdout(predicate::str::contains("**Operation:** GET /pets"))
.stdout(predicate::str::contains("**Operation:** POST /pets"));
}
#[test]
fn optional_request_body_is_documented() {
vimanam()
.arg(OAS3)
.args(["--detail", "standard"])
.assert()
.success()
.stdout(predicate::str::contains(
"| `requestBody` | body | No | Pet to add |",
));
}
#[test]
fn required_only_excludes_non_required_parameters() {
vimanam()
.arg(OAS3)
.args(["--detail", "standard", "--required-only"])
.assert()
.success()
.stdout(predicate::str::contains("| `petId` | path | Yes |"))
.stdout(predicate::str::contains("| `limit` |").not());
}
#[test]
fn required_path_param_is_documented() {
vimanam()
.arg(OAS3)
.args(["--detail", "standard"])
.assert()
.success()
.stdout(predicate::str::contains(
"| `petId` | path | Yes | ID of the pet |",
));
}
#[test]
fn exclude_deprecated_hides_endpoint() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic"])
.assert()
.success()
.stdout(predicate::str::contains("Store_ListOrders"));
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--exclude-deprecated"])
.assert()
.success()
.stdout(predicate::str::contains("Store_ListOrders").not());
}
#[test]
fn method_filter_excludes_other_methods() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--method-filter", "GET"])
.assert()
.success()
.stdout(predicate::str::contains("Pets_ListPets"))
.stdout(predicate::str::contains("Pets_CreatePet").not());
}
#[test]
fn method_filter_is_case_insensitive() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--method-filter", "get"])
.assert()
.success()
.stdout(predicate::str::contains("Pets_ListPets"))
.stdout(predicate::str::contains("Pets_CreatePet").not());
}
#[test]
fn service_filter_is_case_insensitive() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--service-filter", "pets"])
.assert()
.success()
.stdout(predicate::str::contains("Pets_ListPets"))
.stdout(predicate::str::contains("Store_ListOrders").not());
}
#[test]
fn path_filter_excludes_other_paths() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--path-filter", "/store"])
.assert()
.success()
.stdout(predicate::str::contains("Store_ListOrders"))
.stdout(predicate::str::contains("Pets_ListPets").not());
}
#[test]
fn include_auth_shows_servers_and_schemes() {
vimanam()
.arg(OAS3)
.arg("--include-auth")
.assert()
.success()
.stdout(predicate::str::contains(
"https://api.petstore.example.com/v1",
))
.stdout(predicate::str::contains("apiKeyAuth"));
}
#[test]
fn flat_grouping_lists_all_endpoints() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--flat"])
.assert()
.success()
.stdout(predicate::str::contains("## Endpoints"))
.stdout(predicate::str::contains("### Pets_ListPets"))
.stdout(predicate::str::contains("### Store_ListOrders"));
}
#[test]
fn oas2_spec_is_supported() {
vimanam()
.arg(OAS2)
.args(["--detail", "standard", "--include-auth"])
.assert()
.success()
.stdout(predicate::str::contains("# Petstore Legacy API"))
.stdout(predicate::str::contains(
"https://legacy.petstore.example.com/v2",
))
.stdout(predicate::str::contains("Pets_CreatePet"))
.stdout(predicate::str::contains(
"| 200 | application/json | Created |",
));
}
#[test]
fn output_flag_writes_file() {
let dir = tempfile::tempdir().unwrap();
let out_path = dir.path().join("out.md");
vimanam()
.arg(OAS3)
.args(["-o", out_path.to_str().unwrap()])
.assert()
.success();
let content = std::fs::read_to_string(&out_path).unwrap();
assert!(content.contains("# Petstore API"));
}
#[test]
fn invalid_json_fails() {
let mut file = tempfile::NamedTempFile::new().unwrap();
write!(file, "this is not json").unwrap();
vimanam()
.arg(file.path())
.assert()
.failure()
.stderr(predicate::str::contains("Error:"));
}
#[test]
fn json_without_openapi_fields_fails() {
let mut file = tempfile::NamedTempFile::new().unwrap();
write!(file, "{{\"hello\": \"world\"}}").unwrap();
vimanam()
.arg(file.path())
.assert()
.failure()
.stderr(predicate::str::contains("Error:"));
}
#[test]
fn output_is_deterministic() {
let run = || {
vimanam()
.arg(OAS3)
.args([
"--detail",
"full",
"--include-schemas",
"--include-auth",
"--sort",
"none",
])
.output()
.unwrap()
.stdout
};
let first = run();
for _ in 0..4 {
assert_eq!(first, run(), "output differed between identical runs");
}
}
#[test]
fn full_detail_expands_schema_refs_into_tables() {
vimanam()
.arg(OAS3_SCHEMA_REFS)
.args(["--detail", "full", "--include-schemas"])
.assert()
.success()
.stdout(predicate::str::contains("#### Request Schema"))
.stdout(predicate::str::contains(
"| `request.name` | string | Yes | Pet name |",
))
.stdout(predicate::str::contains(
"| `request.category.id` | string | Yes | Category identifier |",
))
.stdout(predicate::str::contains(
"| `response.allOf[1].id` | string | Yes | Pet identifier |",
))
.stdout(predicate::str::contains("request.variant.oneOf[0]"));
}
#[test]
fn multiple_security_schemes_preserve_spec_order() {
let run = || {
String::from_utf8(
vimanam()
.arg(OAS3_MULTI_AUTH)
.arg("--include-auth")
.output()
.unwrap()
.stdout,
)
.unwrap()
};
let output = run();
let zebra = output.find("zebraAuth").expect("zebraAuth missing");
let api_key = output.find("apiKeyAuth").expect("apiKeyAuth missing");
let middle = output.find("middleAuth").expect("middleAuth missing");
assert!(
zebra < api_key && api_key < middle,
"security schemes not in spec order: {output}"
);
for _ in 0..4 {
assert_eq!(output, run(), "authentication order differed between runs");
}
}
#[test]
fn oas2_security_schemes_preserve_spec_order() {
let output = String::from_utf8(
vimanam()
.arg(OAS2_MULTI_AUTH)
.arg("--include-auth")
.output()
.unwrap()
.stdout,
)
.unwrap();
let zebra = output.find("zebraAuth").expect("zebraAuth missing");
let api_key = output.find("apiKey").expect("apiKey missing");
let middle = output.find("middleAuth").expect("middleAuth missing");
assert!(
zebra < api_key && api_key < middle,
"OAS2 security schemes not in spec order: {output}"
);
}
#[test]
fn group_by_method_groups_by_http_method() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--group-by", "method"])
.assert()
.success()
.stdout(predicate::str::contains("## GET"))
.stdout(predicate::str::contains("## POST"));
}
#[test]
fn toc_order_matches_body_order() {
let output = String::from_utf8(
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--sort", "alpha"])
.output()
.unwrap()
.stdout,
)
.unwrap();
let toc_list = output
.find("[Pets_ListPets]")
.expect("ListPets TOC link missing");
let toc_create = output
.find("[Pets_CreatePet]")
.expect("CreatePet TOC link missing");
let body_list = output
.find("### Pets_ListPets")
.expect("ListPets section missing");
let body_create = output
.find("### Pets_CreatePet")
.expect("CreatePet section missing");
assert!(toc_list < toc_create, "TOC order unexpected: {output}");
assert!(body_list < body_create, "body order unexpected: {output}");
}
#[test]
fn include_examples_renders_request_and_response() {
vimanam()
.arg(OAS3_EXAMPLES)
.args(["--detail", "full", "--include-examples"])
.assert()
.success()
.stdout(predicate::str::contains("#### Examples"))
.stdout(predicate::str::contains("**Request**"))
.stdout(predicate::str::contains("\"name\": \"Fluffy\""))
.stdout(predicate::str::contains("Response `201`"))
.stdout(predicate::str::contains("\"id\": 7"));
}
#[test]
fn include_examples_only_at_full_detail() {
vimanam()
.arg(OAS3_EXAMPLES)
.args(["--detail", "standard", "--include-examples"])
.assert()
.success()
.stdout(predicate::str::contains("#### Examples").not());
}
#[test]
fn ref_request_body_is_resolved() {
vimanam()
.arg(OAS3_REF_BODY)
.args(["--detail", "standard"])
.assert()
.success()
.stdout(predicate::str::contains(
"| `requestBody` | body | Yes | Pet to add |",
));
vimanam()
.arg(OAS3_REF_BODY)
.args(["--detail", "full", "--include-schemas"])
.assert()
.success()
.stdout(predicate::str::contains(
"| `request.name` | string | Yes | Pet name |",
));
}
#[test]
fn group_by_path_groups_by_path() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--group-by", "path"])
.assert()
.success()
.stdout(predicate::str::contains("## Paths"))
.stdout(predicate::str::contains("## /pets/{petId}"))
.stdout(predicate::str::contains("## /store/orders"))
.stdout(predicate::str::contains("### Pets_ListPets"))
.stdout(predicate::str::contains("### Pets_CreatePet"));
}
#[test]
fn max_tokens_steps_down_detail_level() {
vimanam()
.arg(OAS3)
.args([
"--detail",
"full",
"--include-schemas",
"--max-tokens",
"40",
])
.assert()
.success()
.stderr(predicate::str::contains("token budget"))
.stderr(predicate::str::contains("--detail summary"));
}
#[test]
fn max_tokens_keeps_detail_when_it_fits() {
vimanam()
.arg(OAS3)
.args(["--detail", "basic", "--max-tokens", "100000"])
.assert()
.success()
.stdout(predicate::str::contains("### Pets_ListPets"))
.stderr(predicate::str::is_empty());
}
#[test]
fn full_detail_schema_expansion_detects_ref_cycles() {
vimanam()
.arg(OAS3_SCHEMA_REFS)
.args(["--detail", "full", "--include-schemas"])
.assert()
.success()
.stdout(predicate::str::contains(
"Cycle detected while expanding schema reference",
));
}
#[test]
fn ref_parameter_is_resolved() {
vimanam()
.arg(REF_PARAMETER)
.args(["--detail", "standard"])
.assert()
.success()
.stdout(predicate::str::contains(
"| `limit` | query | No | Max results |",
));
}
#[test]
fn path_item_ref_yields_operation() {
vimanam()
.arg(REF_PATH_ITEM)
.args(["--detail", "basic"])
.assert()
.success()
.stdout(predicate::str::contains("Things_ListThings"))
.stdout(predicate::str::contains("**Operation:** GET /things"));
}
#[test]
fn type_array_parameter_parses() {
vimanam()
.arg(TYPE_ARRAY)
.args(["--detail", "standard"])
.assert()
.success()
.stdout(predicate::str::contains(
"| `q` | query | No | Search term |",
));
}
#[test]
fn operation_missing_responses_still_parses() {
vimanam()
.arg(MISSING_RESPONSES)
.args(["--detail", "basic"])
.assert()
.success()
.stdout(predicate::str::contains("A_NoResponses"))
.stdout(predicate::str::contains("B_HasResponses"));
}
#[test]
fn duplicate_parameter_is_deduplicated() {
let output = vimanam()
.arg(OVERRIDE_PARAM)
.args(["--detail", "standard"])
.assert()
.success()
.get_output()
.stdout
.clone();
let text = String::from_utf8(output).unwrap();
let id_rows = text.matches("| `id` | path |").count();
assert_eq!(id_rows, 1, "expected a single `id` row, got {id_rows}");
assert!(text.contains("Operation-level id wins"));
}
#[test]
fn unknown_operation_tag_keeps_its_own_service() {
vimanam()
.arg(UNKNOWN_TAG)
.args(["--detail", "basic"])
.assert()
.success()
.stdout(predicate::str::contains("## Gamma"))
.stdout(predicate::str::contains("W_Get"))
.stdout(predicate::str::contains(
"## Alpha {#alpha}\n\nNo endpoints found for this service.",
));
}