Repository: pactumjs/pactum (js)

Issue:
Support Global Expectations
### Background

PactumJS supports global request settings which are applied to every request that is sent through this library. These global request settings can be overridden at `spec` level.

```js
const { request } = require('pactum');

request.setBaseUrl();
request.setDefaultHeaders();
```

### Description

In the similar terms, this library should support global expectations that are applied to all the responses received by this library.

#### Example 

```js
const { response } = require('pactum');

response.setDefaultExpectResponseTime();
```

Add default global expectations

- setDefaultExpectResponseTime()
- setDefaultExpectStatus()
- setDefaultExpectHeader()

These expectations should be overridden at spec level as well.

```js
const pactum = require('pactum');
const { response } = pactum;

response.setDefaultExpectResponseTime(1000);

it('should be a teapot', async () => {
  await pactum.spec()
    .get('http://httpbin.org/status/418')
    .expectStatus(418); 
  // default expectation of response time is applied here
});

it('should not be a teapot', async () => {
  await pactum.spec()
    .get('http://httpbin.org/status/200')
    .expectStatus(200)
    .expectResponseTime(500);
  // default expectation of response time is overridden here
});
```

### AC

1. PactumJS supports global expectations on status, headers & response time
2. Add tests


Pull request:
Support global response default expectations
Closes #55 

Support for Global Expectations on responses.

**Feature Implemented**
Supports global response expectations for response - headers, status and response time.

- pactum.response
- setDefaultExpectResponseTime()
- setDefaultExpectStatus()
- setDefaultExpectHeader()

**Change log:**

- Files updated:
  - src/config.js
  - src/helpers/errors.js
  - src/helpers/utils.js
  - src/index.d.ts
  - src/index.js
  - src/models/Spec.js
  - src/models/expect.js
  
- Files added:
  - src/exports/response.d.ts
  - src/exports/response.js
  - test/unit/response.spec.js
  - src/exports/response.d.ts

Documentation updated: https://github.com/pactumjs/pactumjs.github.io/pull/2

Cheer!
Prasad

Patch:
diff --git a/src/config.js b/src/config.js
index fe64596..71f823d 100644
--- a/src/config.js
+++ b/src/config.js
@@ -9,6 +9,11 @@ const config = {
     headers: {},
     followRedirects: false
   },
