Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sending binary files in POST #277

Closed
krzysztofstepniak-silvair opened this issue Mar 4, 2019 · 9 comments
Closed

Sending binary files in POST #277

krzysztofstepniak-silvair opened this issue Mar 4, 2019 · 9 comments

Comments

@krzysztofstepniak-silvair

Hi, is there any option to update binary files in post request such as posting images etc.
I found that you have something in groovy (withFileUpload), but can't find it in node and js library.

@krzysztofstepniak-silvair krzysztofstepniak-silvair changed the title Updating binary files in POST Sending binary files in POST Mar 4, 2019
@mefellows
Copy link
Member

We don't have an option per-se, but it is possible to do with the correct headers etc. Perhaps it would be a nice DSL addition. I believe all it is in the groovy implementation is sugar that sets the correct headers and matchers.

@krzysztofstepniak-silvair
Copy link
Author

Well it would be great to have this implementation with matchers since we can use one client for sending requests for mocks/tests and another from provider side (real service).
In my case I have now different headers everytime because browser generates it (boundary on consumer side and weBkitFormBoundary on real service side), so adding matchers would help.

@krzysztofstepniak-silvair
Copy link
Author

krzysztofstepniak-silvair commented Mar 6, 2019

Well I have huge problems with your matcher.
Regex which was checked on few online regex testers
Here is stack:

`Error: Example '--------------------------560782525175769486914756
Content-Disposition: form-data; name="image"; filename="floorplan.png"
Content-Type: image/png


--------------------------560782525175769486914756
Content-Disposition: form-data; name="name"

Zone
--------------------------560782525175769486914756--' does not match provided regular expression ```
-------------------------[0-9]{24}$
Content-Disposition: form-data; name="image"; filename="floorplan.png"
Content-Type: image\png

-------------------------[0-9]{24}$
Content-Disposition: form-data; name="name"

[a-z]{4}$
---------------------------[0-9]{24}$--'


Here is interaction(btw. funny thing is that part for headers works fine):
```const postFloor = (projectId) => {
  return ({
    uponReceiving: 'POST for creating floor',
    withRequest: {
      headers: {'Content-Type': term({generate: 'multipart/form-data; boundary=--------------------------560782525175769486914756',
        matcher: 'multipart/form-data; boundary=--------------------------[0-9]{24}'})
      },
      method: 'POST',
      path:`/projects/${projectId}/floors,
      body: term({generate: '--------------------------560782525175769486914756\nContent-Disposition: form-data; name="image"; filename="floorplan.png"\nContent-Type: image/png\n\n\n--------------------------560782525175769486914756\nContent-Disposition: form-data; name="name"\n\nzone\n--------------------------560782525175769486914756--',
        matcher: '-------------------------[0-9]{24}$\nContent-Disposition: form-data; name="image"; filename="floorplan.png"\nContent-Type: image\\png\n\n\n-------------------------[0-9]{24}$\nContent-Disposition: form-data; name="name"\n\n[a-z]{4}$\n---------------------------[0-9]{24}$--'})
    },
    willRespondWith: {
      status: 201,
      headers: {'Content-Type': 'application/json'},
      body: Response
    }
  });
};

@mefellows
Copy link
Member

I'm sorry could you please format that code and text? It is basically unreadable and I'm not able to properly follow.

Secondly, just in case it's not obvious, you cannot perform matching on the body of a binary object. Currently, matching is only support for the content-type application/json. This is probably why the headers are working as expected.

So you can do the contract for all of the other bits (e.g. headers, path, query string etc.)

@YOU54F
Copy link
Member

YOU54F commented Apr 30, 2019

Given the following two files
test-base64.pdf

JVBERi0xLjIgCjkgMCBvYmoKPDwKPj4Kc3RyZWFtCkJULyA5IFRmKFRlc3QpJyBFVAplbmRzdHJlYW0KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCA1IDAgUgovQ29udGVudHMgOSAwIFIKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0tpZHMgWzQgMCBSIF0KL0NvdW50IDEKL1R5cGUgL1BhZ2VzCi9NZWRpYUJveCBbIDAgMCA5OSA5IF0KPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1BhZ2VzIDUgMCBSCi9UeXBlIC9DYXRhbG9nCj4+CmVuZG9iagp0cmFpbGVyCjw8Ci9Sb290IDMgMCBSCj4+CiUlRU9G

