MoonBit HTTP recording, replay, response contract validation, and offline testing library
moon add chenqi-arch/moonbit-project@0.3.2import {
"chenqi-arch/moonbit-project" @moonvcr,
}
pkgtype(kind: "executable")fn main {
let cassette_text = @moonvcr.Cassette::encode(@moonvcr.example_cassette())
let cassette = @moonvcr.Cassette::decode(cassette_text) catch {
_ => abort("cassette decode failed")
}
let request : @moonvcr.Request = {
method: "GET",
url: "https://api.example.test/v1/items?page=1",
headers: [{ name: "accept", value: "application/json", }],
body: @moonvcr.Empty,
}
let session = @moonvcr.Session::with_defaults(
cassette,
@moonvcr.StrictOffline,
)
let response = match session.replay(request) {
Ok(response) => response
Err(error) => abort(error.summary())
}
guard response.status == 200 else { abort("unexpected status") }
match session.assert_complete() {
Ok(_) => ()
Err(error) => abort(error.summary())
}
println("offline consumer passed status=200")
}let request_rules = RedactionConfig::default()
request_rules.json_paths.push("/credentials/token")
let response_rules = RedactionConfig::default()
response_rules.optional_json_paths.push("/session/token")
let session = Session::new_with_redaction_policy(
Cassette::empty(),
Record,
MatchConfig::default(),
{ request: request_rules, response: response_rules, },
)
let result = session.record_response(request, response)
let text = session.cassette().encode()let contract : ResponseContract = {
allowed_statuses: [200, 201],
required_headers: ["content-type"],
required_header_values: [
{ name: "content-type", expected_value: "application/json", },
],
body_kind: Some(ExpectTextBody),
json_fields: [{ path: "/id", expected_kind: JsonNumber, }],
optional_json_fields: [{ path: "/note", expected_kind: JsonString, }],
json_array_elements: [{ path: "/items", expected_kind: JsonObject, }],
}
let report = response.validate_contract(contract)
guard report.is_valid() else { abort(report.summary()) }let response = @native.send_http_with_timeout(request, 2_000)
let recorded = @native.record_http(session, request, timeout_millis=2_000)
@native.save_cassette_new("fixtures/orders.json", session.cassette())
let cassette = @native.load_cassette_default("fixtures/orders.json")moon run cmd/moonvcr-pagination
moon run cmd/moonvcr-order-contract
moon run cmd/moonvcr-restricted-cipagination SDK offline regression passed pages=2
order compatibility contract passed status headers JSON array optional-field
restricted-network CI passed network_calls=0moon run cmd/moonvcr-pagination-failure
moon run cmd/moonvcr-order-contract-failure
moon run cmd/moonvcr-restricted-ci-failuremoon fmt --check
moon check
moon build
moon test
moon run cmd/moonvcr-demo
moon run cmd/moonvcr-pagination
moon run cmd/moonvcr-order-contract
moon run cmd/moonvcr-restricted-ci
moon run cmd/moonvcr-security-acceptance
moon package --list| 能力 | wasm(Windows 本地已验证) | Linux native(GitHub CI) | Windows native |
|---|---|---|---|
| 核心模型/匹配/会话/脱敏/契约 | 支持 | 支持 | 类型检查通过 |
| 内存与脚本 transport | 支持 | 支持 | 类型检查通过 |
| 官方 async HTTP + 文件 IO | 不适用 | 支持并集成验收 | 未声明运行支持 |
| 跨进程文件回放 | 不适用 | 支持并集成验收 | 未声明运行支持 |
pub(all) struct ContractViolation {
kind : ContractViolationKind
path : String
expected : String
actual : String
} derive(Eq, Debug)pub(all) struct JsonArrayElementContract {
path : String
expected_kind : JsonValueKind
} derive(Eq, Debug)pub(all) struct MatchConfig {
include_headers : Array[String]
ignore_headers : Array[String]
body_mode : BodyMatchMode
} derive(Eq, Debug)pub(all) struct Mismatch {
candidate_index : Int
kind : MismatchKind
expected : String
actual : String
} derive(Eq, Debug)pub(all) struct RedactionPolicy {
request : RedactionConfig
response : RedactionConfig
} derive(Eq, Debug)pub(all) struct ResponseContract {
allowed_statuses : Array[Int]
required_headers : Array[String]
required_header_values : Array[HeaderValueContract]
body_kind : ContractBodyKind?
json_fields : Array[JsonFieldContract]
optional_json_fields : Array[JsonFieldContract]
json_array_elements : Array[JsonArrayElementContract]
} derive(Eq, Debug)fn ScriptedTransport::send(self : ScriptedTransport, _request : Request) -> Result[Response, TransportError]pub struct Session {
// private fields
}fn Session::new_with_redaction(cassette : Cassette, mode : SessionMode, config : MatchConfig, redaction : RedactionConfig) -> Sessionfn Session::new_with_redaction_policy(cassette : Cassette, mode : SessionMode, config : MatchConfig, redaction : RedactionPolicy) -> Sessionfn Session::send(self : Session, request : Request, transport : (Request) -> Result[Response, TransportError]) -> Result[Response, SessionError]pub(all) enum SessionError {
ReplayMiss(Array[MatchCandidate])
TransportFailed(TransportError)
RedactionFailed(RedactionError)
IncompleteReplay(Int)
InvalidMode
} derive(Eq, Debug)let CURRENT_FORMAT_VERSION : Intlet REDACTION_MARKER : Stringfn diagnose_mismatch(request : Request, cassette : Cassette, config : MatchConfig, consumed : Array[Bool]) -> MismatchReportfn diagnose_mismatch_with_redaction(request : Request, cassette : Cassette, config : MatchConfig, consumed : Array[Bool], redaction : RedactionConfig) -> MismatchReportfn match_candidates(request : Request, cassette : Cassette, config : MatchConfig, consumed : Array[Bool]) -> Array[MatchCandidate]fn match_request(request : Request, cassette : Cassette, config : MatchConfig, consumed : Array[Bool]) -> MatchResultfn normalize_url(url : String) -> Stringfn redact_interaction(interaction : Interaction, config : RedactionConfig) -> Result[Interaction, RedactionError]fn redact_interaction_with_policy(interaction : Interaction, policy : RedactionPolicy) -> Result[Interaction, RedactionError]fn redact_response(response : Response, config : RedactionConfig) -> Result[Response, RedactionError]Install
Download zipMoonBit HTTP recording, replay, response contract validation, and offline testing library