+  response: {
+    time: process.env.PACTUM_RESPONSE_TIME ? parseInt(process.env.PACTUM_RESPONSE_TIME) : null,
+    status: process.env.PACTUM_RESPONSE_STATUS ? parseInt(process.env.PACTUM_RESPONSE_STATUS) : null,
+    headers: {}
+  },
   data: {
     ref: {
       map: {
diff --git a/src/exports/response.d.ts b/src/exports/response.d.ts
new file mode 100644
index 0000000..0f20f97
--- /dev/null
+++ b/src/exports/response.d.ts
@@ -0,0 +1,27 @@
+/**
+ * sets default expected response headers to all the responses
+ */
+export function setDefaultExpectHeaders(key: string, value: string): void;
+export function setDefaultExpectHeaders(headers: object): void;
+
+/**
+ * sets a default expected response time for all responses in ms
+ * @env PACTUM_RESPONSE_TIME
+ */
+export function setDefaultExpectResponseTime(respTime: number): void;
+
+/**
+ * sets default expected response status
+ * @env PACTUM_RESPONSE_STATUS 
+ */
+export function setDefaultExpectStatus(status: number): void;
+
+/**
+ * removes default header
+ */
+ export function removeDefaultExpectHeader(key: string): void;
+
+ /**
+  * removes all default headers
+  */
+ export function removeDefaultExpectHeaders(): void;
\ No newline at end of file
diff --git a/src/exports/response.js b/src/exports/response.js
new file mode 100644
index 0000000..4247d76
--- /dev/null
+++ b/src/exports/response.js
@@ -0,0 +1,49 @@
+const config = require('../config');
+const helper = require('../helpers/helper');
+const { PactumResponseError } = require('../helpers/errors');
+
+
+const response = {
+
+  setDefaultExpectHeaders(key, value) {
+    if (!key) {
+      throw new PactumResponseError(`Invalid expected response header key provided - ${key}`);
+    }
+    if (typeof key === 'string') {
+      config.response.headers[key] = value;
+    } else {
+      if (!helper.isValidObject(key)) {
+        throw new PactumResponseError(`Invalid expected response headers provided - ${key}`);
+      }
+      Object.assign(config.response.headers, key);
+    }
+  },
+
+  setDefaultExpectResponseTime(respTime) {
+    if (typeof respTime !== 'number') {
+      throw new PactumResponseError(`Invalid expected response time provided - ${respTime}`);
+    }
+    config.response.time = respTime;
+  },
+
+  setDefaultExpectStatus(status) {
+    if (typeof status !== 'number' || 100 > status || status > 599) {
+      throw new PactumResponseError(`Invalid expected response status provided - ${status}`);
+    }
+    config.response.status = status;
+  },
+
+  removeDefaultExpectHeader(key) {
+    if (!key) {
+      throw new PactumResponseError(`Invalid expected response header key provided - ${key}`);
+    }
+    delete config.response.headers[key];
+  },
+
+  removeDefaultExpectHeaders() {
+    config.response.headers = {};
+  }
+
+};
+
+module.exports = response;
\ No newline at end of file
diff --git a/src/helpers/errors.js b/src/helpers/errors.js
index 53ca17c..cd16f5b 100644
--- a/src/helpers/errors.js
+++ b/src/helpers/errors.js
@@ -1,5 +1,6 @@
 class PactumInteractionError extends Error {}
 class PactumRequestError extends Error {}
+class PactumResponseError extends Error {}
 class PactumConfigurationError extends Error {}
 class PactumMatcherError extends Error {}
 class PactumOptionsError extends Error {}
@@ -8,6 +9,7 @@ class PactumHandlerError extends Error {}
 module.exports = {
   PactumInteractionError,
   PactumRequestError,
+  PactumResponseError,
   PactumConfigurationError,
   PactumMatcherError,
   PactumOptionsError,
diff --git a/src/helpers/utils.js b/src/helpers/utils.js
index e98c97d..1183179 100644
--- a/src/helpers/utils.js
+++ b/src/helpers/utils.js
@@ -49,8 +49,17 @@ const utils = {
   printReqAndRes(request, response) {
     log.warn('Request', request);
     log.warn('Response', helper.getTrimResponse(response));
-  }
+  },
 
+  upsertValues(jsonArray, item) {
+    const index = jsonArray.findIndex(_item => _item.key === item.key);
+    if (index > -1 ) {
+      jsonArray[index] = item
+    } else {
+      jsonArray.push(item);
+    }
+  }
+  
 };
 
 function validatePath(req, interaction) {
diff --git a/src/index.d.ts b/src/index.d.ts
index 4523652..2dd33bf 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -8,6 +8,7 @@ export * as matchers from './exports/matcher';
 export * as mock from './exports/mock';
 export * as reporter from './exports/reporter';
 export * as request from './exports/request';
+export * as response from './exports/response';
 export * as settings from './exports/settings';
 export * as stash from './exports/stash';
 export * as state from './exports/state';
diff --git a/src/index.js b/src/index.js
index bbea544..5cde337 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,6 +6,7 @@ const helper = require('./helpers/helper');
 
 const mock = require('./exports/mock');
 const request = require('./exports/request');
+const response = require('./exports/response');
 const settings = require('./exports/settings');
 const handler = require('./exports/handler');
 const state = require('./exports/state');
@@ -17,6 +18,7 @@ const pactum = {
 
   mock,
   request,
+  response,
   settings,
   handler,
   state,
diff --git a/src/models/Spec.js b/src/models/Spec.js
index 9ed65fe..a11824c 100644
--- a/src/models/Spec.js
+++ b/src/models/Spec.js
@@ -7,6 +7,7 @@ const State = require('./State');
 const helper = require('../helpers/helper');
 const log = require('../exports/logger').get();
 const th = require('../helpers/toss.helper');
+const utils = require('../helpers/utils');
 const { PactumRequestError } = require('../helpers/errors');
 const responseExpect = require('../exports/expect');
 const hr = require('../helpers/handler.runner');
@@ -31,6 +32,7 @@ class Spec {
     this.interactions = [];
     this._wait = null;
     hr.spec(name, data, this);
+    this._expect.setDefaultResponseExpectations();
   }
 
   name(value) {
@@ -311,7 +313,7 @@ class Spec {
   }
 
   expectHeader(header, value) {
-    this._expect.headers.push({
+    utils.upsertValues(this._expect.headers, {
       key: header,
       value
     });
@@ -319,7 +321,7 @@ class Spec {
   }
 
   expectHeaderContains(header, value) {
-    this._expect.headerContains.push({
+    utils.upsertValues(this._expect.headerContains, {
       key: header,
       value
     });
diff --git a/src/models/expect.js b/src/models/expect.js
index 82719fe..b4ee7b3 100644
--- a/src/models/expect.js
+++ b/src/models/expect.js
@@ -1,6 +1,8 @@
 const assert = require('assert');
 const jqy = require('json-query');
 
+const config = require('../config');
+const utils = require('../helpers/utils');
 const file = require('../helpers/file.utils');
 const log = require('../exports/logger').get();
 const Compare = require('../helpers/compare');
@@ -95,7 +97,7 @@ class Expect {
       const expectedHeaderObject = this.headers[i];
       const expectedHeader = expectedHeaderObject.key;
       const expectedHeaderValue = expectedHeaderObject.value;
-      if (!response.headers[expectedHeader]) {
+      if (!(expectedHeader in response.headers)) {
         this.fail(`Header '${expectedHeader}' not present in HTTP response`);
       }
       if (expectedHeaderValue !== undefined) {
@@ -119,7 +121,7 @@ class Expect {
       const expectedHeaderObject = this.headerContains[i];
       const expectedHeader = expectedHeaderObject.key;
       const expectedHeaderValue = expectedHeaderObject.value;
-      if (!response.headers[expectedHeader]) {
+      if (!(expectedHeader in response.headers)) {
         this.fail(`Header '${expectedHeader}' not present in HTTP response`);
       }
       if (expectedHeaderValue !== undefined) {
@@ -362,6 +364,20 @@ class Expect {
     assert.fail(error);
   }
 
+  setDefaultResponseExpectations() {
+    if (config.response.status) {
+      this.statusCode = config.response.status;
+    }
+    if (config.response.time) {
+      this.responseTime = config.response.time;
+    }
+    if (config.response.headers && Object.keys(config.response.headers).length !== 0) {
+      for (const [key, value] of Object.entries(config.response.headers)) {
+        utils.upsertValues(this.headers, { key, value });
+      }
+    }
+  }
+
 }
 
 module.exports = Expect;


Test patch:
diff --git a/test/component/response.spec.js b/test/component/response.spec.js
new file mode 100644
index 0000000..4496022
--- /dev/null
+++ b/test/component/response.spec.js
@@ -0,0 +1,207 @@
+const pactum = require('../../src/index');
+const { request, response } = pactum;
+const config = require('../../src/config');
+const { expect } = require('chai');
+
+describe('Response', () => {
+
+  it('with default expected response status - override value', async () => {
+    request.setBaseUrl('http://localhost:9392');
+    response.setDefaultExpectStatus(200);
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 400
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(400);
+  });
+
+  it('with default expected response status - valid status', async () => {
+    request.setBaseUrl('http://localhost:9392');
+    response.setDefaultExpectStatus(200);
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 200
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(200);
+  });
+
+  it('with default expected response time - override value', async () => {
+    request.setBaseUrl('http://localhost:9392');
+    response.setDefaultExpectResponseTime(100);
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 400,
+          fixedDelay: 130
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(400)
+      .expectResponseTime(500);
+  });
+
+  it('with default expected response time - valid time', async () => {
+    request.setBaseUrl('http://localhost:9392');
+    response.setDefaultExpectResponseTime(500);
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 200,
+          fixedDelay: 50
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(200);
+  });
+
+  it('with default expected response header - header not present', async () => {
+    request.setBaseUrl('http://localhost:9393');
+    response.setDefaultExpectHeaders('x-header', 'value');
+    try {
+      await pactum
+        .spec()
+        .useInteraction({
+          request: {
+            method: 'GET',
+            path: '/users'
+          },
+          response: {
+            status: 200,
+            headers: {
+              'x': '2'
+            }
+          }
+        })
+        .get('http://localhost:9393/users')
+        .expectStatus(200);
+    }
+    catch (error) {
+      expect(error.message).equals(`Header 'x-header' not present in HTTP response`);
+    }
+  });
+
+  it('with default expected response header - override to empty value', async () => {
+    request.setBaseUrl('http://localhost:9393');
+    response.setDefaultExpectHeaders('x-header', 'value');
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 200,
+          headers: {
+            'x-header': ''
+          }
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(200)
+      .expectHeader('x-header', '');
+  });
+
+  it('with default expected response header - override value', async () => {
+    request.setBaseUrl('http://localhost:9393');
+    response.setDefaultExpectHeaders('x-header', 'a');
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 200,
+          headers: {
+            'x-header': 'b'
+          }
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectHeader('x-header', 'b')
+      .expectStatus(200);
+  });
+
+  it('with default expected response header - multiple headers', async () => {
+    request.setBaseUrl('http://localhost:9393');
+    response.setDefaultExpectHeaders({
+      'x-header': 'a',
+      'x-header2': 'b'
+    });
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 200,
+          headers: {
+            'x-header': 'a',
+            'x-header2': 'b'
+          }
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(200);
+  });
+
+  it('with default expected response - all valid values', async () => {
+    request.setBaseUrl('http://localhost:9392');
+    response.setDefaultExpectResponseTime(500);
+    response.setDefaultExpectHeaders("x-header", "value");
+    response.setDefaultExpectStatus(200)
+    await pactum
+      .spec()
+      .useInteraction({
+        request: {
+          method: 'GET',
+          path: '/users'
+        },
+        response: {
+          status: 200,
+          headers: {
+            "x-header": "value"
+          },
+          fixedDelay: 50
+        }
+      })
+      .get('http://localhost:9393/users')
+      .expectStatus(200);
+  });
+
+  afterEach(() => {
+    config.response.status = null;
+    config.response.time = null;
+    config.response.headers = {};
+  });
+});
diff --git a/test/unit/response.spec.js b/test/unit/response.spec.js
new file mode 100644
index 0000000..4d22da7
--- /dev/null
+++ b/test/unit/response.spec.js
@@ -0,0 +1,89 @@
+const expect = require('chai').expect;
+
+const response = require('../../src/exports/response');
+const config = require('../../src/config');
+
+describe('response', () => {
+
+  it('setDefaultExpectHeaders - null', () => {
+    expect(() => response.setDefaultExpectHeaders(null)).throws('Invalid expected response header key provided - null');
+  });
+
+  it('setDefaultExpectHeaders - undefined', () => {
+    expect(() => response.setDefaultExpectHeaders()).throws('Invalid expected response header key provided - undefined');
+  });
+
+  it('removeDefaultExpectHeader - undefined', () => {
+    expect(() => response.removeDefaultExpectHeader()).throws('Invalid expected response header key provided - undefined');
+  });
+
+  it('setDefaultHeader & setDefaultHeaders & remove', () => {
+    response.setDefaultExpectHeaders('hello', 'world');
+    response.setDefaultExpectHeaders('no', 'space');
+    response.setDefaultExpectHeaders({
+      'gta': 'v',
+      'hello': 'space'
+    });
+    expect(config.response.headers).deep.equals({
+      'hello': 'space',
+      'no': 'space',
+      'gta': 'v'
+    });
+    response.removeDefaultExpectHeader('no');
+    expect(config.response.headers).deep.equals({
+      'hello': 'space',
+      'gta': 'v'
+    });
+    response.removeDefaultExpectHeaders();
+    expect(config.response.headers).deep.equals({});
+  });
+
+  it('setDefaultExpectResponseTime - null', () => {
+    expect(() => response.setDefaultExpectResponseTime(null)).throws('Invalid expected response time provided - null');
+  });
+
+  it('setDefaultExpectResponseTime - undefined', () => {
+    expect(() => response.setDefaultExpectResponseTime()).throws('Invalid expected response time provided - undefined');
+  });
+
+  it('setDefaultExpectResponseTime - string', () => {
+    expect(() => response.setDefaultExpectResponseTime('1000')).throws('Invalid expected response time provided - 1000');
+  });
+
+  it('setDefaultExpectResponseTime - Valid value', () => {
+    response.setDefaultExpectResponseTime(1000);
+    expect(config.response.time).deep.equals(1000);
+  });
+
+  it('setDefaultExpectStatus - null', () => {
+    expect(() => response.setDefaultExpectStatus(null)).throws('Invalid expected response status provided - null');
+  });
+
+  it('setDefaultExpectStatus - undefined', () => {
+    expect(() => response.setDefaultExpectStatus()).throws('Invalid expected response status provided - undefined');
+  });
+
+  it('setDefaultExpectStatus - string', () => {
+    expect(() => response.setDefaultExpectStatus('1000')).throws('Invalid expected response status provided - 1000');
+  });
+
+  it('setDefaultExpectStatus - status > 599', () => {
+    expect(() => response.setDefaultExpectStatus(600)).throws('Invalid expected response status provided - 600');
+  });
+
+  it('setDefaultExpectStatus - status < 100', () => {
+    expect(() => response.setDefaultExpectStatus(99)).throws('Invalid expected response status provided - 99');
+  });
+
+  it('setDefaultExpectStatus - Valid value', () => {
+    response.setDefaultExpectStatus(200);
+    expect(config.response.status).deep.equals(200);
+  });
+
+  afterEach(() => {
+    config.response.headers = {};
+    config.response.time = null;
+    config.response.status = null;
+  });
+
+});
\ No newline at end of file