test.pdf

%PDF-1.0
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
xref
0 4
0000000000 65535 f
0000000010 00000 n
0000000053 00000 n
0000000102 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
149
%EOF
import { InteractionObject } from "@pact-foundation/pact";
import { like, term } from "@pact-foundation/pact/dsl/matchers";
import { readFileSync } from "fs";
import * as supertest from "supertest";
import { getProvider } from "../../provider";

const pactPort = 9889;

const getClient = () => {
  const url = `http://localhost:${pactPort}`;
  return supertest(url);
};

describe("Document Storage service provider pact", () => {
  const provider = getProvider({
    provider: "document-storage-service",
    pactPort
  });

  beforeAll(async () => await provider.setup());
  afterEach(async () => await provider.verify());
  afterAll(async () => await provider.finalize());

  describe("document service storage", () => {
    test("should successfully allow upload of a base 64 encoded pdf", async () => {
      const pdfname: string = "test-base64.pdf";
      const pdf = readFileSync(pdfname);
      const body = `----------------------------713166514119664968500586\r\nContent-Disposition: form-data; name=\"test\"\r\n\r\ntest\r\n----------------------------713166514119664968500586\r\nContent-Disposition: form-data; name=\"document\"; filename=\"${pdfname}\"\r\nContent-Type: application/pdf\r\n\r\n${pdf}\r\n----------------------------713166514119664968500586--\r\n`;

      const interaction: InteractionObject = {
        state: "Service is up and healthy",
        uponReceiving:
          "a well formed request with a base 64 encoded pdf to upload",
        withRequest: {
          method: "POST",
          path: "/test",
          headers: {
            "Content-Type": term({
              generate:
                "multipart/form-data; boundary=--------------------------560782525175769486914756",
              matcher:
                "multipart/form-data; boundary=--------------------------[0-9]{24}"
            }),
            Authorization: term({
              generate: "Bearer eyJhbGciOiJIUzI1NiIXVCJ9",
              matcher: "Bearer [0-9A-z]{24}"
            }),
            "Content-Length": like("299")
          },
          body
        },
        willRespondWith: {
          headers: {
            "Content-Type": "application/json"
          },
          status: 201
        }
      };

      await provider.addInteraction(interaction);

      const client = getClient();
      await client
        .post("/test")
        .set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIXVCJ9")
        .field("test", "test")
        .attach("document", pdfname)
        .expect(201);
    });
    test("should successfully allow upload of binary encoded pdf", async () => {
      const pdfname: string = "test.pdf";
      const pdf = readFileSync(pdfname);
      const body = `----------------------------713166514119664968500586\r\nContent-Disposition: form-data; name=\"test\"\r\n\r\ntest\r\n----------------------------713166514119664968500586\r\nContent-Disposition: form-data; name=\"document\"; filename=\"${pdfname}\"\r\nContent-Type: application/pdf\r\n\r\n${pdf}\r\n----------------------------713166514119664968500586--\r\n`;

      const interaction: InteractionObject = {
        state: "Service is up and healthy",
        uponReceiving:
          "a well formed request with a binary encoded pdf to upload",
        withRequest: {
          method: "POST",
          path: "/test",
          headers: {
            "Content-Type": term({
              generate:
                "multipart/form-data; boundary=--------------------------560782525175769486914756",
              matcher:
                "multipart/form-data; boundary=--------------------------[0-9]{24}"
            }),
            Authorization: term({
              generate: "Bearer eyJhbGciOiJIUzI1NiIXVCJ9",
              matcher: "Bearer [0-9A-z]{24}"
            }),
            "Content-Length": like("299")
          },
          body
        },
        willRespondWith: {
          headers: {
            "Content-Type": "application/json"
          },
          status: 201
        }
      };

      await provider.addInteraction(interaction);

      const client = getClient();
      await client
        .post("/test")
        .set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIXVCJ9")
        .field("test", "test")
        .attach("document", pdfname)
        .expect(201);
    });
  });
});

Useful threads

pact-foundation/pact-ruby#175

pact-foundation/pact-ruby@7a16ab1

pact-foundation/pact-python#97

https://github.com/pact-foundation/pact-ruby/blob/master/spec/features/consumer_with_file_upload_spec.rb#L35

