Repository: pactumjs/pactum
Pull request #47 (2021-03-14 11:00:42)

3.0.8
- added `withFile` method for uploading files
- bump pactum-matchers version
- added basic initialize and cleanup handlers


Patch:
diff --git a/README.md b/README.md
index e150741..e51ab8c 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
 
 **pactum** is a REST API Testing Tool used to automate e2e, integration, contract & component (*or service level*) tests. It comes with a powerful *mock server* which can control the state of external dependencies.
 
+- ⏱ Swift
 - 🎈 Lightweight
 - 🚀 Simple & Powerful
 - 🛠️ Compelling Mock Server
@@ -118,25 +119,20 @@ Running **pactum** as a standalone *mock server*.
 
 ```javascript
 const { mock } = require('pactum');
-const { regex } = require('pactum-matchers');
 
 mock.addInteraction({
   request: {
     method: 'GET',
-    path: '/api/projects',
-    queryParams: {
-      date: regex('2020-12-12', /^\d{4}-\d{2}-\d{2}$/)
-    }
+    path: '/api/projects'
   },
   response: {
     status: 200,
-    headers: {
-      'content-type': 'application/json'
-    },
-    body: {
-      id: 1,
-      name: 'fake'
-    }
+    body: [
+      {
+        id: 'project-id',
+        name: 'project-name'
+      }
+    ]
   }
 });
 
diff --git a/package-lock.json b/package-lock.json
index cbe8cc3..fd31187 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "pactum",
-  "version": "3.0.7",
+  "version": "3.0.8",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1658,9 +1658,9 @@
       }
     },
     "pactum-matchers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/pactum-matchers/-/pactum-matchers-1.0.0.tgz",
-      "integrity": "sha512-4qSMZwbYAXdWPQgdhP74HWXAN8fe6+RPqcyAt+btZVbJCd2nZGhq2fvucCry0n0CyxlUAIRtpZxSpNGu72920w=="
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pactum-matchers/-/pactum-matchers-1.0.1.tgz",
+      "integrity": "sha512-D+/HN7TjFl4DpPWZnnlzk+G52Ku49s5wSinfva6QIPPbBU6WFsXjrK+HiaXamfQFlS6q5JoUyLfNDyVLjXb79A=="
     },
     "parse-graphql": {
       "version": "1.0.0",
diff --git a/package.json b/package.json
index 297c308..29d6c0c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pactum",
-  "version": "3.0.7",
+  "version": "3.0.8",
   "description": "REST API Testing Tool for all levels in a Test Pyramid",
   "main": "./src/index.js",
   "types": "./src/index.d.ts",
@@ -65,7 +65,7 @@
     "form-data": "^3.0.0",
     "json-query": "^2.2.2",
     "openapi-fuzzer-core": "^1.0.6",
-    "pactum-matchers": "^1.0.0",
+    "pactum-matchers": "^1.0.1",
     "parse-graphql": "^1.0.0",
     "phin": "^3.4.1",
     "polka": "^0.5.2"
diff --git a/src/exports/handler.js b/src/exports/handler.js
index 8d2ff83..2d1e77a 100644
--- a/src/exports/handler.js
+++ b/src/exports/handler.js
@@ -9,6 +9,8 @@ const stateHandlers =  {};
 const dataHandlers = {};
 const interactionHandlers = {};
 const assertHandlers = {};
+const initializeHandlers = {};
+const cleanupHandlers = {};
 
 const handler = {
 
@@ -91,7 +93,25 @@ const handler = {
   getAssertHandler(name) {
     if (assertHandlers[name]) return assertHandlers[name];
     throw new PactumHandlerError(`Assert Handler Not Found - '${name}'`);
-  }
+  },
+
+  addInitializeHandler(name, func) {
+    isValidHandler(name, func);
+    initializeHandlers[name] = func;
+  },
+
+  getInitializeHandlers() {
+    return initializeHandlers;
+  },
+
+  addCleanupHandler(name, func) {
+    isValidHandler(name, func);
+    cleanupHandlers[name] = func;
+  },
+
+  getCleanupHandlers() {
+    return cleanupHandlers;
+  },
 
 };
 
diff --git a/src/helpers/handler.runner.js b/src/helpers/handler.runner.js
index c101364..cb0dfc9 100644
--- a/src/helpers/handler.runner.js
+++ b/src/helpers/handler.runner.js
@@ -1,4 +1,5 @@
 const handler = require('../exports/handler');
+const log = require('./logger');
 
 const hr = {
 
@@ -16,6 +17,24 @@ const hr = {
     if (typeof name !== 'undefined') {
       handler.getSpecHandler(name)({ spec, data });
     }
+  },
+
+  async initialize() {
+    const handlers = handler.getInitializeHandlers();
+    const keys = Object.keys(handlers);
+    for (let i = 0; i < keys.length; i++) {
+      log.info(`Running Initializer - ${keys[i]}`);
+      await handlers[keys[i]]();
+    }
+  },
+
+  async cleanup() {
+    const handlers = handler.getCleanupHandlers();
+    const keys = Object.keys(handlers);
+    for (let i = 0; i < keys.length; i++) {
+      log.info(`Running Cleaner - ${keys[i]}`);
+      await handlers[keys[i]]();
+    }
   }
 
 };
diff --git a/src/helpers/requestProcessor.js b/src/helpers/requestProcessor.js
index c196579..658cadf 100644
--- a/src/helpers/requestProcessor.js
+++ b/src/helpers/requestProcessor.js
@@ -12,10 +12,10 @@ const requestProcessor = {
     setBaseUrl(request);
     setPathParams(request);
     setQueryParams(request);
-    setHeaders(request);
     setBody(request);
     setMultiPartFormData(request);
     setFollowRedirects(request);
+    setHeaders(request);
     request.timeout = request.timeout || config.request.timeout;
     return request;
   }
diff --git a/src/models/Spec.d.ts b/src/models/Spec.d.ts
index 4572a6d..0047a93 100644
--- a/src/models/Spec.d.ts
+++ b/src/models/Spec.d.ts
@@ -229,6 +229,20 @@ declare class Spec {
    */
   withBody(body: any): Spec;
 
+  /**
+   * appends file to the form-data
+   * @example
+   * await pactum.spec()
+   *  .post('url')
+   *  .withFile('./file/path')
+   * 
+   * await pactum.spec()
+   *  .post('url')
+   *  .withFile('key', './file/path', { contentType: 'text/plain' })
+   */
+  withFile(path: string, options?: FormData.AppendOptions): Spec;
+  withFile(key: string, path: string, options?: FormData.AppendOptions): Spec;
+
   /**
    * attaches form data to the request with header - "application/x-www-form-urlencoded"
    * @example
diff --git a/src/models/Spec.js b/src/models/Spec.js
index 0bba29a..f39af91 100644
--- a/src/models/Spec.js
+++ b/src/models/Spec.js
@@ -1,4 +1,6 @@
 const FormData = require('form-data');
+const fs = require('fs');
+const path = require('path');
 const Tosser = require('./Tosser');
 const Expect = require('./expect');
 const State = require('./State');
@@ -204,6 +206,23 @@ class Spec {
     return this;
   }
 
+  withFile(key, filePath, options) {
+    if (typeof filePath === 'object') {
+      options = filePath;
+      filePath = key;
+      key = 'file';
+    }
+    if (!filePath) {
+      filePath = key;
+      key = 'file';
+    }
+    if (!options) {
+      options = {};
+    }
+    options.filename = options.filename ? options.filename : path.basename(filePath);
+    return this.withMultiPartFormData(key, fs.readFileSync(filePath), options);
+  }
+
   withCore(options) {
     this._request.core = options;
     return this;


Test patch:
diff --git a/src/models/E2E.js b/src/models/E2E.js
index 512f908..a03b963 100644
--- a/src/models/E2E.js
+++ b/src/models/E2E.js
@@ -2,6 +2,7 @@ const Step = require('./Step');
 const log = require('../helpers/logger');
 const helper = require('../helpers/helper');
 const rlc = require('../helpers/reporter.lifeCycle');
+const hr = require('../helpers/handler.runner');
 
 class E2E {
 
@@ -17,6 +18,15 @@ class E2E {
     this.steps = [];
   }
 
+  async init() {
+    try {
+      await hr.initialize();
+    } catch (error) {
+      log.error('Failed to initialize');
+      throw error;
+    }
+  }
+
   step(name) {
     reportStep(this.steps);
     const bail = this.steps.some(_step => _step.specs.some(spec => spec.status === 'FAILED'));
@@ -50,6 +60,12 @@ class E2E {
     if (errors.length > 0) {
       throw errors[0];
     }
+    try {
+      await hr.cleanup();
+    } catch (error) {
+      log.error('Failed to cleanup');
+      throw error;
+    }
   }
 
 }
diff --git a/test/component/e2e.spec.js b/test/component/e2e.spec.js
index 85d4f5b..f08572a 100644
--- a/test/component/e2e.spec.js
+++ b/test/component/e2e.spec.js
@@ -17,7 +17,7 @@ describe('E2E', () => {
             id: 1
           }
         }
-      }
+      };
     });
     handler.addInteractionHandler('save user', () => {
       return {
@@ -28,7 +28,7 @@ describe('E2E', () => {
         response: {
           status: 200
         }
-      }
+      };
     });
     handler.addInteractionHandler('delete user', () => {
       return {
@@ -39,7 +39,7 @@ describe('E2E', () => {
         response: {
           status: 200
         }
-      }
+      };
     });
     handler.addSpecHandler('User.AddUser', (ctx) => {
       const spec = ctx.spec;
@@ -322,4 +322,81 @@ describe('E2E', () => {
 
   });
 
+  describe('E2E - With Initializers & Cleanups', () => {
+
+    before(async () => {
+      handler.addInitializeHandler('Authorize', () => { });
+      handler.addCleanupHandler('UnAuthorize', () => { });
+      this.e2e = pactum.e2e('E2E with Initializers & Cleanups');
+      await this.e2e.init();
+    });
+
+    it('save user', async () => {
+      await this.e2e
+        .step('Save User')
+        .spec('User.AddUser')
+        .clean('User.DeleteUser');
+    });
+
+    it('get user', async () => {
+      await this.e2e
+        .step('Get User')
+        .spec('User.GetUser');
+    });
+
+    it('clean up', async () => {
+      await this.e2e.cleanup();
+    });
+
+  });
+
+  describe('E2E - With Initializer Fails', () => {
+
+    before(async () => {
+      handler.addInitializeHandler('Authorize', () => { throw 'Failed'; });
+      handler.addCleanupHandler('UnAuthorize', () => { });
+      this.e2e = pactum.e2e('E2E with Initializers & Cleanups');
+    });
+
+    it('init failed', async () => {
+      let err;
+      try {
+        await this.e2e.init();
+      } catch (error) {
+        err = error;
+      }
+      expect(err).not.undefined;
+    });
+
+    it('clean up', async () => {
+      await this.e2e.cleanup();
+    });
+
+  });
+
+  describe('E2E - With Initializer Fails', () => {
+
+    before(async () => {
+      handler.addInitializeHandler('Authorize', () => { });
+      handler.addCleanupHandler('UnAuthorize', () => { throw 'Failed'; });
+      this.e2e = pactum.e2e('E2E with Initializers & Cleanups');
+    });
+
+    it('clean up failed', async () => {
+      let err;
+      try {
+        await this.e2e.cleanup();
+      } catch (error) {
+        err = error;
+      }
+      expect(err).not.undefined;
+    });
+
+  });
+
+  after(() => {
+    handler.addInitializeHandler('Authorize', () => { });
+    handler.addCleanupHandler('UnAuthorize', () => { });
+  });
+
 });
\ No newline at end of file
diff --git a/test/component/request.spec.js b/test/component/request.spec.js
index 232815e..734d743 100644
--- a/test/component/request.spec.js
+++ b/test/component/request.spec.js
@@ -99,6 +99,74 @@ describe('Request', () => {
       .expectStatus(200);
   });
 
+  it('with file - just path', async () => {
+    await pactum.spec()
+      .useInteraction({
+        strict: false,
+        request: {
+          method: 'POST',
+          path: '/api/file'
+        },
+        response: {
+          status: 200
+        }
+      })
+      .post('http://localhost:9393/api/file')
+      .withFile('./package.json')
+      .expectStatus(200);
+  });
+
+  it('with file - path & options', async () => {
+    await pactum.spec()
+      .useInteraction({
+        strict: false,
+        request: {
+          method: 'POST',
+          path: '/api/file'
+        },
+        response: {
+          status: 200
+        }
+      })
+      .post('http://localhost:9393/api/file')
+      .withFile('./package.json', { contentType: 'application/json' })
+      .expectStatus(200);
+  });
+
+  it('with file - key & path', async () => {
+    await pactum.spec()
+      .useInteraction({
+        strict: false,
+        request: {
+          method: 'POST',
+          path: '/api/file'
+        },
+        response: {
+          status: 200
+        }
+      })
+      .post('http://localhost:9393/api/file')
+      .withFile('file-2', './package.json')
+      .expectStatus(200);
+  });
+
+  it('with file - key, path & options', async () => {
+    await pactum.spec()
+      .useInteraction({
+        strict: false,
+        request: {
+          method: 'POST',
+          path: '/api/file'
+        },
+        response: {
+          status: 200
+        }
+      })
+      .post('http://localhost:9393/api/file')
+      .withFile('file-2', './package.json', { contentType: 'application/json' })
+      .expectStatus(200);
+  });
+
   afterEach(() => {
     config.request.baseUrl = '';
     config.request.headers = {};