Repository: pactumjs/pactum
Pull request #196 (2022-08-23 17:18:40)

feat: support data templates in form data
closes: #195 


Patch:
diff --git a/package-lock.json b/package-lock.json
index acd7b25..ff1be38 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "pactum",
-  "version": "3.1.14",
+  "version": "3.2.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -503,7 +503,7 @@
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
     },
     "balanced-match": {
       "version": "1.0.2",
@@ -777,7 +777,7 @@
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
     },
     "diff": {
       "version": "5.0.0",
@@ -868,13 +868,13 @@
       }
     },
     "form-data-lite": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/form-data-lite/-/form-data-lite-1.0.2.tgz",
-      "integrity": "sha512-P0Ohn4DNGUIm3gTriMxMSeeR74Q2m25ZGPduSEpEMWN+q7SL2HYHGyuk/B5U61Wb04vtWDTUIYEDafhJ9PGwJg==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/form-data-lite/-/form-data-lite-1.0.3.tgz",
+      "integrity": "sha512-P7xPqAiOPKzC9Q9aywAZJCQq4QOE5WokPb3HrcWRh7C57RKytueJzoORZAVgHBNvK/lL7E+FxjQjd4X/zbecEQ==",
       "requires": {
         "asynckit": "^0.4.0",
         "combined-stream": "^1.0.8",
-        "mime-lite": "^1.0.2"
+        "mime-lite": "^1.0.3"
       }
     },
     "fromentries": {
@@ -1291,9 +1291,9 @@
       }
     },
     "mime-lite": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/mime-lite/-/mime-lite-1.0.2.tgz",
