Wave 5: Schemas command, sync command, network policy, test fixtures (bd-x15, bd-3f4, bd-1cv, bd-lx6)
- Implement schemas command with list/show modes, regex filtering, ref expansion - Implement sync command with conditional fetch, content hash diffing, dry-run - Add NetworkPolicy enum (Auto/Offline/OnlineOnly) with env var + CLI flag resolution - Integrate network policy into AsyncHttpClient and fetch command - Create test fixtures (petstore.json/yaml, minimal.json) and integration test helpers - Fix clippy lints: derivable_impls, len_zero, borrow-after-move, deprecated API - 192 tests passing (179 unit + 13 integration), all quality gates green
This commit is contained in:
92
tests/fixtures/minimal.json
vendored
Normal file
92
tests/fixtures/minimal.json
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Minimal API",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"paths": {
|
||||
"/items": {
|
||||
"get": {
|
||||
"operationId": "listItems",
|
||||
"summary": "List items",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of items",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ItemList" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createItem",
|
||||
"summary": "Create item",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/Item" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Item created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/Item" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/items/{id}": {
|
||||
"get": {
|
||||
"operationId": "getItem",
|
||||
"summary": "Get item by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A single item",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/Item" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer", "format": "int64" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"ItemList": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/Item" }
|
||||
},
|
||||
"total": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
tests/fixtures/petstore.yaml
vendored
Normal file
144
tests/fixtures/petstore.yaml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: Petstore
|
||||
version: "1.0.0"
|
||||
description: A minimal Petstore API for testing.
|
||||
security:
|
||||
- api_key: []
|
||||
tags:
|
||||
- name: pets
|
||||
description: Pet operations
|
||||
- name: store
|
||||
description: Store operations
|
||||
paths:
|
||||
/pets:
|
||||
get:
|
||||
operationId: listPets
|
||||
summary: List all pets
|
||||
tags:
|
||||
- pets
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
description: Maximum number of items to return
|
||||
- name: offset
|
||||
in: query
|
||||
required: false
|
||||
description: Pagination offset
|
||||
responses:
|
||||
"200":
|
||||
description: A list of pets
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
post:
|
||||
operationId: createPet
|
||||
summary: Create a pet
|
||||
tags:
|
||||
- pets
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewPet"
|
||||
responses:
|
||||
"201":
|
||||
description: Pet created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
/pets/{petId}:
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
required: true
|
||||
description: The ID of the pet
|
||||
get:
|
||||
operationId: showPetById
|
||||
summary: Get a pet by ID
|
||||
tags:
|
||||
- pets
|
||||
responses:
|
||||
"200":
|
||||
description: A single pet
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
"404":
|
||||
description: Pet not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
delete:
|
||||
operationId: deletePet
|
||||
summary: Delete a pet
|
||||
tags:
|
||||
- pets
|
||||
deprecated: true
|
||||
responses:
|
||||
"204":
|
||||
description: Pet deleted
|
||||
/store/inventory:
|
||||
get:
|
||||
operationId: getInventory
|
||||
summary: Get store inventory
|
||||
tags:
|
||||
- store
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
description: Inventory counts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
components:
|
||||
schemas:
|
||||
Pet:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
NewPet:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
message:
|
||||
type: string
|
||||
securitySchemes:
|
||||
api_key:
|
||||
type: apiKey
|
||||
name: X-API-Key
|
||||
in: header
|
||||
134
tests/helpers/mod.rs
Normal file
134
tests/helpers/mod.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use assert_cmd::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// An isolated test environment with its own home, cache, and config directories.
|
||||
///
|
||||
/// Setting `SWAGGER_CLI_HOME` on each command invocation ensures tests never
|
||||
/// touch the real user cache or config.
|
||||
pub struct TestEnv {
|
||||
// Kept alive for RAII cleanup; not read directly.
|
||||
_temp_dir: TempDir,
|
||||
pub home_dir: PathBuf,
|
||||
pub cache_dir: PathBuf,
|
||||
pub config_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
/// Creates a fresh isolated test environment backed by a temporary directory.
|
||||
pub fn new() -> Self {
|
||||
let temp_dir = TempDir::new().expect("failed to create temp dir");
|
||||
let home_dir = temp_dir.path().join("swagger-cli-home");
|
||||
let cache_dir = home_dir.join("cache");
|
||||
let config_dir = home_dir.join("config");
|
||||
|
||||
std::fs::create_dir_all(&cache_dir).expect("failed to create cache dir");
|
||||
std::fs::create_dir_all(&config_dir).expect("failed to create config dir");
|
||||
|
||||
Self {
|
||||
_temp_dir: temp_dir,
|
||||
home_dir,
|
||||
cache_dir,
|
||||
config_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `swagger-cli` with the given arguments inside the provided test environment.
|
||||
///
|
||||
/// `SWAGGER_CLI_HOME` is set so all reads/writes go to the temp directory.
|
||||
#[allow(deprecated)]
|
||||
pub fn run_cmd(env: &TestEnv, args: &[&str]) -> assert_cmd::assert::Assert {
|
||||
Command::cargo_bin("swagger-cli")
|
||||
.expect("binary not found -- run `cargo build` first")
|
||||
.env("SWAGGER_CLI_HOME", &env.home_dir)
|
||||
.args(args)
|
||||
.assert()
|
||||
}
|
||||
|
||||
/// Fetch a fixture file into the test environment's cache under the given alias.
|
||||
///
|
||||
/// Runs: `swagger-cli fetch <fixture_path> --alias <alias>`
|
||||
pub fn fetch_fixture(env: &TestEnv, fixture_name: &str, alias: &str) {
|
||||
let path = fixture_path(fixture_name);
|
||||
assert!(
|
||||
path.exists(),
|
||||
"fixture file does not exist: {}",
|
||||
path.display()
|
||||
);
|
||||
|
||||
let path_str = path.to_str().expect("fixture path is not valid UTF-8");
|
||||
|
||||
run_cmd(env, &["fetch", path_str, "--alias", alias]).success();
|
||||
}
|
||||
|
||||
/// Parse robot-mode JSON output from command stdout bytes.
|
||||
///
|
||||
/// The robot envelope has the shape: `{"ok": true, "data": {...}, "meta": {...}}`
|
||||
pub fn parse_robot_json(output: &[u8]) -> serde_json::Value {
|
||||
let text = std::str::from_utf8(output).expect("stdout is not valid UTF-8");
|
||||
serde_json::from_str(text.trim()).expect("stdout is not valid JSON")
|
||||
}
|
||||
|
||||
/// Return the absolute path to a test fixture file by name.
|
||||
///
|
||||
/// Fixtures live in `tests/fixtures/` relative to the project root.
|
||||
pub fn fixture_path(name: &str) -> PathBuf {
|
||||
let manifest_dir =
|
||||
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set -- run via cargo");
|
||||
Path::new(&manifest_dir)
|
||||
.join("tests")
|
||||
.join("fixtures")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Self-tests for the helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_env_creates_directories() {
|
||||
let env = TestEnv::new();
|
||||
assert!(env.home_dir.exists());
|
||||
assert!(env.cache_dir.exists());
|
||||
assert!(env.config_dir.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_path_exists() {
|
||||
let path = fixture_path("petstore.json");
|
||||
assert!(path.exists(), "petstore.json fixture missing");
|
||||
assert!(path.is_absolute());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_path_yaml() {
|
||||
let path = fixture_path("petstore.yaml");
|
||||
assert!(path.exists(), "petstore.yaml fixture missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixture_path_minimal() {
|
||||
let path = fixture_path("minimal.json");
|
||||
assert!(path.exists(), "minimal.json fixture missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_robot_json_valid() {
|
||||
let input = br#"{"ok":true,"data":{"x":1},"meta":{}}"#;
|
||||
let val = parse_robot_json(input);
|
||||
assert_eq!(val["ok"], true);
|
||||
assert_eq!(val["data"]["x"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "not valid JSON")]
|
||||
fn test_parse_robot_json_invalid() {
|
||||
parse_robot_json(b"not json");
|
||||
}
|
||||
}
|
||||
106
tests/integration_test.rs
Normal file
106
tests/integration_test.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
mod helpers;
|
||||
|
||||
#[test]
|
||||
fn test_petstore_json_is_valid() {
|
||||
let path = helpers::fixture_path("petstore.json");
|
||||
let content = std::fs::read_to_string(&path).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||
assert!(json.get("openapi").is_some());
|
||||
assert!(json.get("paths").is_some());
|
||||
assert!(json.get("components").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_petstore_yaml_is_valid() {
|
||||
let path = helpers::fixture_path("petstore.yaml");
|
||||
let content = std::fs::read_to_string(&path).unwrap();
|
||||
let yaml: serde_json::Value = serde_yaml::from_str(&content).unwrap();
|
||||
assert!(yaml.get("openapi").is_some());
|
||||
assert!(yaml.get("paths").is_some());
|
||||
assert!(yaml.get("components").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimal_json_is_valid() {
|
||||
let path = helpers::fixture_path("minimal.json");
|
||||
let content = std::fs::read_to_string(&path).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||
assert_eq!(json["openapi"], "3.0.3");
|
||||
assert_eq!(json["info"]["title"], "Minimal API");
|
||||
|
||||
let paths = json["paths"].as_object().unwrap();
|
||||
assert_eq!(paths.len(), 2); // /items and /items/{id}
|
||||
|
||||
// Count total operations: GET /items, POST /items, GET /items/{id} = 3
|
||||
let items_ops = json["paths"]["/items"].as_object().unwrap();
|
||||
let item_by_id_ops = json["paths"]["/items/{id}"].as_object().unwrap();
|
||||
let op_count = items_ops.len() + item_by_id_ops.len();
|
||||
// /items has get+post (2), /items/{id} has get+parameters (but parameters isn't an op)
|
||||
assert!(op_count >= 3);
|
||||
|
||||
let schemas = json["components"]["schemas"].as_object().unwrap();
|
||||
assert_eq!(schemas.len(), 2); // Item and ItemList
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_petstore_yaml_matches_json_structure() {
|
||||
let json_path = helpers::fixture_path("petstore.json");
|
||||
let yaml_path = helpers::fixture_path("petstore.yaml");
|
||||
|
||||
let json_content = std::fs::read_to_string(&json_path).unwrap();
|
||||
let yaml_content = std::fs::read_to_string(&yaml_path).unwrap();
|
||||
|
||||
let json_val: serde_json::Value = serde_json::from_str(&json_content).unwrap();
|
||||
let yaml_val: serde_json::Value = serde_yaml::from_str(&yaml_content).unwrap();
|
||||
|
||||
// Both should have the same top-level keys
|
||||
assert_eq!(json_val["openapi"], yaml_val["openapi"]);
|
||||
assert_eq!(json_val["info"]["title"], yaml_val["info"]["title"]);
|
||||
assert_eq!(json_val["info"]["version"], yaml_val["info"]["version"]);
|
||||
|
||||
// Same number of paths
|
||||
let json_paths = json_val["paths"].as_object().unwrap();
|
||||
let yaml_paths = yaml_val["paths"].as_object().unwrap();
|
||||
assert_eq!(json_paths.len(), yaml_paths.len());
|
||||
|
||||
// Same schemas
|
||||
let json_schemas = json_val["components"]["schemas"].as_object().unwrap();
|
||||
let yaml_schemas = yaml_val["components"]["schemas"].as_object().unwrap();
|
||||
assert_eq!(json_schemas.len(), yaml_schemas.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_and_list_fixture() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.json", "petstore");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["list", "petstore", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert!(!json["data"]["endpoints"].as_array().unwrap().is_empty());
|
||||
assert!(json["data"]["total"].as_u64().unwrap() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_yaml_fixture() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "petstore.yaml", "petstore-yaml");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["list", "petstore-yaml", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_minimal_fixture() {
|
||||
let env = helpers::TestEnv::new();
|
||||
helpers::fetch_fixture(&env, "minimal.json", "minimal");
|
||||
|
||||
let assert = helpers::run_cmd(&env, &["list", "minimal", "--robot"]).success();
|
||||
|
||||
let json = helpers::parse_robot_json(&assert.get_output().stdout);
|
||||
assert_eq!(json["ok"], true);
|
||||
assert_eq!(json["data"]["total"], 3);
|
||||
}
|
||||
Reference in New Issue
Block a user