How can we add plugin config to a contract for a g...
# pact-plugins
a
How can we add plugin config to a contract for a given interaction when that interaction uses a core matcher? We are using JSON-RPC payloads over a WebSocket connection so the content matching is done with the core JSON matcher. However on a given interaction we need to be able to set the following: • Path to connect to the WS server on. e.g.
<ws://localhost:9998/jsonrpc>
- specifically the
/jsonrpc
bit • Headers to send in the connection request • Message payload encoding as text or binary We are using synchronous message contracts to do the verification and we are trying to extend the plugin so that these metadata properties can be set, however we are struggling to find the code path that allows plugin config to be added to the contract files when a core matcher is being used. Advice/guidance on how to achieve this would be greatly appreciated
ralph wave 1
I think I have been approaching this from the wrong angle. If we ignore the message encoding (which for now can always be text anyway) then I can just use the ProviderInfo.transports.path and customHeaders. Then when verifying contracts these just need to be passed to the verifier. I guess if some contracts are using Sec-WebSocket-Protocol header and path different to other contracts then this information should be in the contract itself (or maybe these are 2 providers in that case??) but for now we can just take it from the verifier CLI args. I just need to implement support for those args in the verification side of the plugin.
One concern with adding the headers and path only when doing verification is that it does not ensure the client implements these details - specifically the header - which is required for the implementation to work.
y
when you say core matcher, are you talking about traditional matchers used in http and message pact tests, not the plugin matchers? https://docs.pact.io/implementation_guides/pact_plugins/docs/matching-rule-definition-expressions
a
Yes that is what I mean
Our plugin is not adding any new matchers
Also, looking at the pact_verifier it doesn't look like the
path
property of the ProviderInfo is being added to the verifyInteraction request. I can only see host and port being sent in the config
y
the pact plugin framework only support the matchers in my linked document at the moment
a
I think I mean Content Matcher šŸ¤”
Maybe I am phrasing my question/intention incorrectly. We have a consumer that communicates with a provider over WS so we have created a plugin that adds the WS transport. When the consumer interacts with the provider they need to provide a base path and an appropriate header to establish the WS connection. We are using synchronous message interactions to capture the contracts. The messages are JSON payloads. We the consumer to be able to specify the base-path and header in the contract so that during verification they can be sent to the provider. Currently it does not look possible to any of the following: • Add custom data in the consumer contract for use with a transport plugin • Specify path / headers / metadata for sync message contracts • Pass the path property for a transport from the verifier API over to a plugin handling verification requests
y
Ok so I think you need to define your own interaction type, and you can set your own properties, your code just needs to know to look for them, and then write the appropriate values https://github.com/pact-foundation/pact-plugins/blob/bfb1fa52506ec42add5abda5e8245[…]s/protobuf/src/main/kotlin/io/pact/protobuf/plugin/PluginApp.kt your provider side, will expect those keys and can do whatever you need You can define your own shape of your plugin interaction, it may just align to the async/sync message and http specs shapes defined by the v4 spec
don't know if that helps
• Add custom data in the consumer contract for use with a transport plugin
Your consumers can add whatever they want in, ideally to a known spec, so the plugin knows where to read it
• Specify path / headers / metadata for sync message contracts
only if you are adhering to the pact specification, the idea with plugins is you can step outside these bounds
• Pass the path property for a transport from the verifier API over to a plugin handling verification requests
Your verifier side of the plugin needs to aware of the key in the contract, which is can use to do whatever it needs to do, to get in the right state
I guess if some contracts are using Sec-WebSocket-Protocol header and path different to other contracts then this information should be in the contract itself (or maybe these are 2 providers in that case??)
could that header be optional? and therefore the plugin verifier is agnostic so it being set or not, if it is set use it, if the path differs from normal use that, otherwise ignore the header and path? maybe some diagrams might help our mentalmodel šŸ™‚
a
Consumer Custom Data If I look in the whole pact-reference code base I cannot find "message-type" anywhere. If I look at this code which can be used for a consumer to set up a sync interaction the
add_plugin_data
function is only called when the content matcher is not a core matcher. So are we saying here that we need our plugin to add a new content matcher which is essentially the same as the core application/json one but it facilitates us adding the custom plugin data fields? On the verifier side I can see how to read the content from the plugin data in the contract but I cannot work out how to get it in the contract using the pact_consumer library. Consumer Contract Metadata Again, looking at the same function for defining consumer contracts it doesn't look like a metadata field is supported for either core or plugin based content types. We can set the request and response bodies but not much else. Pact Verifier ProviderInfo struct has a transports property which contains the transports and their config. We have a websockets transport in here with a port number. The ProviderTransport struct has a path property. However it seems impossible to set the path property when using the pact-standalone-verifier and even if we could set it it doesn't get sent over to the plugin at any point. Only the host and port get sent as part of the "config" in the verifyInteraction request. https://github.com/pact-foundation/pact-reference/blob/46628a8bf6b4d514adcb317d749fb8a79130b472/rust/pact_verifier/src/lib.rs#L459 https://github.com/pact-foundation/pact-reference/blob/46628a8bf6b4d514adcb317d749fb8a79130b472/rust/pact_verifier/src/lib.rs#LL427C25-L427C25 https://github.com/pact-foundation/pact-plugins/blob/main/drivers/rust/driver/src/plugin_manager.rs#L579 From looking at this code I cannot work out how to get the additional info we need into the config in the contract in order for it to be sent to the provider when doing the verifications. Diagrams Let me see if I can put something together to help illustrate what we are trying to do. Thank you very much for taking the time to assist us with this.
I have a simple diagram here which (hopefully!) illustrates the use case. 1. The consumer (C) sends a HTTP request to the WS server (P). This request is on a specific path on the server and requires the Sec-WebSocket-Protocol header to be sent. If the path is wrong or the header is missing / value is wrong the connection will not be established. 2. The WS server responds that the protocol can switched over WS and a connection is established 3. The client sends a JSON payload to the server 4. The server processes the payload and sends a response Currently our contract just has the JSON payloads from 3 + 4 in it. However when verifying the contract it fails as the messages are being sent to the server base path (e.g. localhost:9998 - no base path) and without the correct header. This meant that the WS connection was never established and so the payloads from the contract could not be sent.
I guess there are a few questions on how best to handle this: 1. Should the path and header be on the contract at all? I'm torn because if these details are missed by the consumer then the integration won't work in prod. But maybe this should be tested elsewhere? 2. If not in the contract how can we configure the provider verification to establish the connection in the correct place? 3. If multiple request/response interactions are happing over a single connection (rather than a connection created for each interaction) does the connection info even belong in the contract and should the plugin setup the connection before verifying the interactions? If so where would this connection be best made in the verification workflow
y
Hey Adam, I have read this and super appreciative for the diagrams. I will invite @uglyog in for some noodling as he is our plugin Jedi. I don't think I'll be super valuable at the moment as I am generally newish to the plugin framework, know nothing about websockets/json-rpc so you are well further ahead in the field that I am on both fronts šŸ™‚
a
Thanks @Yousaf Nabi (pactflow.io). I think your questions have lead me to outline the thoughts/problems much better so I really appreciate your input on this so far šŸ™‚
u
Any data required should be stored in the metadata associated with the interaction. This is up to the plugin to decide how these are interpreted. I.e., you can maybe add a headers key to the metadata.
šŸ‘ 1
The Pact provider verifier won't know how to verify these interactions or interpret those values. This is why the plugin needs to implement
PrepareInteractionForVerification
and
VerifyInteraction
to do the actual verification.
šŸ‘ 1
should the plugin setup the connection before verifying the interactions?
This is up to the plugin author to decide how best to handle this. It will depend on the protocol being used. For websockets, this does make sense as it is an upgrade over standard HTTP.
The current workflow doesn't have any way to directly support that, it would have to be done in a lazy manner, i.e. create the connection the first time it is needed and then cache it.
šŸ‘ 1
Also, depending how the test framework runs things, the plugin may not always be running for the duration of all the tests.
šŸ‘ 1
a
Thanks @uglyog. This is very helpful. How can we get data into the metadata of the sync message contracts? I can't see how to add request metadata using
contents_from
or the sync message builder api.
u
hmm, let me check that
a
Thank you. I'll take another look. I couldn't see any reference of
requestMetadata
in the rust pact_consumer for handling sync messages. Let me see if it works
I have just tried the following and the metadata is not present in the contract.
Copy code
let ms = pb
        .synchronous_message_interaction("sync message example", |mut i| async move {
            i.contents_from(json!({
                "pact:content-type": "application/json-rpc",
                "request": {
                    "query": "matching(string, 'getMacAddress')"
                },
                "requestMetadata": {
                    "headers": {
                        "Sec-WebSocket-Protocol": "jsonrpc"
                    }
                },
                "response": {
                    "data": "matching(string, '00-B0-D0-63-C2-26')"
                }
            }))
            .await;

            i.test_name("sync message example");

            i
        })
        .await
        .start_mock_server_async(Some("websockets/transport/websockets"))
        .await;
