surpher / surpher/PactSwift

Nested arrayContains reifies to null in generated body (bundled pact_ffi 0.4.7)

Open
#133 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

good first issue Trivial
Dominant language
Swift
Stars
56
Forks
21
PR merge metrics
No merged PRs in 30d

Description

Summary

An arrayContains matcher that is nested inside another arrayContains reifies to null in the generated mock-server body, instead of to a concrete array. Strict consumer-side decoding of that body then fails. Top-level arrayContains works correctly; only nesting is affected.

The root cause is in the bundled pact_ffi (0.4.7), not in the Swift layer.

Environment
  • Bundled FFI: pact_ffi 0.4.7, pact_models 1.1.9 (read from PactSwiftMockServer.xcframework)
  • Dependency chain: PactSwiftPactSwiftServer 0.4.7PactMockServer 0.1.2 (the libpact_ffi xcframework)
  • Discovered while using a fork that adds an arrayContains matcher (pepejeria/PactSwift@feat/array-contains-matcher), but the underlying defect is in the bundled FFI and affects the whole surpher stack.
Root cause

In pact_ffi 0.4.7, rust/pact_ffi/src/mock_server/bodies.rsprocess_object, the example body for an arrayContains matcher is generated by re-processing its variants array with skip_matchers = true:

let (value, skip_matchers) = if let Ok(rule) = &matching_rule {
  match rule {
    MatchingRule::ArrayContains(_) => (obj.get("variants"), true), // reprocess variants, skip_matchers = true
    _ => (obj.get("value"), false)
  }
} ...;

Under skip_matchers = true, any object carrying pact:matcher:type is reified only from its "value" key:

} else { // skip_matchers == true
  match obj.get("value") {
    Some(val) => ...,
    None => Value::Null   // <-- an arrayContains object has no "value" -> null
  }
}

An arrayContains matcher serialises as { "pact:matcher:type": "arrayContains", "variants": [...] } with no "value" key. So:

  • Top-level arrayContains works — reached via the non-skip path, which reads variants.
  • Nested arrayContains collapses to null — reached only via the skip-matchers path, which looks for "value" and finds none.
Already fixed upstream

In pact_ffi ≥ 0.4.9, process_matcher was refactored so the arrayContains branch recursively builds its example from variants and returns Value::Array(json_values), independent of any "value" key. On that version nested arrayContains works with no client change. So bumping the bundled libpact_ffi (via PactMockServer / PactSwiftServer) to ≥ 0.4.9 is the clean fix.

Reproduction

An end-to-end consumer test with a nested arrayContains, run against the real mock server:

try response.jsonBody(
    .arrayContains([
        [
            "id": .like("outer-1"),
            "children": .arrayContains([
                ["childId": .like("inner-1")],
            ]),
        ],
    ])
)

Fetch the generated body inside builder.verify { ... } and inspect it:

  • Actual (0.4.7): the top-level array is present, but children is null.
  • Expected: children is a non-empty array ([{ "childId": "inner-1" }]).
Proposed fixes

Bump the bundled libpact_ffi to ≥ 0.4.9 in PactMockServer / PactSwiftServer, where arrayContains example generation is recursive and no longer depends on a "value" key.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Inspect how PactSwiftServer 0.4.7 and PactMockServer 0.1.2 bundle libpact_ffi, then verify the dependency can use pact_ffi >= 0.4.9. Run the nested arrayContains end-to-end consumer test against the real mock server and inspect the generated body. Done means the nested children value is a non-empty array rather than null.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, swift
Domain
testing
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.