@krzysztofstepniak-silvair
Copy link
Author

krzysztofstepniak-silvair commented May 5, 2019

Well I have still problem. I have two files. First one have interaction:


const {Matchers} = require('@pact-foundation/pact');
const {like, term} = Matchers;
const path = require('path');
const {readFileSync} = require('fs');
const floorplanPath = path.resolve(__dirname, '../../../test/framework', 'floorplan.png');
const file = readFileSync(floorplanPath);

const responseBodyPost = {
  created: like('2019-04-30T11:02:38.566Z'),
  image: like('https://s3.eu-central-1.amazonaws.com/fs.localhost.silvair.com/floorplans/default.png'),
  imageSmall: like('https://s3.eu-central-1.amazonaws.com/fs.localhost.silvair.com/floorplans/default-small.png'),
  name: like('blabla'),
  projectId: like('5cc82b2e29842b2aaf244430'),
  updated: like('2019-04-30T11:02:38.566Z'),
  _id: like('5cc82b4e29842b2aaf244431')
};

const body = `----------------------------713166514119664968500586\\nContent-Disposition: form-data; name="image"; filename="floorplan.png"\\nContent-Type: image\\/png\\n\\n${file}\\n----------------------------713166514119664968500586\\nContent-Disposition: form-data; name="name"\\n\\nfloorplan\\n----------------------------713166514119664968500586--`;

const postFloor = {
  state: 'Project in db',
  uponReceiving: 'POST Floor',
  withRequest: {
    method: 'POST',
    path: '/projects/5c98d39e3c099a303e14fbb6/floors',
    headers: {'Content-Type': term({
      generate:
        'multipart/form-data; boundary=--------------------------308944615496122587648929',
      matcher:
        'multipart/form-data; boundary=--------------------------[0-9]{24}$'
    }),
    'Content-Length': like('299')
    },
    body
  },
  willRespondWith: {
    status: 201,
    headers: {'Content-Type': 'application/json'},
    body: responseBodyPost
  }
};

module.exports  = {postFloor};

Second one have mock and test:

'use strict';

const {Pact} = require('@pact-foundation/pact');
const path = require('path');
const getPort = require('get-port');

const {postFloor} = require('./interactions/floors');
const bffService = require('../../test/framework/services/platform-bff');

const PACTS_DIR = path.resolve(process.cwd(),'pact','contracts');
const PACT_LOG_LEVEL = 'info';
const {PROJECT_ID} = require('../shared-consts');

describe('ComponentTests', () => {
  let platformBFF;
  let portPlatformBff;
  let request;

  before(async () => {
    portPlatformBff = await getPort();

    platformBFF = new Pact({
      dir: PACTS_DIR,
      consumer: 'spaces-web',
      provider: 'platform-bff',
      port: portPlatformBff,
      logLevel: PACT_LOG_LEVEL,
      pactfileWriteMode: 'update'
    });

    await platformBFF.setup();
  });

  after(async () => {
    return await platformBFF.finalize();
  });

  afterEach(async () => {
    await platformBFF.removeInteractions();
  });

  describe('POST /projects/{id}/floors', async () => {
    before(async () => {
      await Promise.all([platformBFF.addInteraction(postFloor),
        request = bffService.create(`http://localhost:${portPlatformBff}`)]);
    });
    it('POST for sending pic /projects/{id}/floors', async () => {
      await request.createFloor(PROJECT_ID, 'floorplan');
      await platformBFF.verify();
    });
  });
});

When I run test i have this kind of error:

Error: Pact verification failed - expected interactions did not match actual.

Pact Binary Error: Could not load existing consumer contract from /Users/krzysiek/Desktop/silvair/platform/platform-e2e-test/pact/contracts/spaces-web-platform-bff.json due to undefined method `gsub' for nil:NilClass. Creating a new file.

@krzysztofstepniak-silvair
Copy link
Author

Here is log file.
pact.log

@mefellows
Copy link
Member

mefellows commented Jun 30, 2019

I think the key line here is :

Encoding::UndefinedConversionError - "\x89" from ASCII-8BIT to UTF-8

We've seen this before, it seems to be environment dependent

See:

If you could please provide a reproducable example (shown in a travis/appveyor build) that will help us get to the bottom of it.

@mefellows
Copy link
Member

Closing as supported in latest versions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants