Wave 6: Integration tests, golden tests, index invariant tests, diff command (bd-rex, bd-2gp, bd-1ck)
This commit is contained in:
72
tests/fixtures/golden/list.json
vendored
Normal file
72
tests/fixtures/golden/list.json
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"data": {
|
||||
"applied_filters": {},
|
||||
"endpoints": [
|
||||
{
|
||||
"deprecated": false,
|
||||
"method": "GET",
|
||||
"operation_id": "listPets",
|
||||
"path": "/pets",
|
||||
"summary": "List all pets",
|
||||
"tags": [
|
||||
"pets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"deprecated": false,
|
||||
"method": "POST",
|
||||
"operation_id": "createPet",
|
||||
"path": "/pets",
|
||||
"summary": "Create a pet",
|
||||
"tags": [
|
||||
"pets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"deprecated": false,
|
||||
"method": "GET",
|
||||
"operation_id": "showPetById",
|
||||
"path": "/pets/{petId}",
|
||||
"summary": "Get a pet by ID",
|
||||
"tags": [
|
||||
"pets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"deprecated": true,
|
||||
"method": "DELETE",
|
||||
"operation_id": "deletePet",
|
||||
"path": "/pets/{petId}",
|
||||
"summary": "Delete a pet",
|
||||
"tags": [
|
||||
"pets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"deprecated": false,
|
||||
"method": "GET",
|
||||
"operation_id": "getInventory",
|
||||
"path": "/store/inventory",
|
||||
"summary": "Get store inventory",
|
||||
"tags": [
|
||||
"store"
|
||||
]
|
||||
}
|
||||
],
|
||||
"filtered": 5,
|
||||
"meta": {
|
||||
"alias": "petstore",
|
||||
"cached_at": "MASKED_TIMESTAMP",
|
||||
"duration_ms": 0,
|
||||
"spec_version": "1.0.0"
|
||||
},
|
||||
"total": 5
|
||||
},
|
||||
"meta": {
|
||||
"command": "list",
|
||||
"duration_ms": 0,
|
||||
"schema_version": 1,
|
||||
"tool_version": "MASKED"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
23
tests/fixtures/golden/schemas_list.json
vendored
Normal file
23
tests/fixtures/golden/schemas_list.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"data": {
|
||||
"schemas": [
|
||||
{
|
||||
"name": "Error"
|
||||
},
|
||||
{
|
||||
"name": "NewPet"
|
||||
},
|
||||
{
|
||||
"name": "Pet"
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
},
|
||||
"meta": {
|
||||
"command": "schemas",
|
||||
"duration_ms": 0,
|
||||
"schema_version": 1,
|
||||
"tool_version": "MASKED"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
31
tests/fixtures/golden/schemas_show.json
vendored
Normal file
31
tests/fixtures/golden/schemas_show.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"data": {
|
||||
"name": "Pet",
|
||||
"schema": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"command": "schemas",
|
||||
"duration_ms": 0,
|
||||
"schema_version": 1,
|
||||
"tool_version": "MASKED"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
110
tests/fixtures/golden/search.json
vendored
Normal file
110
tests/fixtures/golden/search.json
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"data": {
|
||||
"results": [
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"field": "path",
|
||||
"snippet": "/pets"
|
||||
},
|
||||
{
|
||||
"field": "summary",
|
||||
"snippet": "List all pets"
|
||||
}
|
||||
],
|
||||
"method": "GET",
|
||||
"name": "/pets",
|
||||
"rank": 1,
|
||||
"score": 3000,
|
||||
"summary": "List all pets",
|
||||
"type": "endpoint"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"field": "path",
|
||||
"snippet": "/pets"
|
||||
},
|
||||
{
|
||||
"field": "summary",
|
||||
"snippet": "Create a pet"
|
||||
}
|
||||
],
|
||||
"method": "POST",
|
||||
"name": "/pets",
|
||||
"rank": 2,
|
||||
"score": 3000,
|
||||
"summary": "Create a pet",
|
||||
"type": "endpoint"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"field": "path",
|
||||
"snippet": "/pets/{petId}"
|
||||
},
|
||||
{
|
||||
"field": "summary",
|
||||
"snippet": "Get a pet by ID"
|
||||
}
|
||||
],
|
||||
"method": "GET",
|
||||
"name": "/pets/{petId}",
|
||||
"rank": 3,
|
||||
"score": 3000,
|
||||
"summary": "Get a pet by ID",
|
||||
"type": "endpoint"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"field": "path",
|
||||
"snippet": "/pets/{petId}"
|
||||
},
|
||||
{
|
||||
"field": "summary",
|
||||
"snippet": "Delete a pet"
|
||||
}
|
||||
],
|
||||
"method": "DELETE",
|
||||
"name": "/pets/{petId}",
|
||||
"rank": 4,
|
||||
"score": 3000,
|
||||
"summary": "Delete a pet",
|
||||
"type": "endpoint"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"field": "schema_name",
|
||||
"snippet": "NewPet"
|
||||
}
|
||||
],
|
||||
"name": "NewPet",
|
||||
"rank": 5,
|
||||
"score": 1600,
|
||||
"type": "schema"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"field": "schema_name",
|
||||
"snippet": "Pet"
|
||||
}
|
||||
],
|
||||
"name": "Pet",
|
||||
"rank": 6,
|
||||
"score": 1600,
|
||||
"type": "schema"
|
||||
}
|
||||
],
|
||||
"total": 6
|
||||
},
|
||||
"meta": {
|
||||
"command": "search",
|
||||
"duration_ms": 0,
|
||||
"schema_version": 1,
|
||||
"tool_version": "MASKED"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
51
tests/fixtures/golden/show.json
vendored
Normal file
51
tests/fixtures/golden/show.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"data": {
|
||||
"deprecated": false,
|
||||
"description": null,
|
||||
"method": "GET",
|
||||
"operation_id": "listPets",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Maximum number of items to return",
|
||||
"in": "query",
|
||||
"name": "limit",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"description": "Pagination offset",
|
||||
"in": "query",
|
||||
"name": "offset",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"path": "/pets",
|
||||
"request_body": null,
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "A list of pets"
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"summary": "List all pets",
|
||||
"tags": [
|
||||
"pets"
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"command": "show",
|
||||
"duration_ms": 0,
|
||||
"schema_version": 1,
|
||||
"tool_version": "MASKED"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
24
tests/fixtures/golden/tags.json
vendored
Normal file
24
tests/fixtures/golden/tags.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"data": {
|
||||
"tags": [
|
||||
{
|
||||
"description": "Pet operations",
|
||||
"endpoint_count": 4,
|
||||
"name": "pets"
|
||||
},
|
||||
{
|
||||
"description": "Store operations",
|
||||
"endpoint_count": 1,
|
||||
"name": "store"
|
||||
}
|
||||
],
|
||||
"total": 2
|
||||
},
|
||||
"meta": {
|
||||
"command": "tags",
|
||||
"duration_ms": 0,
|
||||
"schema_version": 1,
|
||||
"tool_version": "MASKED"
|
||||
},
|
||||
"ok": true
|
||||
}
|
||||
321
tests/golden_test.rs
Normal file
321
tests/golden_test.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
mod helpers;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Recursively mask dynamic fields (timestamps, durations, versions) for stable comparison.
|
||||
fn normalize_value(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for (key, val) in map.iter_mut() {
|
||||
match key.as_str() {
|
||||
"duration_ms" if val.is_number() => *val = Value::Number(0.into()),
|
||||
"tool_version" if val.is_string() => *val = Value::String("MASKED".into()),
|
||||
"cached_at" | "fetched_at" | "last_accessed" if val.is_string() => {
|
||||
*val = Value::String("MASKED_TIMESTAMP".into())
|
||||
}
|
||||
_ => normalize_value(val),
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for item in arr.iter_mut() {
|
||||
normalize_value(item);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize robot JSON for golden comparison: mask all dynamic fields.
|
||||
fn normalize_robot(mut json: Value) -> Value {
|
||||
normalize_value(&mut json);
|
||||
json
|
||||
}
|
||||
|
||||
/// Assert that a robot success envelope has the correct structural invariants.
|
||||
fn assert_robot_success_structure(json: &Value, expected_command: &str) {
|
||||
// "ok" must be bool true
|
||||
assert_eq!(
|
||||
json.get("ok").and_then(|v| v.as_bool()),
|
||||
Some(true),
|
||||
"ok field must be true"
|
||||
);
|
||||
|
||||
// "data" must be an object
|
||||
assert!(
|
||||
json.get("data").is_some_and(|v| v.is_object()),
|
||||
"data field must be an object"
|
||||
);
|
||||
|
||||
// "meta" must be an object with required fields
|
||||
let meta = json.get("meta").expect("meta field is required");
|
||||
assert!(meta.is_object(), "meta must be an object");
|
||||
|
||||
// meta.schema_version must be number == 1
|
||||
let sv = meta
|
||||
.get("schema_version")
|
||||
.expect("meta.schema_version required");
|
||||
assert!(sv.is_number(), "meta.schema_version must be a number");
|
||||
assert_eq!(sv.as_u64(), Some(1), "meta.schema_version must equal 1");
|
||||
|
||||
// meta.tool_version must be a string
|
||||
let tv = meta
|
||||
.get("tool_version")
|
||||
.expect("meta.tool_version required");
|
||||
assert!(tv.is_string(), "meta.tool_version must be a string");
|
||||
|
||||
// meta.command must be a string matching expected
|
||||
let cmd = meta.get("command").expect("meta.command required");
|
||||
assert!(cmd.is_string(), "meta.command must be a string");
|
||||
assert_eq!(
|
||||
cmd.as_str().unwrap(),
|
||||
expected_command,
|
||||
"meta.command must match the command run"
|
||||
);
|
||||
|
||||
// meta.duration_ms must be a number
|
||||
let dur = meta.get("duration_ms").expect("meta.duration_ms required");
|
||||
assert!(dur.is_number(), "meta.duration_ms must be a number");
|
||||
}
|
||||
|
||||
/// Assert that a robot error envelope has the correct structural invariants.
|
||||
fn assert_robot_error_structure(json: &Value) {
|
||||
// "ok" must be bool false
|
||||
assert_eq!(
|
||||
json.get("ok").and_then(|v| v.as_bool()),
|
||||
Some(false),
|
||||
"ok field must be false on error"
|
||||
);
|
||||
|
||||
// "data" should be absent or null
|
||||
let data = json.get("data");
|
||||
assert!(
|
||||
data.is_none() || data.unwrap().is_null(),
|
||||
"data field must be null or absent on error"
|
||||
);
|
||||
|
||||
// "error" must be an object with code and message
|
||||
let error = json.get("error").expect("error field is required");
|
||||
assert!(error.is_object(), "error must be an object");
|
||||
assert!(
|
||||
error.get("code").is_some_and(|v| v.is_string()),
|
||||
"error.code must be a string"
|
||||
);
|
||||
assert!(
|
||||
error.get("message").is_some_and(|v| v.is_string()),
|
||||
"error.message must be a string"
|
||||
);
|
||||
|
||||
// "meta" structural invariants (same as success)
|
||||
let meta = json.get("meta").expect("meta field is required");
|
||||
assert!(meta.is_object(), "meta must be an object");
|
||||
assert!(
|
||||
meta.get("schema_version").is_some_and(|v| v.is_number()),
|
||||
"meta.schema_version must be a number"
|
||||
);
|
||||
assert!(
|
||||
meta.get("tool_version").is_some_and(|v| v.is_string()),
|
||||
"meta.tool_version must be a string"
|
||||
);
|
||||
assert!(
|
||||
meta.get("command").is_some_and(|v| v.is_string()),
|
||||
"meta.command must be a string"
|
||||
);
|
||||
assert!(
|
||||
meta.get("duration_ms").is_some_and(|v| v.is_number()),
|
||||
"meta.duration_ms must be a number"
|
||||
);
|
||||
}
|
||||
|
||||
/// Load a golden snapshot file, parse as JSON.
|
||||
fn load_golden(name: &str) -> Value {
|
||||
let path = helpers::fixture_path(&format!("golden/{name}"));
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to read golden file {}: {e}", path.display()));
|
||||
serde_json::from_str(&content)
|
||||
.unwrap_or_else(|e| panic!("failed to parse golden file {}: {e}", path.display()))
|
||||
}
|
||||
|
||||
/// Write a golden snapshot file if it does not exist (first-run bootstrap).
|
||||
fn write_golden_if_missing(name: &str, value: &Value) {
|
||||
let path = helpers::fixture_path(&format!("golden/{name}"));
|
||||
if !path.exists() {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("failed to create golden dir");
|
||||
}
|
||||
let pretty =
|
||||
serde_json::to_string_pretty(value).expect("failed to serialize golden snapshot");
|
||||
std::fs::write(&path, pretty).expect("failed to write golden snapshot");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Golden structure tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_golden_list_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["list", "petstore", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "list");
|
||||
|
||||
// Snapshot comparison
|
||||
let normalized = normalize_robot(json);
|
||||
write_golden_if_missing("list.json", &normalized);
|
||||
let golden = load_golden("list.json");
|
||||
assert_eq!(
|
||||
normalized, golden,
|
||||
"list robot output does not match golden snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_show_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(
|
||||
&env,
|
||||
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
|
||||
)
|
||||
.success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "show");
|
||||
|
||||
let normalized = normalize_robot(json);
|
||||
write_golden_if_missing("show.json", &normalized);
|
||||
let golden = load_golden("show.json");
|
||||
assert_eq!(
|
||||
normalized, golden,
|
||||
"show robot output does not match golden snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_search_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["search", "petstore", "pet", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "search");
|
||||
|
||||
let normalized = normalize_robot(json);
|
||||
write_golden_if_missing("search.json", &normalized);
|
||||
let golden = load_golden("search.json");
|
||||
assert_eq!(
|
||||
normalized, golden,
|
||||
"search robot output does not match golden snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_schemas_list_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "schemas");
|
||||
|
||||
let normalized = normalize_robot(json);
|
||||
write_golden_if_missing("schemas_list.json", &normalized);
|
||||
let golden = load_golden("schemas_list.json");
|
||||
assert_eq!(
|
||||
normalized, golden,
|
||||
"schemas list robot output does not match golden snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_schemas_show_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--show", "Pet", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "schemas");
|
||||
|
||||
let normalized = normalize_robot(json);
|
||||
write_golden_if_missing("schemas_show.json", &normalized);
|
||||
let golden = load_golden("schemas_show.json");
|
||||
assert_eq!(
|
||||
normalized, golden,
|
||||
"schemas show robot output does not match golden snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_tags_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["tags", "petstore", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "tags");
|
||||
|
||||
let normalized = normalize_robot(json);
|
||||
write_golden_if_missing("tags.json", &normalized);
|
||||
let golden = load_golden("tags.json");
|
||||
assert_eq!(
|
||||
normalized, golden,
|
||||
"tags robot output does not match golden snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_aliases_robot_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["aliases", "--list", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_robot_success_structure(&json, "aliases");
|
||||
|
||||
// Aliases output has dynamic timestamps so we only check structure,
|
||||
// not golden snapshot equality. Normalize and check key shape.
|
||||
let data = json.get("data").unwrap();
|
||||
assert!(data.get("aliases").is_some_and(|v| v.is_array()));
|
||||
assert!(data.get("count").is_some_and(|v| v.is_number()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_error_structure() {
|
||||
let env = helpers::TestEnv::new();
|
||||
// Do NOT fetch -- alias does not exist
|
||||
|
||||
let a = helpers::run_cmd(&env, &["list", "nonexistent", "--robot"]);
|
||||
|
||||
// Command should fail with non-zero exit
|
||||
let a = a.failure();
|
||||
|
||||
// Error JSON goes to stderr
|
||||
let stderr = std::str::from_utf8(&a.get_output().stderr).expect("stderr not UTF-8");
|
||||
let json: Value = serde_json::from_str(stderr.trim()).expect("stderr is not valid robot JSON");
|
||||
|
||||
assert_robot_error_structure(&json);
|
||||
|
||||
// Verify error code
|
||||
assert_eq!(
|
||||
json["error"]["code"].as_str(),
|
||||
Some("ALIAS_NOT_FOUND"),
|
||||
"error code should be ALIAS_NOT_FOUND"
|
||||
);
|
||||
|
||||
// Verify meta.command
|
||||
assert_eq!(
|
||||
json["meta"]["command"].as_str(),
|
||||
Some("list"),
|
||||
"meta.command should be list"
|
||||
);
|
||||
}
|
||||
84
tests/index_invariant_test.rs
Normal file
84
tests/index_invariant_test.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
mod helpers;
|
||||
|
||||
/// Delete the raw.json file from a cached alias to prove index-only reads.
|
||||
fn delete_raw_json(env: &helpers::TestEnv, alias: &str) {
|
||||
let raw_path = env.home_dir.join("cache").join(alias).join("raw.json");
|
||||
assert!(
|
||||
raw_path.exists(),
|
||||
"raw.json must exist before deletion: {}",
|
||||
raw_path.display()
|
||||
);
|
||||
std::fs::remove_file(&raw_path).expect("failed to delete raw.json");
|
||||
assert!(!raw_path.exists(), "raw.json should be gone after deletion");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Index-only reads: these commands must work WITHOUT raw.json
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_list_does_not_read_raw_json() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
delete_raw_json(&env, "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["list", "petstore", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_eq!(json["ok"], true);
|
||||
assert!(json["data"]["total"].as_u64().unwrap() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_does_not_read_raw_json() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
delete_raw_json(&env, "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["search", "petstore", "pet", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_eq!(json["ok"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tags_does_not_read_raw_json() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
delete_raw_json(&env, "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["tags", "petstore", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_eq!(json["ok"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schemas_list_does_not_read_raw_json() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
delete_raw_json(&env, "petstore");
|
||||
|
||||
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--robot"]).success();
|
||||
let json = helpers::parse_robot_json(&a.get_output().stdout);
|
||||
|
||||
assert_eq!(json["ok"], true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Negative case: show REQUIRES raw.json
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_show_requires_raw_json() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
delete_raw_json(&env, "petstore");
|
||||
|
||||
// show needs raw.json to resolve the operation pointer
|
||||
helpers::run_cmd(
|
||||
&env,
|
||||
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
|
||||
)
|
||||
.failure();
|
||||
}
|
||||
@@ -104,3 +104,521 @@ fn test_fetch_minimal_fixture() {
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["total"], 3);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Fetch tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_fetch_success_robot() {
|
||||
let env = helpers::TestEnv::new();
|
||||
let fixture = helpers::fixture_path("petstore.json");
|
||||
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&[
|
||||
"fetch",
|
||||
fixture.to_str().unwrap(),
|
||||
"--alias",
|
||||
"ps",
|
||||
"--robot",
|
||||
],
|
||||
)
|
||||
.success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["alias"], "ps");
|
||||
assert!(json["data"]["endpoint_count"].as_u64().unwrap() > 0);
|
||||
assert!(json["data"]["schema_count"].as_u64().unwrap() > 0);
|
||||
assert!(
|
||||
json["data"]["content_hash"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("sha256:")
|
||||
);
|
||||
assert_eq!(json["data"]["source_format"], "json");
|
||||
assert_eq!(json["meta"]["command"], "fetch");
|
||||
assert!(json["meta"]["schema_version"].as_u64().is_some());
|
||||
assert!(json["meta"]["duration_ms"].as_u64().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_invalid_json() {
|
||||
let env = helpers::TestEnv::new();
|
||||
|
||||
// Create a non-JSON, non-YAML file that is not a valid OpenAPI spec
|
||||
let bad_file = env.home_dir.join("not-a-spec.json");
|
||||
std::fs::write(&bad_file, b"this is not json or yaml").unwrap();
|
||||
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&[
|
||||
"fetch",
|
||||
bad_file.to_str().unwrap(),
|
||||
"--alias",
|
||||
"bad",
|
||||
"--robot",
|
||||
],
|
||||
);
|
||||
|
||||
// Should fail -- the file is not valid JSON or YAML
|
||||
let output = assert.get_output();
|
||||
assert!(!output.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_alias_exists_error() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "dupe");
|
||||
|
||||
let fixture = helpers::fixture_path("petstore.json");
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&[
|
||||
"fetch",
|
||||
fixture.to_str().unwrap(),
|
||||
"--alias",
|
||||
"dupe",
|
||||
"--robot",
|
||||
],
|
||||
);
|
||||
|
||||
let output = assert.get_output();
|
||||
assert!(!output.status.success());
|
||||
|
||||
// Exit code 6 = ALIAS_EXISTS
|
||||
let stderr_json = helpers::parse_robot_json(&output.stderr);
|
||||
assert_eq!(stderr_json["ok"], false);
|
||||
assert_eq!(stderr_json["error"]["code"], "ALIAS_EXISTS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_force_overwrite() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "overwrite-me");
|
||||
|
||||
let fixture = helpers::fixture_path("minimal.json");
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&[
|
||||
"fetch",
|
||||
fixture.to_str().unwrap(),
|
||||
"--alias",
|
||||
"overwrite-me",
|
||||
"--force",
|
||||
"--robot",
|
||||
],
|
||||
)
|
||||
.success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["alias"], "overwrite-me");
|
||||
// After force overwrite, the title should be from minimal.json
|
||||
assert_eq!(json["data"]["title"], "Minimal API");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_yaml_success() {
|
||||
let env = helpers::TestEnv::new();
|
||||
let fixture = helpers::fixture_path("petstore.yaml");
|
||||
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&[
|
||||
"fetch",
|
||||
fixture.to_str().unwrap(),
|
||||
"--alias",
|
||||
"yaml-test",
|
||||
"--robot",
|
||||
],
|
||||
)
|
||||
.success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["source_format"], "yaml");
|
||||
assert!(json["data"]["endpoint_count"].as_u64().unwrap() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_stdin() {
|
||||
let env = helpers::TestEnv::new();
|
||||
let fixture = helpers::fixture_path("petstore.json");
|
||||
let content = std::fs::read(&fixture).unwrap();
|
||||
|
||||
#[allow(deprecated)]
|
||||
let assert = assert_cmd::Command::cargo_bin("swagger-cli")
|
||||
.expect("binary not found")
|
||||
.env("SWAGGER_CLI_HOME", &env.home_dir)
|
||||
.args(["fetch", "-", "--alias", "stdin-test", "--robot"])
|
||||
.write_stdin(content)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["alias"], "stdin-test");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// List tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_list_filter_by_method() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert =
|
||||
helpers::run_cmd(&env, &["list", "petstore", "--method", "GET", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let endpoints = json["data"]["endpoints"].as_array().unwrap();
|
||||
assert!(!endpoints.is_empty());
|
||||
for ep in endpoints {
|
||||
assert_eq!(ep["method"], "GET");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_filter_by_tag() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert =
|
||||
helpers::run_cmd(&env, &["list", "petstore", "--tag", "store", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let endpoints = json["data"]["endpoints"].as_array().unwrap();
|
||||
assert!(!endpoints.is_empty());
|
||||
for ep in endpoints {
|
||||
let tags = ep["tags"].as_array().unwrap();
|
||||
let tag_strings: Vec<&str> = tags.iter().filter_map(|t| t.as_str()).collect();
|
||||
assert!(
|
||||
tag_strings
|
||||
.iter()
|
||||
.any(|t| t.to_lowercase().contains("store")),
|
||||
"Expected 'store' tag in {tag_strings:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_path_regex() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert =
|
||||
helpers::run_cmd(&env, &["list", "petstore", "--path", "pet.*", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let endpoints = json["data"]["endpoints"].as_array().unwrap();
|
||||
assert!(!endpoints.is_empty());
|
||||
for ep in endpoints {
|
||||
let path = ep["path"].as_str().unwrap();
|
||||
assert!(
|
||||
path.contains("pet"),
|
||||
"Expected path to match 'pet.*', got: {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_invalid_regex_error() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["list", "petstore", "--path", "[invalid", "--robot"]);
|
||||
|
||||
let output = assert.get_output();
|
||||
assert!(!output.status.success());
|
||||
|
||||
let stderr_json = helpers::parse_robot_json(&output.stderr);
|
||||
assert_eq!(stderr_json["ok"], false);
|
||||
assert_eq!(stderr_json["error"]["code"], "USAGE_ERROR");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_limit() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["list", "petstore", "--limit", "2", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let endpoints = json["data"]["endpoints"].as_array().unwrap();
|
||||
assert!(endpoints.len() <= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_all_flag() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["list", "petstore", "--all", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let endpoints = json["data"]["endpoints"].as_array().unwrap();
|
||||
let total = json["data"]["total"].as_u64().unwrap() as usize;
|
||||
// With --all, endpoints shown should equal total (no truncation)
|
||||
assert_eq!(endpoints.len(), total);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Show tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_show_endpoint_details() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
|
||||
)
|
||||
.success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["path"], "/pets");
|
||||
assert_eq!(json["data"]["method"], "GET");
|
||||
assert!(json["data"]["summary"].as_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_show_method_disambiguation() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
// /pets has both GET and POST -- omitting --method should error
|
||||
let assert = helpers::run_cmd(&env, &["show", "petstore", "/pets", "--robot"]);
|
||||
|
||||
let output = assert.get_output();
|
||||
assert!(!output.status.success());
|
||||
|
||||
let stderr_json = helpers::parse_robot_json(&output.stderr);
|
||||
assert_eq!(stderr_json["ok"], false);
|
||||
assert_eq!(stderr_json["error"]["code"], "USAGE_ERROR");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Search tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_search_basic() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["search", "petstore", "pet", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let total = json["data"]["total"].as_u64().unwrap();
|
||||
assert!(total > 0, "Expected search results for 'pet'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_no_results() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(
|
||||
&env,
|
||||
&["search", "petstore", "zzzznonexistent99999", "--robot"],
|
||||
)
|
||||
.success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["total"], 0);
|
||||
assert!(json["data"]["results"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_case_insensitive() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
// Default search is case-insensitive; "PET" should match "pet" paths
|
||||
let assert = helpers::run_cmd(&env, &["search", "petstore", "PET", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let total = json["data"]["total"].as_u64().unwrap();
|
||||
assert!(
|
||||
total > 0,
|
||||
"Expected case-insensitive search for 'PET' to find results"
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Schemas tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_schemas_list() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["schemas", "petstore", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let schemas = json["data"]["schemas"].as_array().unwrap();
|
||||
assert!(!schemas.is_empty());
|
||||
|
||||
let names: Vec<&str> = schemas.iter().filter_map(|s| s["name"].as_str()).collect();
|
||||
assert!(
|
||||
names.contains(&"Pet"),
|
||||
"Expected 'Pet' schema, got: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.contains(&"Error"),
|
||||
"Expected 'Error' schema, got: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schemas_show() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert =
|
||||
helpers::run_cmd(&env, &["schemas", "petstore", "--show", "Pet", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["name"], "Pet");
|
||||
assert!(json["data"]["schema"].is_object());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Tags tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_tags_list() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["tags", "petstore", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let tags = json["data"]["tags"].as_array().unwrap();
|
||||
assert!(!tags.is_empty());
|
||||
|
||||
let names: Vec<&str> = tags.iter().filter_map(|t| t["name"].as_str()).collect();
|
||||
assert!(
|
||||
names.contains(&"pets"),
|
||||
"Expected 'pets' tag, got: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.contains(&"store"),
|
||||
"Expected 'store' tag, got: {names:?}"
|
||||
);
|
||||
|
||||
// Each tag should have an endpoint_count
|
||||
for tag in tags {
|
||||
assert!(
|
||||
tag["endpoint_count"].as_u64().is_some(),
|
||||
"Expected endpoint_count on tag: {tag}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Aliases tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_aliases_list() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "alias-one");
|
||||
helpers::fetch_fixture(&env, "minimal.json", "alias-two");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["aliases", "--list", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
let aliases = json["data"]["aliases"].as_array().unwrap();
|
||||
let names: Vec<&str> = aliases.iter().filter_map(|a| a["name"].as_str()).collect();
|
||||
assert!(
|
||||
names.contains(&"alias-one"),
|
||||
"Expected 'alias-one', got: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.contains(&"alias-two"),
|
||||
"Expected 'alias-two', got: {names:?}"
|
||||
);
|
||||
assert_eq!(json["data"]["count"].as_u64().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aliases_set_default() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "my-default");
|
||||
|
||||
let assert =
|
||||
helpers::run_cmd(&env, &["aliases", "--set-default", "my-default", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["name"], "my-default");
|
||||
|
||||
// Verify by listing
|
||||
let list_assert = helpers::run_cmd(&env, &["aliases", "--list", "--robot"]).success();
|
||||
let list_json = helpers::parse_robot_json(&list_assert.get_output().stdout);
|
||||
assert_eq!(list_json["data"]["default_alias"], "my-default");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Doctor tests
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_doctor_healthy() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "healthy-spec");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["doctor", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
|
||||
// health should be "healthy" or "warning" (warning is ok if stale threshold is very low)
|
||||
let health = json["data"]["health"].as_str().unwrap();
|
||||
assert!(
|
||||
health == "healthy" || health == "warning",
|
||||
"Expected healthy or warning, got: {health}"
|
||||
);
|
||||
|
||||
let aliases = json["data"]["aliases"].as_array().unwrap();
|
||||
assert!(!aliases.is_empty());
|
||||
|
||||
let spec_report = aliases
|
||||
.iter()
|
||||
.find(|a| a["name"] == "healthy-spec")
|
||||
.unwrap();
|
||||
assert_eq!(spec_report["status"], "healthy");
|
||||
assert!(spec_report["endpoint_count"].as_u64().unwrap() > 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user