Copy code
{
  "consumer": {
    "name": "message-consumer"
  },
  "interactions": [
    {
      "comments": {
        "testname": "sync message example"
      },
      "description": "sync message example",
      "key": "8e58be3b3ff9c72b",
      "pending": false,
      "request": {
        "contents": {
          "content": {
            "query": "matching(string, 'getMacAddress')"
          },
          "contentType": "application/json-rpc",
          "encoded": false
        }
      },
      "response": [
        {
          "contents": {
            "content": {
              "data": "matching(string, '00-B0-D0-63-C2-26')"
            },
            "contentType": "application/json-rpc",
            "encoded": false
          }
        }
      ],
      "type": "Synchronous/Messages"
    }
  ],
  "metadata": {
    "pactRust": {
      "consumer": "0.10.5",
      "models": "1.0.13"
    },
    "pactSpecification": {
      "version": "4.0"
    },
    "plugins": [
      {
        "configuration": {},
        "name": "websockets",
        "version": "test"
      }
    ]
  },
  "provider": {
    "name": "message-provider"
  }
}
Its doesn't look like the pact_consumer in rust looks for these other fields in the sync_message_builder and so they are ignored.
So I suppose in the short term, would it work if we provide the
application/json
content type from the plugin and then use the generate_content functionality to support metadata? It seems like more work than the plugin should need to do but I can't see another way of doing it right now
generate_content configure_interaction
u
pact_consumer won't set those values, it all gets passed to the plugin to configure.
You can specify what ever format you want, and the plugin then needs to read that format and configure the interaction appropriately during the
configure_interaction
call
a
I was trying to avoid having to implement the configure_interaction call as I am essentially re-implementing the core application/json content-matcher but adding support for a "metadata" field. The application/json matcher works very well for us already but using it offers no ability to add anything extra to the
metadata
field of the contract.
šŸ‘ 1