-      "integrity": "sha512-HkTeRkg8B7nSw+ScQFPXBPtk6sKHVUj/HL9dvvgWRe2NZODrNTAIp5ni84oWb9jZBggheZDgYL9Fz0b29Q9a5A=="
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/mime-lite/-/mime-lite-1.0.3.tgz",
+      "integrity": "sha512-V85l97zJSTG8FEvmdTlmNYb0UMrVBwvRjw7bWTf/aT6KjFwtz3iTz8D2tuFIp7lwiaO2C5ecnrEmSkkMRCrqVw=="
     },
     "minimatch": {
       "version": "3.0.4",
diff --git a/package.json b/package.json
index 22db48d..80c73b2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pactum",
-  "version": "3.1.14",
+  "version": "3.2.0",
   "description": "REST API Testing Tool for all levels in a Test Pyramid",
   "main": "./src/index.js",
   "types": "./src/index.d.ts",
@@ -62,7 +62,7 @@
   "dependencies": {
     "@exodus/schemasafe": "^1.0.0-rc.7",
     "deep-override": "^1.0.2",
-    "form-data-lite": "^1.0.2",
+    "form-data-lite": "^1.0.3",
     "json-query": "^2.2.2",
     "klona": "^2.0.5",
     "lightcookie": "^1.0.25",
diff --git a/src/helpers/requestProcessor.js b/src/helpers/requestProcessor.js
index 0ab6262..4cea1b9 100644
--- a/src/helpers/requestProcessor.js
+++ b/src/helpers/requestProcessor.js
@@ -2,6 +2,7 @@ const { URL } = require('url');
 const processor = require('./dataProcessor');
 const helper = require('./helper');
 const config = require('../config');
+const fd = require('../plugins/form.data')
 
 const requestProcessor = {
 
@@ -15,6 +16,7 @@ const requestProcessor = {
     setGraphQLParams(request);
     setQueryParams(request);
     setBody(request);
+    setFormData(request);
     setMultiPartFormData(request);
     setFollowRedirects(request);
     setHeaders(request);
@@ -82,20 +84,57 @@ function setBody(request) {
   }
 }
 
+function setFormData(request) {
+  if (request._forms) {
+    request.form = {};
+    for (let i = 0; i < request._forms.length; i++) {
+      const { key, value } = request._forms[i];
+      if (typeof key === 'string') {
+        request.form[key] = value;
+      } else {
+        for (const form_key of Object.keys(key)) {
+          request.form[form_key] = key[form_key];
+        }
+      }
+    }
+    delete request._forms;
+  }
+}
+
 function setMultiPartFormData(request) {
-  if (request._multiPartFormData) {
-    request.data = request._multiPartFormData.getBuffer();
-    const multiPartHeaders = request._multiPartFormData.getHeaders();
+  if (request._multi_parts) {
+    const FormData = fd.get();
+    let multi_part_form_data;
+    for (let i = 0; i < request._multi_parts.length; i++) {
+      const { key, value, options } = request._multi_parts[i];
+      if (key instanceof FormData) { 
+        multi_part_form_data = key;
+      } else {
+        if (typeof multi_part_form_data === 'undefined') {
+          multi_part_form_data = new FormData();
+        }
+        if (typeof key === 'string') {
+          multi_part_form_data.append(key, value, options);
+        } else {
+          for (const form_key of Object.keys(key)) {
+            multi_part_form_data.append(form_key, key[form_key], options);
+          }
+        }
+        
+      }
+    }
+    request.data = multi_part_form_data.getBuffer();
+    const multi_part_headers = multi_part_form_data.getHeaders();
     if (!request.headers) {
-      request.headers = multiPartHeaders;
+      request.headers = multi_part_headers;
     } else {
-      for (const prop in multiPartHeaders) {
+      for (const prop in multi_part_headers) {
         if (request.headers[prop] === undefined) {
-          request.headers[prop] = multiPartHeaders[prop];
+          request.headers[prop] = multi_part_headers[prop];
         }
       }
     }
-    delete request._multiPartFormData;
+    delete request._multi_parts;
   }
 }
 
diff --git a/src/models/Spec.d.ts b/src/models/Spec.d.ts
index 63072a6..cea0d51 100644
--- a/src/models/Spec.d.ts
+++ b/src/models/Spec.d.ts
@@ -176,7 +176,8 @@ declare class Spec {
    * attaches form data to the request with header - "application/x-www-form-urlencoded"
    * @see https://pactumjs.github.io/api/requests/withForm.html
    */
-  withForm(form: any): Spec;
+  withForm(form: object): Spec;
+  withForm(key: string, value: string): Spec;
 
   /**
    * attaches multi part form data to the request with header - "multipart/form-data"
@@ -184,6 +185,7 @@ declare class Spec {
    * @see https://pactumjs.github.io/api/requests/withMultiPartFormData.html
    */
   withMultiPartFormData(form: FormData): Spec;
+  withMultiPartFormData(form: object): Spec;
   withMultiPartFormData(key: string, value: string | Buffer | Array<any> | ArrayBuffer, options?: FormData.AppendOptions): Spec;
 
   /**
diff --git a/src/models/Spec.js b/src/models/Spec.js
index d78a3d9..ee7443e 100644
--- a/src/models/Spec.js
+++ b/src/models/Spec.js
@@ -240,27 +240,15 @@ class Spec {
     return this;
   }
 
-  withForm(form) {
-    if (!helper.isValidObject(form)) {
-      throw new PactumRequestError(`Invalid form provided - ${form}`);
-    }
-    if (typeof this._request.form !== 'undefined') {
-      throw new PactumRequestError(`Duplicate form in request`);
-    }
-    this._request.form = form;
+  withForm(key, value) {
+    if (!this._request._forms) this._request._forms = [];
+    this._request._forms.push({ key, value });
     return this;
   }
 
   withMultiPartFormData(key, value, options) {
-    const FD = fd.get();
-    if (key instanceof FD) {
-      this._request._multiPartFormData = key;
-    } else {
-      if (this._request._multiPartFormData === undefined) {
-        this._request._multiPartFormData = new FD();
-      }
-      this._request._multiPartFormData.append(key, value, options);
-    }
+    if (!this._request._multi_parts) this._request._multi_parts = [];
+    this._request._multi_parts.push({ key, value, options });
     return this;
   }
 


Test patch:
diff --git a/test/component/base.spec.js b/test/component/base.spec.js
index bd67645..676510d 100644
--- a/test/component/base.spec.js
+++ b/test/component/base.spec.js
@@ -1,5 +1,4 @@
-const pactum = require('../../src/index');
-const handler = pactum.handler;
+const { handler, mock } = require('../../src/index');
 
 function addDefaultMockHandlers() {
   handler.addInteractionHandler('default get', () => {
@@ -102,9 +101,9 @@ function addDefaultMockHandlers() {
 
 before(async () => {
   addDefaultMockHandlers();
-  await pactum.mock.start();
+  await mock.start();
 });
 
 after(async () => {
-  await pactum.mock.stop();
+  await mock.stop();
 });
\ No newline at end of file
diff --git a/test/component/form.multi-part.spec.js b/test/component/form.multi-part.spec.js
new file mode 100644
index 0000000..e6e2974
--- /dev/null
+++ b/test/component/form.multi-part.spec.js
@@ -0,0 +1,92 @@
+const { spec, stash, handler } = require('../../src/index');
+const { includes } = require('pactum-matchers');
+const FormData = require('form-data-lite');
+
+describe('Multi Part FormData', () => {
+
+  before(() => {
+    handler.addInteractionHandler('post form data', () => {
+      return {
+        request: {
+          method: 'POST',
+          path: '/api/projects/form',
+          headers: {
+            'content-type': includes('multipart/form-data; boundary=-')
+          },
+          body: includes('age')
+        },
+        response: {
+          status: 200
+        }
+      }
+    });
+    handler.addInteractionHandler('post form data with file', () => {
+      return {
+        request: {
+          method: 'POST',
+          path: '/api/projects/form',
+          headers: {
+            'content-type': includes('multipart/form-data; boundary=-')
+          },
+          body: includes('file')
+        },
+        response: {
+          status: 200
+        }
+      }
+    });
+    stash.addDataTemplate({
+      FORM: { 'user': 'drake', 'age': '10', 'job': 'leader' }
+    });
+  });
+
+  after(() => {
+    stash.clearDataTemplates();
+  });
+
+  it('submit form data as an object in a single method', async () => {
+    await spec()
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
+      .withMultiPartFormData({ 'user': 'drake', 'age': 10, 'job': 'leader' })
+      .expectStatus(200);
+  });
+
+  it('submit form data as kv pair and object in a multiple method', async () => {
+    await spec()
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
+      .withMultiPartFormData('user', 'drake')
+      .withMultiPartFormData({ 'age': 10, 'job': 'leader' })
+      .expectStatus(200);
+  });
+
+  it('submit form data as using form-data object', async () => {
+    const form = new FormData();
+    form.append('user', 'drake');
+    await spec()
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
+      .withMultiPartFormData(form)
+      .withMultiPartFormData({ 'age': 10, 'job': 'leader' })
+      .expectStatus(200);
+  });
+
+  it('submit form data with file', async () => {
+    await spec()
+      .useInteraction('post form data with file')
+      .post('http://localhost:9393/api/projects/form')
+      .withMultiPartFormData({ 'user': 'drake', 'age': 10, 'job': 'leader' })
+      .withFile('file', './test/component/base.spec.js', { contentType: 'application/js', filename: 'interactions.spec.js' })
+      .expectStatus(200);
+  });
+
+  it('submit form data as a data template object', async () => {
+    await spec()
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
+      .withMultiPartFormData({ '@DATA:TEMPLATE@': 'FORM' })
+      .expectStatus(200);
+  });
+
+});
\ No newline at end of file
diff --git a/test/component/form.spec.js b/test/component/form.spec.js
index 5dc0b57..6bbec33 100644
--- a/test/component/form.spec.js
+++ b/test/component/form.spec.js
@@ -1,51 +1,61 @@
-const { spec } = require('../../src/index');
+const { spec, stash, handler } = require('../../src/index');
 const { like } = require('pactum-matchers');
 const { expect } = require('chai');
 
 describe('Form', () => {
 
-  it('single pair of form data', async () => {
-    await spec()
-      .useInteraction({
+  before(() => {
+    handler.addInteractionHandler('post form data', () => {
+      return {
         request: {
           method: 'POST',
-          path: '/api/projects',
+          path: '/api/projects/form',
           headers: {
             'content-type': 'application/x-www-form-urlencoded'
           },
           form: {
-            'user': 'drake'
+            'user': 'drake',
+            'age': '10',
+            'job': 'leader'
           }
         },
         response: {
           status: 200
         }
-      })
-      .post('http://localhost:9393/api/projects')
+      }
+    });
+    stash.addDataTemplate({
+      FORM: { 'user': 'drake', 'age': 10, 'job': 'leader' }
+    });
+  });
+
+  after(() => {
+    stash.clearDataTemplates();
+  });
+
+  it('submit form data as an object in a single method', async () => {
+    await spec()
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
+      .withForm({ 'user': 'drake', 'age': 10, 'job': 'leader' })
+      .expectStatus(200);
+  });
+
+  it('submit form data as an object and kv pairs using independent methods', async () => {
+    await spec()
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
       .withForm({ 'user': 'drake' })
+      .withForm({ 'age': '10' })
+      .withForm('job', 'leader')
       .expectStatus(200);
   });
 
-  it('multiple pairs of form data', async () => {
+  it('submit form data as an object using data template', async () => {
     await spec()
-      .useInteraction({
-        request: {
-          method: 'POST',
-          path: '/api/projects',
-          headers: {
-            'content-type': 'application/x-www-form-urlencoded'
-          },
-          form: {
-            'user': 'drake',
-            'age': '10'
-          }
-        },
-        response: {
-          status: 200
-        }
-      })
-      .post('http://localhost:9393/api/projects')
-      .withForm({ 'user': 'drake', 'age': 10 })
+      .useInteraction('post form data')
+      .post('http://localhost:9393/api/projects/form')
+      .withForm({ '@DATA:TEMPLATE@': 'FORM' })
       .expectStatus(200);
   });
 
diff --git a/test/unit/spec.spec.js b/test/unit/spec.spec.js
index ab22fce..69f5a7d 100644
--- a/test/unit/spec.spec.js
+++ b/test/unit/spec.spec.js
@@ -219,33 +219,6 @@ describe('Spec', () => {
     expect(err.toString()).equals('Error: Duplicate request initiated. Existing request - GET /api');
   });
 
-  it('null form request', async () => {
-    let err;
-    try {
-      const spec = new Spec();
-      await spec
-        .post('/')
-        .withForm(null);
-    } catch (error) {
-      err = error;
-    }
-    expect(err.toString()).equals('Error: Invalid form provided - null');
-  });
-
-  it('duplicate form request', async () => {
-    let err;
-    try {
-      const spec = new Spec();
-      await spec
-        .post('/')
-        .withForm({})
-        .withForm({});
-    } catch (error) {
-      err = error;
-    }
-    expect(err.toString()).equals('Error: Duplicate form in request');
-  });
-
   it('invalid request timeout', async () => {
     let err;
     try {