如果 headers 和 body 都存在,Pact.io(打字稿)失败

Pact.io (typescript) fails if both headers and body are present

我在前端 (TypeScript) 和后端之间使用 Pact.io 合同。我正在尝试为文件上传编写协议;但是,如果我在 withRequest 部分同时指定 headers.Content-Typebody,验证显然会无缘无故地失败(在前端 -> 甚至没有生成协议)。如果我遗漏了这两个部分中的任何一个,验证就会起作用(但当然会丢失一些数据,所以我无法成功验证后端)。我的设置如下所示:

const fileBody = '------boundary\r\n' +
    'Content-Disposition: form-data; name="file"; ' +
    'filename="test-attachment.pdf"\r\n' +
    'Content-Type: application/pdf\r\n' +
    '\r\n' +
    'file content\r\n' +
    '------boundary--\r\n';
const likeFileBody = fileBody.replace(/boundary/g, ".*");
const interaction: InteractionObject = {
    state: "my state",
    uponReceiving: "a file upload",
    withRequest: {
        method: 'POST',
        path: `/my/upload/path`,
        headers: {
            "Content-Type": Matchers.regex({
                generate: "multipart/form-data; boundary=----boundary",
                matcher: "multipart/form-data; boundary=.*",
            })
        },
        body: Matchers.regex({
            generate: fileBody,
            matcher: likeFileBody,
        }),
    },
    willRespondWith: {
        status: 201,
        headers: {'Content-Type': 'application/json'},
        body: {"some": "data"},
    }
};
await provider.addInteraction(interaction);

// ng2-file-upload module
const uploader = new FileUploader({
    url: `/my/upload/path`
});

uploader.addToQueue([new File(["file content"], "test-attachment.pdf")]);
uploader.uploadAll();
// poll uploader status
return new Promise(async resolve => {
    while (uploader.isUploading) {
        await new Promise((pollResolve => setTimeout(pollResolve, 10)));
    }
    resolve();
});

协议日志如下:

 INFO -- : Registered expected interaction POST /my/upload/path
DEBUG -- : {
  "description": "a file upload",
  "providerState": "my state",                                                              
  "request": {                          
    "method": "POST",                                                                        
    "path": "/my/upload/path",
    "headers": {    
      "Content-Type": {      
        "json_class": "Pact::Term",    
        "data": {         
          "generate": "multipart/form-data; boundary=----boundary",
          "matcher": {"json_class":"Regexp","o":0,"s":"multipart/form-data; boundary=.*"}
        }
      }                                                                                                                                                   
    },                                                                                                                                                                                                               
    "body": { 
      "json_class": "Pact::Term",                                                                                                                                                     
      "data": {                                                                                                                                                                        
        "generate": "------boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test-attachment.pdf\"\r\nContent-Type: application/pdf\r\n\r\nfile content\r\n------boundary--\r\n",
        "matcher": {"json_class":"Regexp","o":0,"s":"------.*\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test-attachment.pdf\"\r\nContent-Type: application/pdf\r\n\r\nfile content\r\n------.*--\r
\n"}              
      }                                                                                                                                                                                                        
    }                                                                                                                                                                                                                
  },     
  "response": {                                                                                                                                                                                                      
    "status": 201,
    "headers": {                                                                                                                                                                                                 
      "Content-Type": "application/json"                                                                                                                                                                        
    },                                                                                                                                                          
    "body": {}
  }
}
 INFO -- : Received request POST /my/upload/path
DEBUG -- : {
  "path": "/my/upload/path",
  "query": "",
  "method": "post",
  "body": "----------------------------163633642410335919983066\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test-attachment.pdf\"\r\nContent-Type: application/pdf\r\n\r\nfile content\r\n----------
------------------163633642410335919983066--\r\n",
  "headers": {
    "Content-Length": "234",
    "Content-Type": "multipart/form-data; boundary=--------------------------163633642410335919983066",
    "Referer": "http://localhost:8991/",
    "User-Agent": "Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/11.12.0",
    "Accept-Language": "en",
    "Accept": "*/*",
    "Host": "localhost:8991",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "close",
    "Version": "HTTP/1.1"
  }
}
ERROR -- : Error ocurred in mock service: NoMethodError - undefined method `split' for #<Pact::Term:0x00561356747038>
ERROR -- : /srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-support-1.8.0/lib/pact/shared/multipart_form_differ.
rb:8:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-support-1.8.0/lib/pact/consumer_contract/request.rb:72:in `body_diff'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-support-1.8.0/lib/pact/consumer_contract/request.rb:42:in `difference'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-support-1.8.0/lib/pact/consumer_contract/request.rb:28:in `matches?'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/interactions/candidate_interactions.rb:8:in `block in matc
hing_interactions'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/interactions/candidate_interactions.rb:7:in `select'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/interactions/candidate_interactions.rb:7:in `matching_inte
ractions'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/request_handlers/interaction_replay.rb:51:in `find_respons
e'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/request_handlers/interaction_replay.rb:41:in `respond'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/request_handlers/base_request_handler.rb:17:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/rack-2.0.5/lib/rack/cascade.rb:33:in `block in call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/rack-2.0.5/lib/rack/cascade.rb:24:in `each'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/rack-2.0.5/lib/rack/cascade.rb:24:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/consumer/mock_service/cors_origin_header_middleware.rb:11:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/consumer/mock_service/error_handler.rb:13:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/mock_service/app.rb:33:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/pact-mock_service-2.12.0/lib/pact/consumer/mock_service/set_location.rb:14:in `call'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/rack-2.0.5/lib/rack/handler/webrick.rb:86:in `service'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/webrick-1.3.1/lib/webrick/httpserver.rb:138:in `service'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/webrick-1.3.1/lib/webrick/httpserver.rb:94:in `run'
/srv/service/node_modules/@pact-foundation/pact-node/standalone/linux-x64-1.61.1/lib/vendor/ruby/2.2.0/gems/webrick-1.3.1/lib/webrick/server.rb:191:in `block in start_thread'
 WARN -- : Verifying - actual interactions do not match expected interactions.
Missing requests:
        POST /my/upload/path


 WARN -- : Missing requests:
        POST /my/upload/path

我是不是遗漏了什么或者这是契约框架的错误?

目前您不能将正文匹配器应用于 JSON 以外的内容类型。

官方指导是测试交互的其他方面而不是二进制负载本身:https://docs.pact.io/faq#how-do-i-test-binary-files-in-responses-such-as-a-download

查看pact框架的源码后,发现它已经用通配符替换了边界;但是,它仅在 body 中而不是在 Content-Type header 中这样做。因此,该协定的正确表述是:

const fileBody = '------boundary\r\n' +
    'Content-Disposition: form-data; name="file"; ' +
    'filename="test-attachment.pdf"\r\n' +
    'Content-Type: application/pdf\r\n' +
    '\r\n' +
    'file content\r\n' +
    '------boundary--\r\n';

...

headers: {
    "Content-Type": Matchers.regex({
        generate: "multipart/form-data; boundary=----boundary",
        matcher: "multipart/form-data; boundary=.*",
    })
},
body: fileBody,

有了这个,我能够在消费者和提供者中进行验证。