Repository: pactumjs/pactum
Pull request #44 (2021-03-01 13:53:33)

3.0.6
- reversed retry logic
- dynamic wait for non CRUD
- reporting for bdd


Patch:
diff --git a/package-lock.json b/package-lock.json
index b8ae524..1117360 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "pactum",
-  "version": "3.0.4",
+  "version": "3.0.6",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index b5a1d1a..d17046c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "pactum",
-  "version": "3.0.5",
+  "version": "3.0.6",
   "description": "REST API Testing Tool for all levels in a Test Pyramid",
   "main": "./src/index.js",
   "types": "./src/index.d.ts",
diff --git a/src/config.js b/src/config.js
index 94df97c..fe64596 100644
--- a/src/config.js
+++ b/src/config.js
@@ -44,6 +44,9 @@ const config = {
   },
   snapshot: {
     dir: '.pactum/snapshots'
+  },
+  reporter : {
+    autoRun: true
   }
 };
 
diff --git a/src/exports/expect.js b/src/exports/expect.js
index 26e2d46..8059287 100644
--- a/src/exports/expect.js
+++ b/src/exports/expect.js
@@ -1,11 +1,12 @@
 const ExpectModel = require('../models/expect');
+const utils = require('../helpers/utils');
 
 class Have {
 
-  constructor(response, request) {
+  constructor(response, spec) {
     this.expect = new ExpectModel();
     this.response = response;
-    this.request = request;
+    this.spec = spec;
   }
 
   status(code) {
@@ -84,30 +85,38 @@ class Have {
   }
 
   _validate() {
-    return this.expect.validate({}, this.response);
+    try {
+      return this.expect.validate({}, this.response);
+    } catch (error) {
+      if (this.spec && this.spec.status !== 'FAILED') {
+        this.spec.status = 'FAILED';
+        utils.printReqAndRes(this.spec._request, this.response);
+      }
+      throw error;
+    }
   }
 
 }
 
 class To {
 
-  constructor(response, request) {
-    this.have = new Have(response, request);
+  constructor(response, spec) {
+    this.have = new Have(response, spec);
   }
 
 }
 
 class Expect {
 
-  constructor(response, request) {
-    this.to = new To(response, request);
-    this.should = new To(response, request);
+  constructor(response, spec) {
+    this.to = new To(response, spec);
+    this.should = new To(response, spec);
   }
 
 }
 
-function expect(response, request) {
-  return new Expect(response, request);
+function expect(response, spec) {
+  return new Expect(response, spec);
 }
 
 module.exports = expect;
\ No newline at end of file
diff --git a/src/exports/mock.d.ts b/src/exports/mock.d.ts
index 58452c9..bb22ba7 100644
--- a/src/exports/mock.d.ts
+++ b/src/exports/mock.d.ts
@@ -87,11 +87,11 @@ export function stop(): Promise<void>;
  * @returns interaction id
  * @example
  * mock.addInteraction({
- *  withRequest: {
+ *  request: {
  *   method: 'GET',
  *   path: '/api/orders'
  *  },
- *  willRespondWith: {
+ *  response: {
  *   status: 200,
  *   body: 'your orders'
  *  }
diff --git a/src/exports/settings.d.ts b/src/exports/settings.d.ts
index 784c93e..24b5edf 100644
--- a/src/exports/settings.d.ts
+++ b/src/exports/settings.d.ts
@@ -15,4 +15,5 @@ export interface Strategy {
 export function setAssertHandlerStrategy(strategy: Strategy): void;
 export function setAssertExpressionStrategy(strategy: Strategy): void;
 export function setCaptureHandlerStrategy(strategy: Strategy): void;
-export function setSnapshotDirectoryPath(path: string): void;
\ No newline at end of file
+export function setSnapshotDirectoryPath(path: string): void;
+export function setReporterAutoRun(auto: boolean): void;
\ No newline at end of file
diff --git a/src/exports/settings.js b/src/exports/settings.js
index db864a1..f97363a 100644
--- a/src/exports/settings.js
+++ b/src/exports/settings.js
@@ -21,6 +21,10 @@ const settings = {
 
   setSnapshotDirectoryPath(path) {
     config.snapshot.dir = path;
+  },
+
+  setReporterAutoRun(val) {
+    config.reporter.autoRun = val;
   }
 
 };
diff --git a/src/helpers/handler.runner.js b/src/helpers/handler.runner.js
index 9c22a2f..c101364 100644
--- a/src/helpers/handler.runner.js
+++ b/src/helpers/handler.runner.js
@@ -10,6 +10,12 @@ const hr = {
 
   capture(name, ctx) {
     return handler.getCaptureHandler(name)(ctx);
+  },
+
+  spec(name, data, spec) {
+    if (typeof name !== 'undefined') {
+      handler.getSpecHandler(name)({ spec, data });
+    }
   }
 
 };
diff --git a/src/helpers/utils.js b/src/helpers/utils.js
index ba56b75..015d912 100644
--- a/src/helpers/utils.js
+++ b/src/helpers/utils.js
@@ -2,6 +2,7 @@ const { compare } = require('pactum-matchers').utils;
 const graphQL = require('./graphQL');
 
 const log = require('./logger');
+const helper = require('./helper');
 
 const utils = {
 
@@ -43,6 +44,11 @@ const utils = {
       log.debug(`Interaction with id ${interactionId} failed to match - HTTP Body`);
     }
     return null;
+  },
+
+  printReqAndRes(request, response) {
+    log.warn('Request', request);
+    log.warn('Response', helper.getTrimResponse(response));
   }
 
 };
diff --git a/src/index.d.ts b/src/index.d.ts
index 0e42a20..809a993 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -20,13 +20,13 @@ export * as state from './exports/state';
  *  .expectStatus(200);
  */
 export function spec(): Spec;
+
 /**
- * @param ctx - test runner context
- */
-export function spec(ctx?: object): Spec;
-/**
- * @param name - spec handler name
- * @param data - custom data
+ * returns an instance of a spec & runs custom spec handler
+ * @example
+ * await pactum.spec()
+ *  .use('spec handler name', { optional: 'data' })
+ *  .expectStatus(200);
  */
 export function spec(name: string, data?: any): Spec;
 
diff --git a/src/models/Spec.d.ts b/src/models/Spec.d.ts
index fc2fa2e..63a67c4 100644
--- a/src/models/Spec.d.ts
+++ b/src/models/Spec.d.ts
@@ -37,6 +37,15 @@ declare class Spec {
    *  .expectStatus(200);
    */
   setState(name: string, data?: any): Spec;
+
+  /**
+   * runs custom spec handler
+   * @example
+   * await pactum.spec()
+   *  .use('spec handler name', { optional: 'data' })
+   *  .expectStatus(200);
+   */
+  use(name: string, data?: any): Spec
   
   /**
    * adds a interaction to the server & auto removed after execution
@@ -529,6 +538,7 @@ declare class Spec {
    *  .expectStatus(200)
    */
   wait(milliseconds: number): Spec;
+  wait(spec: Spec): Spec;
 
   /**
    * prints request & response
@@ -552,6 +562,11 @@ declare class Spec {
    * returns new instance of cleanup spec
    */
   clean(name?: string, data?: any): Spec;
+
+  /**
+   * runs registered reporters
+   */
+  end(): Spec;
 }
 
 declare namespace Spec {}
\ No newline at end of file
diff --git a/src/models/Spec.js b/src/models/Spec.js
index 356dbff..36f309a 100644
--- a/src/models/Spec.js
+++ b/src/models/Spec.js
@@ -7,7 +7,8 @@ const log = require('../helpers/logger');
 const th = require('../helpers/toss.helper');
 const { PactumRequestError } = require('../helpers/errors');
 const responseExpect = require('../exports/expect');
-const handler = require('../exports/handler');
+const hr = require('../helpers/handler.runner');
+const rlc = require('../helpers/reporter.lifeCycle');
 
 class Spec {
 
@@ -27,19 +28,8 @@ class Spec {
     this._state = new State();
     this.previousLogLevel = null;
     this.interactions = [];
-    this._waitDuration = null;
-    this._init(name, data);
-  }
-
-  _init(ctx, data) {
-    if (ctx && typeof ctx === "object") {
-      const test = ctx.test;
-      if (test && test.fullTitle) {
-        this._name = test.fullTitle();
-      }
-    } else {
-      this._runHandler(ctx, data);
-    }
+    this._wait = null;
+    hr.spec(name, data, this);
   }
 
   name(value) {
@@ -48,18 +38,16 @@ class Spec {
     return this;
   }
 
-  _runHandler(name, data) {
-    if (typeof name !== 'undefined') {
-      const fun = handler.getSpecHandler(name);
-      fun({ spec: this, data });
-    }
-  }
-
   setState(name, data) {
     this._state.add(name, data);
     return this;
   }
 
+  use(name, data) {
+    hr.spec(name, data, this);
+    return this;
+  }
+
   useInteraction(interaction, data) {
     this.interactions.push({ interaction, data });
     return this;
@@ -384,7 +372,7 @@ class Spec {
   }
 
   wait(ms) {
-    this._waitDuration = ms;
+    this._wait = ms;
     return this;
   }
 
@@ -408,7 +396,12 @@ class Spec {
     if (!this._response) {
       throw new PactumRequestError(`'response()' should be called after resolving 'toss()'`);
     }
-    return responseExpect(this._response, this._request);
+    return responseExpect(this._response, this);
+  }
+
+  end() {
+    rlc.afterSpecReport(this);
+    return this;
   }
 
 }
diff --git a/src/models/Tosser.js b/src/models/Tosser.js
index 6c0b6e0..7963fcd 100644
--- a/src/models/Tosser.js
+++ b/src/models/Tosser.js
@@ -4,9 +4,11 @@ const log = require('../helpers/logger');
 const rlc = require('../helpers/reporter.lifeCycle');
 const requestProcessor = require('../helpers/requestProcessor');
 const th = require('../helpers/toss.helper');
+const utils = require('../helpers/utils');
 const mock = require('../exports/mock');
 const handler = require('../exports/handler');
 const request = require('../exports/request');
+const config = require('../config');
 
 class Tosser {
 
@@ -28,7 +30,7 @@ class Tosser {
     await this.addInteractionsToServer();
     await this.setResponse();
     this.inspect();
-    await this.sleep(this.spec._waitDuration);
+    await this.wait();
     this.setPreviousLogLevel();
     await this.getInteractionsFromServer();
     await this.removeInteractionsFromServer();
@@ -56,17 +58,17 @@ class Tosser {
     const retryOptions = this.request.retryOptions;
     if (retryOptions) {
       const { count, delay, strategy } = retryOptions;
-      let retry = false;
+      let retry = true;
       for (let i = 0; i < count; i++) {
+        const ctx = { req: this.request, res: this.response };
         if (typeof strategy === 'function') {
-          retry = strategy(this.request, this.response);
+          retry = strategy(ctx);
         }
         if (typeof strategy === 'string') {
           const handlerFun = handler.getRetryHandler(strategy);
-          const ctx = { req: this.request, res: this.response };
           retry = handlerFun(ctx);
         }
-        if (retry) {
+        if (!retry) {
           await helper.sleep(delay);
           this.response = await getResponse(this.request);
         } else {
@@ -81,7 +83,16 @@ class Tosser {
   inspect() {
     if (this.spec._inspect) {
       log.warn('Inspecting Request & Response');
-      this.printReqAndRes();
+      utils.printReqAndRes(this.request, this.response);
+    }
+  }
+
+  async wait() {
+    const _wait = this.spec._wait;
+    if (typeof _wait === 'number') {
+      await helper.sleep(_wait);
+    } else if (_wait && typeof _wait === 'object') {
+      await _wait;
     }
   }
 
@@ -117,12 +128,12 @@ class Tosser {
       this.validateInteractions();
       await this.validateResponse();
       this.spec.status = 'PASSED';
-      rlc.afterSpecReport(this.spec);
+      this.runReport();
     } catch (error) {
       this.spec.status = 'FAILED';
       this.spec.failure = error.toString();
-      rlc.afterSpecReport(this.spec);
-      this.printReqAndRes();
+      this.runReport();
+      utils.printReqAndRes(this.request, this.response);
       throw error;
     }
   }
@@ -131,7 +142,7 @@ class Tosser {
     if (this.response instanceof Error) {
       this.spec.status = 'ERROR';
       this.spec.failure = this.response.toString();
-      rlc.afterSpecReport(this.spec);
+      this.runReport();
       this.expect.fail(this.response);
     }
   }
@@ -144,14 +155,9 @@ class Tosser {
     await this.expect.validate(this.request, this.response);
   }
 
-  printReqAndRes() {
-    log.warn('Request', this.request);
-    log.warn('Response', helper.getTrimResponse(this.response));
-  }
-
-  sleep(ms) {
-    if (typeof ms === 'number') {
-      return helper.sleep(ms);
+  runReport() {
+    if (config.reporter.autoRun) {
+      rlc.afterSpecReport(this.spec);
     }
   }
 
diff --git a/src/models/server.js b/src/models/server.js
index 8731d8d..e6a21ea 100644
--- a/src/models/server.js
+++ b/src/models/server.js
@@ -175,13 +175,18 @@ function getDelay(response) {
  * @param {ExpressResponse} res - HTTP response
  */
 function sendInteractionNotFoundResponse(req, res) {
-  log.warn('Interaction Not Found in Mock Server', {
+  const msg = {
     method: req.method,
     path: req.path,
     headers: req.headers,
-    query: req.query,
-    body: req.body
-  });
+  };
+  if (req.query && typeof req.query === 'object' && Object.keys(req.query) > 0) {
+    msg.queryParams = req.query;
+  }
+  if (req.body) {
+    msg.body = req.body;
+  }
+  log.warn('Interaction Not Found in Mock Server', msg);
   res.status(404);
   res.send('Interaction Not Found');
 }


Test patch:
diff --git a/test/component/chai.spec.js b/test/component/bdd.spec.js
similarity index 79%
rename from test/component/chai.spec.js
rename to test/component/bdd.spec.js
index aa50b74..7789483 100644
--- a/test/component/chai.spec.js
+++ b/test/component/bdd.spec.js
@@ -1,10 +1,9 @@
 const { like } = require('pactum-matchers');
 const pactum = require('../../src/index');
-const handler = pactum.handler;
-const expect = pactum.expect;
+const { handler, expect, settings } = pactum;
 const ce = require('chai').expect;
 
-describe('Chai Like Assertions', () => {
+describe('BDD', () => {
 
   let spec = pactum.spec();
   let response;
@@ -90,6 +89,12 @@ describe('Chai Like Assertions', () => {
       err = error;
     }
     ce(err).not.undefined;
+    try {
+      spec.response().to.have.responseTimeLessThan(0);
+    } catch (error) {
+      err = error;
+    }
+    ce(err).not.undefined;
   });
 
   it('should run a custom expect handler', async () => {
@@ -108,4 +113,31 @@ describe('Chai Like Assertions', () => {
     ce(spec.returns('name')).equals('snow');
   });
 
+});
+
+describe('BDD - AutoReportRunner Disabled', () => {
+
+  before(() => {
+    settings.setReporterAutoRun(false);
+    this.spec = pactum.spec();
+    this.spec.useInteraction('default get');
+  });
+
+  it('get default', () => {
+    this.spec.get('http://localhost:9393/default/get');
+  });
+
+  it('should return a response', async () => {
+    await this.spec.toss();
+  });
+
+  it('should return a status 200', () => {
+    this.spec.response().to.have.status(200);
+  });
+
+  after(() => {
+    this.spec.end();
+    settings.setReporterAutoRun(true);
+  });
+
 });
\ No newline at end of file
diff --git a/test/component/retries.spec.js b/test/component/retries.spec.js
index 289e5c9..f6a96e8 100644
--- a/test/component/retries.spec.js
+++ b/test/component/retries.spec.js
@@ -28,7 +28,7 @@ describe('Retries', () => {
       .retry({
         delay: 1,
         count: 1,
-        strategy: (req, res) => res.statusCode !== 200
+        strategy: ({ res }) => res.statusCode === 200
       })
       .expectStatus(200)
       .toss();
@@ -47,18 +47,21 @@ describe('Retries', () => {
               status: 200
             }
           }
+        },
+        expects: {
+          callCount: 1
         }
       })
       .get('http://localhost:9393/api/projects/1')
       .retry({
-        strategy: (req, res) => res.statusCode !== 200
+        strategy: ({ res }) => res.statusCode === 200
       })
       .expectStatus(200)
       .toss();
   });
 
   it('custom retry strategy', async () => {
-    pactum.handler.addRetryHandler('RetryTill200', ({res}) => res.statusCode !== 200);
+    pactum.handler.addRetryHandler('RetryTill200', ({ res }) => res.statusCode === 200);
     await pactum.spec()
       .useInteraction({
         request: {
@@ -120,4 +123,103 @@ describe('Retries', () => {
     expect(err.message).contains(`Retry Handler Not Found - 'RetryTill400'`);
   });
 
+});
+
+describe('Wait', () => {
+
+  it('valid wait with spec', async () => {
+    const spec = pactum.spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/api/status'
+        },
+        response: {
+          onCall: {
+            0: {
+              status: 202
+            },
+            1: {
+              status: 200
+            }
+          }
+        },
+        expects: {
+          callCount: 2
+        }
+      })
+      .get('http://localhost:9393/api/status')
+      .retry({
+        delay: 1,
+        count: 1,
+        strategy: ({ res }) => res.statusCode === 200
+      })
+      .expectStatus(200)
+      .toss();
+
+    await pactum.spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/api/wait'
+        },
+        response: {
+          status: 200
+        }
+      })
+      .get('http://localhost:9393/api/wait')
+      .wait(spec)
+      .expectStatus(200);
+  });
+
+  it('invalid wait with spec', async () => {
+    const spec = pactum.spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/api/status'
+        },
+        response: {
+          onCall: {
+            0: {
+              status: 202
+            },
+            1: {
+              status: 400
+            }
+          }
+        },
+        expects: {
+          callCount: 2
+        }
+      })
+      .get('http://localhost:9393/api/status')
+      .retry({
+        delay: 1,
+        count: 1,
+        strategy: ({ res }) => res.statusCode === 200
+      })
+      .expectStatus(200)
+      .toss();
+    let err;
+    try {
+      await pactum.spec()
+        .useInteraction({
+          request: {
+            method: 'GET',
+            path: '/api/wait'
+          },
+          response: {
+            status: 200
+          }
+        })
+        .get('http://localhost:9393/api/wait')
+        .wait(spec)
+        .expectStatus(200);
+    } catch (error) {
+      err = error;
+    }
+    expect(err).not.undefined;
+  });
+
 });
\ No newline at end of file
diff --git a/test/component/spec.handler.spec.js b/test/component/spec.handler.spec.js
index 490c99d..4c940f5 100644
--- a/test/component/spec.handler.spec.js
+++ b/test/component/spec.handler.spec.js
@@ -10,25 +10,76 @@ describe('Spec Handler', () => {
           method: 'GET',
           path: '/api/users'
         },
+        response: {
+          status: 200,
+          body: [
+            {
+              id: 1
+            },
+            {
+              id: 2
+            }
+          ]
+        }
+      };
+    });
+    handler.addInteractionHandler('get user one', () => {
+      return {
+        request: {
+          method: 'GET',
+          path: '/api/users/1'
+        },
         response: {
           status: 200,
           body: {
             id: 1
           }
         }
-      }
+      };
     });
-    handler.addSpecHandler('get user', (ctx) => {
+    handler.addSpecHandler('get users', (ctx) => {
       const spec = ctx.spec;
       spec.useInteraction('get users');
       spec.get('http://localhost:9393/api/users');
     });
+    handler.addSpecHandler('get user', (ctx) => {
+      const spec = ctx.spec;
+      spec.useInteraction('get user one');
+      spec.get('http://localhost:9393/api/users' + `/${ctx.data}`);
+    });
+  });
+
+  it('get users', async () => {
+    await pactum
+      .spec('get users')
+      .expectStatus(200)
+      .expectJson([
+        {
+          id: 1
+        },
+        {
+          id: 2
+        }
+      ]);
+  });
+
+  it('get user one', async () => {
+    await pactum
+      .spec('get user', 1)
+      .expectStatus(200)
+      .expectJson({
+        id: 1
+      });
   });
 
-  it('get user', async () => {
+  it('get user one - use', async () => {
     await pactum
-      .spec('get user')
-      .expectStatus(200);
+      .spec()
+      .use('get user', 1)
+      .expectStatus(200)
+      .expectJson({
+        id: 1
+      });
   });
 
 });
\ No newline at end of file
diff --git a/test/unit/spec.spec.js b/test/unit/spec.spec.js
index 67b5d44..33628a0 100644
--- a/test/unit/spec.spec.js
+++ b/test/unit/spec.spec.js
@@ -316,19 +316,4 @@ describe('Spec', () => {
     expect(spec._name).equals('Custom Name');
   });
 
-  it('name - passing mocha context', function() {
-    const spec = new Spec(this);
-    expect(spec._name).equals('Spec name - passing mocha context');
-  });
-
-  it('name - passing invalid context to spec', function() {
-    const spec = new Spec({});
-    expect(spec._name).equals('');
-  });
-
-  it('name - passing invalid context with test to spec', function() {
-    const spec = new Spec({ test: null });
-    expect(spec._name).equals('');
-  });
-
 });
\ No newline at end of file