From 36f4ba7b7ee8f7340d6d8e91d2ada5e22fc7e56f Mon Sep 17 00:00:00 2001
From: Anand Venkatraman <avenkatraman@pulsepoint.com>
Date: Wed, 4 Oct 2017 01:00:37 +0530
Subject: [PATCH] PulsePoint Lite adpater changes (#1630)

* ET-1691: Pulsepoint Analytics adapter for Prebid. (#1)

* ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter

* ET-1691: Adding pulsepoint analytics and tests for pulsepoint adapter

* ET-1691: cleanup

* ET-1691: minor

* ET-1691: revert package.json change

* Adding bidRequest to bidFactory.createBid method as per https://github.com/prebid/Prebid.js/issues/509

* ET-1765: Adding support for additional params in PulsePoint adapter (#2)

* ET-1850: Fixing https://github.com/prebid/Prebid.js/issues/866

* Minor fix

* Refactoring current functionality to work with bidderFactory (for 1.0 migration)

* More tests.

* Adding support for "app" requests.

* fixing eslint issues

* adding adapter documentation

* minor doc update

* removing usage of reserved keyword 'native'
---
 modules/pulsepointLiteBidAdapter.js           | 505 +++++++++---------
 modules/pulsepointLiteBidAdapter.md           |  43 ++
 .../modules/pulsepointLiteBidAdapter_spec.js  | 271 +++++-----
 3 files changed, 437 insertions(+), 382 deletions(-)
 create mode 100644 modules/pulsepointLiteBidAdapter.md

diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js
index c80e45eb2f0..84f955317c0 100644
--- a/modules/pulsepointLiteBidAdapter.js
+++ b/modules/pulsepointLiteBidAdapter.js
@@ -1,9 +1,14 @@
-import {createBid} from 'src/bidfactory';
-import {addBidResponse} from 'src/bidmanager';
+/* eslint dot-notation:0, quote-props:0 */
 import {logError, getTopWindowLocation} from 'src/utils';
-import {ajax} from 'src/ajax';
-import {STATUS} from 'src/constants';
-import adaptermanager from 'src/adaptermanager';
+import { registerBidder } from 'src/adapters/bidderFactory';
+
+const NATIVE_DEFAULTS = {
+  TITLE_LEN: 100,
+  DESCR_LEN: 200,
+  SPONSORED_BY_LEN: 50,
+  IMG_MIN: 150,
+  ICON_MIN: 50,
+};
 
 /**
  * PulsePoint "Lite" Adapter.  This adapter implementation is lighter than the
@@ -11,291 +16,293 @@ import adaptermanager from 'src/adaptermanager';
  * dependencies and relies on a single OpenRTB request to the PulsePoint
  * bidder instead of separate requests per slot.
  */
-function PulsePointLiteAdapter() {
-  const bidUrl = window.location.protocol + '//bid.contextweb.com/header/ortb';
-  const ajaxOptions = {
-    method: 'POST',
-    withCredentials: true,
-    contentType: 'text/plain'
-  };
-  const NATIVE_DEFAULTS = {
-    TITLE_LEN: 100,
-    DESCR_LEN: 200,
-    SPONSORED_BY_LEN: 50,
-    IMG_MIN: 150,
-    ICON_MIN: 50,
-  };
+export const spec = {
 
-  /**
-   * Makes the call to PulsePoint endpoint and registers bids.
-   */
-  function _callBids(bidRequest) {
-    try {
-      // construct the openrtb bid request from slots
-      const request = {
-        imp: bidRequest.bids.map(slot => impression(slot)),
-        site: site(bidRequest),
-        device: device(),
-      };
-      ajax(bidUrl, (rawResponse) => {
-        bidResponseAvailable(bidRequest, rawResponse);
-      }, JSON.stringify(request), ajaxOptions);
-    } catch (e) {
-      // register passback on any exceptions while attempting to fetch response.
-      logError('pulsepoint.requestBid', 'ERROR', e);
-      bidResponseAvailable(bidRequest);
-    }
-  }
+  code: 'pulseLite',
 
-  /**
-   * Callback for bids, after the call to PulsePoint completes.
-   */
-  function bidResponseAvailable(bidRequest, rawResponse) {
-    const idToSlotMap = {};
-    const idToBidMap = {};
-    // extract the request bids and the response bids, keyed by impr-id
-    bidRequest.bids.forEach((slot) => {
-      idToSlotMap[slot.bidId] = slot;
-    });
-    const bidResponse = parse(rawResponse);
-    if (bidResponse) {
-      bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach((bid) => {
-        idToBidMap[bid.impid] = bid;
-      }));
-    }
-    // register the responses
-    Object.keys(idToSlotMap).forEach((id) => {
-      if (idToBidMap[id]) {
-        const size = adSize(idToSlotMap[id]);
-        const bid = createBid(STATUS.GOOD, bidRequest);
-        bid.bidderCode = bidRequest.bidderCode;
-        bid.cpm = idToBidMap[id].price;
-        bid.adId = id;
-        if (isNative(idToSlotMap[id])) {
-          bid['native'] = nativeResponse(idToSlotMap[id], idToBidMap[id]);
-          bid.mediaType = 'native';
-        } else {
-          bid.ad = idToBidMap[id].adm;
-          bid.width = size[0];
-          bid.height = size[1];
-        }
-        addBidResponse(idToSlotMap[id].placementCode, bid);
-      } else {
-        const passback = createBid(STATUS.NO_BID, bidRequest);
-        passback.bidderCode = bidRequest.bidderCode;
-        passback.adId = id;
-        addBidResponse(idToSlotMap[id].placementCode, passback);
-      }
-    });
-  }
+  aliases: ['pulsepointLite'],
 
-  /**
-   * Produces an OpenRTBImpression from a slot config.
-   */
-  function impression(slot) {
+  supportedMediaTypes: ['native'],
+
+  isBidRequestValid: bid => (
+    !!(bid && bid.params && bid.params.cp && bid.params.ct)
+  ),
+
+  buildRequests: bidRequests => {
+    const request = {
+      id: bidRequests[0].bidderRequestId,
+      imp: bidRequests.map(slot => impression(slot)),
+      site: site(bidRequests),
+      app: app(bidRequests),
+      device: device(),
+    };
     return {
-      id: slot.bidId,
-      banner: banner(slot),
-      'native': nativeImpression(slot),
-      tagid: slot.params.ct.toString(),
+      method: 'POST',
+      url: '//bid.contextweb.com/header/ortb',
+      data: JSON.stringify(request),
     };
-  }
+  },
 
-  /**
-   * Produces an OpenRTB Banner object for the slot given.
-   */
-  function banner(slot) {
-    const size = adSize(slot);
-    return slot.nativeParams ? null : {
-      w: size[0],
-      h: size[1],
-    };
-  }
+  interpretResponse: (response, request) => (
+    bidResponseAvailable(request, response)
+  ),
 
-  /**
-   * Produces an OpenRTB Native object for the slot given.
-   */
-  function nativeImpression(slot) {
-    if (slot.nativeParams) {
-      const assets = [];
-      addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN));
-      addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN));
-      addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN));
-      addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN));
-      addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN));
-      return {
-        request: JSON.stringify({ assets }),
-        ver: '1.1',
-      };
+  getUserSyncs: syncOptions => {
+    if (syncOptions.iframeEnabled) {
+      return [{
+        type: 'iframe',
+        url: '//bh.contextweb.com/visitormatch'
+      }];
     }
-    return null;
   }
 
-  /**
-   * Helper method to add an asset to the assets list.
-   */
-  function addAsset(assets, asset) {
-    if (asset) {
-      assets.push(asset);
-    }
-  }
+};
 
-  /**
-   * Produces a Native Title asset for the configuration given.
-   */
-  function titleAsset(id, params, defaultLen) {
-    if (params) {
-      return {
-        id: id,
-        required: params.required ? 1 : 0,
-        title: {
-          len: params.len || defaultLen,
-        },
+/**
+ * Callback for bids, after the call to PulsePoint completes.
+ */
+function bidResponseAvailable(bidRequest, bidResponse) {
+  const idToImpMap = {};
+  const idToBidMap = {};
+  // extract the request bids and the response bids, keyed by impr-id
+  const ortbRequest = parse(bidRequest.data);
+  ortbRequest.imp.forEach(imp => {
+    idToImpMap[imp.id] = imp;
+  });
+  if (bidResponse) {
+    bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => {
+      idToBidMap[bid.impid] = bid;
+    }));
+  }
+  const bids = [];
+  Object.keys(idToImpMap).forEach(id => {
+    if (idToBidMap[id]) {
+      const bid = {
+        requestId: id,
+        cpm: idToBidMap[id].price,
+        creative_id: id,
+        creativeId: id,
+        adId: id,
       };
+      if (idToImpMap[id]['native']) {
+        bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]);
+        bid.mediaType = 'native';
+      } else {
+        bid.ad = idToBidMap[id].adm;
+        bid.width = idToImpMap[id].banner.w;
+        bid.height = idToImpMap[id].banner.h;
+      }
+      bids.push(bid);
     }
-    return null;
+  });
+  return bids;
+}
+
+/**
+ * Produces an OpenRTBImpression from a slot config.
+ */
+function impression(slot) {
+  return {
+    id: slot.bidId,
+    banner: banner(slot),
+    'native': nativeImpression(slot),
+    tagid: slot.params.ct.toString(),
+  };
+}
+
+/**
+ * Produces an OpenRTB Banner object for the slot given.
+ */
+function banner(slot) {
+  const size = adSize(slot);
+  return slot.nativeParams ? null : {
+    w: size[0],
+    h: size[1],
+  };
+}
+
+/**
+ * Produces an OpenRTB Native object for the slot given.
+ */
+function nativeImpression(slot) {
+  if (slot.nativeParams) {
+    const assets = [];
+    addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN));
+    addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN));
+    addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN));
+    addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN));
+    addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN));
+    return {
+      request: JSON.stringify({ assets }),
+      ver: '1.1',
+    };
   }
+  return null;
+}
 
-  /**
-   * Produces a Native Image asset for the configuration given.
-   */
-  function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) {
-    return params ? {
-      id: id,
-      required: params.required ? 1 : 0,
-      img: {
-        type,
-        wmin: params.wmin || defaultMinWidth,
-        hmin: params.hmin || defaultMinHeight,
-      }
-    } : null;
+/**
+ * Helper method to add an asset to the assets list.
+ */
+function addAsset(assets, asset) {
+  if (asset) {
+    assets.push(asset);
   }
+}
 
-  /**
-   * Produces a Native Data asset for the configuration given.
-   */
-  function dataAsset(id, params, type, defaultLen) {
-    return params ? {
-      id: id,
+/**
+ * Produces a Native Title asset for the configuration given.
+ */
+function titleAsset(id, params, defaultLen) {
+  if (params) {
+    return {
+      id,
       required: params.required ? 1 : 0,
-      data: {
-        type,
+      title: {
         len: params.len || defaultLen,
-      }
-    } : null;
+      },
+    };
   }
+  return null;
+}
+
+/**
+ * Produces a Native Image asset for the configuration given.
+ */
+function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) {
+  return params ? {
+    id,
+    required: params.required ? 1 : 0,
+    img: {
+      type,
+      wmin: params.wmin || defaultMinWidth,
+      hmin: params.hmin || defaultMinHeight,
+    }
+  } : null;
+}
+
+/**
+ * Produces a Native Data asset for the configuration given.
+ */
+function dataAsset(id, params, type, defaultLen) {
+  return params ? {
+    id,
+    required: params.required ? 1 : 0,
+    data: {
+      type,
+      len: params.len || defaultLen,
+    }
+  } : null;
+}
 
-  /**
-   * Produces an OpenRTB site object.
-   */
-  function site(bidderRequest) {
-    const pubId = bidderRequest.bids.length > 0 ? bidderRequest.bids[0].params.cp : '0';
+/**
+ * Produces an OpenRTB site object.
+ */
+function site(bidderRequest) {
+  const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0';
+  const appParams = bidderRequest[0].params.app;
+  if (!appParams) {
     return {
       publisher: {
         id: pubId.toString(),
       },
       ref: referrer(),
       page: getTopWindowLocation().href,
-    };
-  }
-
-  /**
-   * Attempts to capture the referrer url.
-   */
-  function referrer() {
-    try {
-      return window.top.document.referrer;
-    } catch (e) {
-      return document.referrer;
     }
   }
+  return null;
+}
 
-  /**
-   * Produces an OpenRTB Device object.
-   */
-  function device() {
+/**
+ * Produces an OpenRTB App object.
+ */
+function app(bidderRequest) {
+  const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0';
+  const appParams = bidderRequest[0].params.app;
+  if (appParams) {
     return {
-      ua: navigator.userAgent,
-      language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage),
-    };
-  }
-
-  /**
-   * Safely parses the input given. Returns null on
-   * parsing failure.
-   */
-  function parse(rawResponse) {
-    try {
-      if (rawResponse) {
-        return JSON.parse(rawResponse);
-      }
-    } catch (ex) {
-      logError('pulsepointLite.safeParse', 'ERROR', ex);
+      publisher: {
+        id: pubId.toString(),
+      },
+      bundle: appParams.bundle,
+      storeurl: appParams.storeUrl,
+      domain: appParams.domain,
     }
-    return null;
   }
+  return null;
+}
 
-  /**
-   * Determines the AdSize for the slot.
-   */
-  function adSize(slot) {
-    if (slot.params.cf) {
-      const size = slot.params.cf.toUpperCase().split('X');
-      const width = parseInt(slot.params.cw || size[0], 10);
-      const height = parseInt(slot.params.ch || size[1], 10);
-      return [width, height];
-    }
-    return [1, 1];
+/**
+ * Attempts to capture the referrer url.
+ */
+function referrer() {
+  try {
+    return window.top.document.referrer;
+  } catch (e) {
+    return document.referrer;
   }
+}
 
-  /**
-   * Parses the native response from the Bid given.
-   */
-  function nativeResponse(slot, bid) {
-    if (slot.nativeParams) {
-      const nativeAd = parse(bid.adm);
-      const keys = {};
-      if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) {
-        nativeAd['native'].assets.forEach((asset) => {
-          keys.title = asset.title ? asset.title.text : keys.title;
-          keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body;
-          keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy;
-          keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image;
-          keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon;
-        });
-        if (nativeAd['native'].link) {
-          keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url);
-        }
-        keys.impressionTrackers = nativeAd['native'].imptrackers;
-        return keys;
-      }
+/**
+ * Produces an OpenRTB Device object.
+ */
+function device() {
+  return {
+    ua: navigator.userAgent,
+    language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage),
+  };
+}
+
+/**
+ * Safely parses the input given. Returns null on
+ * parsing failure.
+ */
+function parse(rawResponse) {
+  try {
+    if (rawResponse) {
+      return JSON.parse(rawResponse);
     }
-    return null;
+  } catch (ex) {
+    logError('pulsepointLite.safeParse', 'ERROR', ex);
   }
+  return null;
+}
 
-  /**
-   * Parses the native response from the Bid given.
-   */
-  function isNative(slot) {
-    return !!slot.nativeParams;
+/**
+ * Determines the AdSize for the slot.
+ */
+function adSize(slot) {
+  if (slot.params.cf) {
+    const size = slot.params.cf.toUpperCase().split('X');
+    const width = parseInt(slot.params.cw || size[0], 10);
+    const height = parseInt(slot.params.ch || size[1], 10);
+    return [width, height];
   }
-
-  return Object.assign(this, {
-    callBids: _callBids
-  });
+  return [1, 1];
 }
 
 /**
- * "pulseLite" will be the adapter name going forward. "pulsepointLite" to be
- * deprecated, but kept here for backwards compatibility.
- * Reason is key truncation. When the Publisher opts for sending all bids to DFP, then
- * the keys get truncated due to the limit in key-size (20 characters, detailed
- * here https://support.google.com/dfp_premium/answer/1628457?hl=en). Here is an
- * example, where keys got truncated when using the "pulsepointLite" alias - "hb_adid_pulsepointLi=1300bd87d59c4c2"
-*/
-adaptermanager.registerBidAdapter(new PulsePointLiteAdapter(), 'pulseLite', {
-  supportedMediaTypes: [ 'native' ]
-});
-adaptermanager.aliasBidAdapter('pulseLite', 'pulsepointLite');
+ * Parses the native response from the Bid given.
+ */
+function nativeResponse(imp, bid) {
+  if (imp['native']) {
+    const nativeAd = parse(bid.adm);
+    const keys = {};
+    if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) {
+      nativeAd['native'].assets.forEach(asset => {
+        keys.title = asset.title ? asset.title.text : keys.title;
+        keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body;
+        keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy;
+        keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image;
+        keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon;
+      });
+      if (nativeAd['native'].link) {
+        keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url);
+      }
+      keys.impressionTrackers = nativeAd['native'].imptrackers;
+      return keys;
+    }
+  }
+  return null;
+}
 
-module.exports = PulsePointLiteAdapter;
+registerBidder(spec);
diff --git a/modules/pulsepointLiteBidAdapter.md b/modules/pulsepointLiteBidAdapter.md
new file mode 100644
index 00000000000..23c96758ca0
--- /dev/null
+++ b/modules/pulsepointLiteBidAdapter.md
@@ -0,0 +1,43 @@
+# Overview
+
+**Module Name**: PulsePoint Lite Bidder Adapter  
+**Module Type**: Bidder Adapter  
+**Maintainer**: ExchangeTeam@pulsepoint.com  
+
+# Description
+
+Connects to PulsePoint demand source to fetch bids.  
+Banner, Outstream and Native formats are supported.  
+Please use ```pulseLite``` as the bidder code.  
+
+# Test Parameters
+```
+    var adUnits = [{
+      code: 'banner-ad-div',
+      sizes: [[300, 250]],
+      bids: [{
+          bidder: 'pulsepointLite',
+          params: { 
+              cf: '300X250',
+              cp: 512379,
+              ct: 486653
+          }
+      }]
+    },{
+      code: 'native-ad-div',
+      sizes: [[1, 1]],
+      nativeParams: {
+          title: { required: true, len: 75  },
+          image: { required: true  },
+          body: { len: 200  },
+          sponsoredBy: { len: 20 }
+      },
+      bids: [{
+          bidder: 'pulseLite',
+          params: { 
+              cp: 512379,
+              ct: 505642
+          }
+      }]
+    }];
+```
diff --git a/test/spec/modules/pulsepointLiteBidAdapter_spec.js b/test/spec/modules/pulsepointLiteBidAdapter_spec.js
index f7b7a790302..8e1f12dac93 100644
--- a/test/spec/modules/pulsepointLiteBidAdapter_spec.js
+++ b/test/spec/modules/pulsepointLiteBidAdapter_spec.js
@@ -1,71 +1,60 @@
+/* eslint dot-notation:0, quote-props:0 */
 import {expect} from 'chai';
-import PulsePointAdapter from 'modules/pulsepointLiteBidAdapter';
+import {spec} from 'modules/pulsepointLiteBidAdapter';
 import bidManager from 'src/bidmanager';
 import {getTopWindowLocation} from 'src/utils';
-import * as ajax from 'src/ajax';
+import {newBidder} from 'src/adapters/bidderFactory';
 
 describe('PulsePoint Lite Adapter Tests', () => {
-  let pulsepointAdapter = new PulsePointAdapter();
-  let slotConfigs;
-  let nativeSlotConfig;
-  let ajaxStub;
-
-  beforeEach(() => {
-    sinon.stub(bidManager, 'addBidResponse');
-    ajaxStub = sinon.stub(ajax, 'ajax');
-
-    slotConfigs = {
-      bidderCode: 'pulseLite',
-      bids: [
-        {
-          placementCode: '/DfpAccount1/slot1',
-          bidId: 'bid12345',
-          params: {
-            cp: 'p10000',
-            ct: 't10000',
-            cf: '300x250'
-          }
-        }, {
-          placementCode: '/DfpAccount2/slot2',
-          bidId: 'bid23456',
-          params: {
-            cp: 'p10000',
-            ct: 't20000',
-            cf: '728x90'
-          }
-        }
-      ]
-    };
-    nativeSlotConfig = {
-      bidderCode: 'pulseLite',
-      bids: [
-        {
-          placementCode: '/DfpAccount1/slot3',
-          bidId: 'bid12345',
-          nativeParams: {
-            title: { required: true, len: 200 },
-            image: { wmin: 100 },
-            sponsoredBy: { }
-          },
-          params: {
-            cp: 'p10000',
-            ct: 't10000'
-          }
-        }
-      ]
-    };
-  });
-
-  afterEach(() => {
-    bidManager.addBidResponse.restore();
-    ajaxStub.restore();
-  });
+  const slotConfigs = [{
+    placementCode: '/DfpAccount1/slot1',
+    bidId: 'bid12345',
+    params: {
+      cp: 'p10000',
+      ct: 't10000',
+      cf: '300x250'
+    }
+  }, {
+    placementCode: '/DfpAccount2/slot2',
+    bidId: 'bid23456',
+    params: {
+      cp: 'p10000',
+      ct: 't20000',
+      cf: '728x90'
+    }
+  }];
+  const nativeSlotConfig = [{
+    placementCode: '/DfpAccount1/slot3',
+    bidId: 'bid12345',
+    nativeParams: {
+      title: { required: true, len: 200 },
+      image: { wmin: 100 },
+      sponsoredBy: { }
+    },
+    params: {
+      cp: 'p10000',
+      ct: 't10000'
+    }
+  }];
+  const appSlotConfig = [{
+    placementCode: '/DfpAccount1/slot3',
+    bidId: 'bid12345',
+    params: {
+      cp: 'p10000',
+      ct: 't10000',
+      app: {
+        bundle: 'com.pulsepoint.apps',
+        storeUrl: 'http://pulsepoint.com/apps',
+        domain: 'pulsepoint.com',
+      }
+    }
+  }];
 
-  it('Verify requests sent to PulsePoint', () => {
-    pulsepointAdapter.callBids(slotConfigs);
-    expect(ajaxStub.callCount).to.equal(1);
-    expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb');
-    const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]);
+  it('Verify build request', () => {
+    const request = spec.buildRequests(slotConfigs);
+    expect(request.url).to.equal('//bid.contextweb.com/header/ortb');
+    expect(request.method).to.equal('POST');
+    const ortbRequest = JSON.parse(request.data);
     // site object
     expect(ortbRequest.site).to.not.equal(null);
     expect(ortbRequest.site.publisher).to.not.equal(null);
@@ -88,11 +77,10 @@ describe('PulsePoint Lite Adapter Tests', () => {
     expect(ortbRequest.imp[1].banner.h).to.equal(90);
   });
 
-  it('Verify bid', () => {
-    pulsepointAdapter.callBids(slotConfigs);
-    // trigger a mock ajax callback with bid.
-    const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]);
-    ajaxStub.firstCall.args[1](JSON.stringify({
+  it('Verify parse response', () => {
+    const request = spec.buildRequests(slotConfigs);
+    const ortbRequest = JSON.parse(request.data);
+    const ortbResponse = {
       seatbid: [{
         bid: [{
           impid: ortbRequest.imp[0].id,
@@ -100,65 +88,40 @@ describe('PulsePoint Lite Adapter Tests', () => {
           adm: 'This is an Ad'
         }]
       }]
-    }));
-    expect(bidManager.addBidResponse.callCount).to.equal(2);
+    };
+    const bids = spec.interpretResponse(ortbResponse, request);
+    expect(bids).to.have.lengthOf(1);
     // verify first bid
-    let placement = bidManager.addBidResponse.firstCall.args[0];
-    let bid = bidManager.addBidResponse.firstCall.args[1];
-    expect(placement).to.equal('/DfpAccount1/slot1');
-    expect(bid.bidderCode).to.equal('pulseLite');
+    const bid = bids[0];
     expect(bid.cpm).to.equal(1.25);
     expect(bid.ad).to.equal('This is an Ad');
     expect(bid.width).to.equal(300);
     expect(bid.height).to.equal(250);
     expect(bid.adId).to.equal('bid12345');
-    // verify passback on 2nd impression.
-    placement = bidManager.addBidResponse.secondCall.args[0];
-    bid = bidManager.addBidResponse.secondCall.args[1];
-    expect(placement).to.equal('/DfpAccount2/slot2');
-    expect(bid.adId).to.equal('bid23456');
-    expect(bid.bidderCode).to.equal('pulseLite');
-    expect(bid.cpm).to.be.undefined;
+    expect(bid.creative_id).to.equal('bid12345');
+    expect(bid.creativeId).to.equal('bid12345');
   });
 
   it('Verify full passback', () => {
-    pulsepointAdapter.callBids(slotConfigs);
-    // trigger a mock ajax callback with no bid.
-    ajaxStub.firstCall.args[1](null);
-    let placement = bidManager.addBidResponse.firstCall.args[0];
-    let bid = bidManager.addBidResponse.firstCall.args[1];
-    expect(placement).to.equal('/DfpAccount1/slot1');
-    expect(bid.bidderCode).to.equal('pulseLite');
-    expect(bid).to.not.have.property('ad');
-    expect(bid).to.not.have.property('cpm');
-    expect(bid.adId).to.equal('bid12345');
-  });
-
-  it('Verify passback when ajax call fails', () => {
-    ajaxStub.throws();
-    pulsepointAdapter.callBids(slotConfigs);
-    let placement = bidManager.addBidResponse.firstCall.args[0];
-    let bid = bidManager.addBidResponse.firstCall.args[1];
-    expect(placement).to.equal('/DfpAccount1/slot1');
-    expect(bid.bidderCode).to.equal('pulseLite');
-    expect(bid).to.not.have.property('ad');
-    expect(bid).to.not.have.property('cpm');
-    expect(bid.adId).to.equal('bid12345');
+    const request = spec.buildRequests(slotConfigs);
+    const bids = spec.interpretResponse(null, request)
+    expect(bids).to.have.lengthOf(0);
   });
 
   it('Verify Native request', () => {
-    pulsepointAdapter.callBids(nativeSlotConfig);
-    expect(ajaxStub.callCount).to.equal(1);
-    expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb');
-    const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]);
+    const request = spec.buildRequests(nativeSlotConfig);
+    expect(request.url).to.equal('//bid.contextweb.com/header/ortb');
+    expect(request.method).to.equal('POST');
+    const ortbRequest = JSON.parse(request.data);
     // native impression
     expect(ortbRequest.imp[0].tagid).to.equal('t10000');
     expect(ortbRequest.imp[0].banner).to.equal(null);
-    expect(ortbRequest.imp[0].native).to.not.equal(null);
-    expect(ortbRequest.imp[0].native.ver).to.equal('1.1');
-    expect(ortbRequest.imp[0].native.request).to.not.equal(null);
+    const nativePart = ortbRequest.imp[0]['native'];
+    expect(nativePart).to.not.equal(null);
+    expect(nativePart.ver).to.equal('1.1');
+    expect(nativePart.request).to.not.equal(null);
     // native request assets
-    const nativeRequest = JSON.parse(ortbRequest.imp[0].native.request);
+    const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request);
     expect(nativeRequest).to.not.equal(null);
     expect(nativeRequest.assets).to.have.lengthOf(3);
     // title asset
@@ -184,22 +147,22 @@ describe('PulsePoint Lite Adapter Tests', () => {
   });
 
   it('Verify Native response', () => {
-    pulsepointAdapter.callBids(nativeSlotConfig);
-    expect(ajaxStub.callCount).to.equal(1);
-    expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb');
-    const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]);
+    const request = spec.buildRequests(nativeSlotConfig);
+    expect(request.url).to.equal('//bid.contextweb.com/header/ortb');
+    expect(request.method).to.equal('POST');
+    const ortbRequest = JSON.parse(request.data);
     const nativeResponse = {
-      native: {
+      'native': {
         assets: [
           { title: { text: 'Ad Title'} },
           { data: { type: 1, value: 'Sponsored By: Brand' }},
           { img: { type: 3, url: 'http://images.cdn.brand.com/123' } }
         ],
         link: { url: 'http://brand.clickme.com/' },
-        imptrackers: [ 'http://imp1.trackme.com/', 'http://imp1.contextweb.com/' ]
+        imptrackers: ['http://imp1.trackme.com/', 'http://imp1.contextweb.com/']
       }
     };
-    ajaxStub.firstCall.args[1](JSON.stringify({
+    const ortbResponse = {
       seatbid: [{
         bid: [{
           impid: ortbRequest.imp[0].id,
@@ -207,28 +170,70 @@ describe('PulsePoint Lite Adapter Tests', () => {
           adm: JSON.stringify(nativeResponse)
         }]
       }]
-    }));
+    };
+    const bids = spec.interpretResponse(ortbResponse, request);
     // verify bid
-    let placement = bidManager.addBidResponse.firstCall.args[0];
-    let bid = bidManager.addBidResponse.firstCall.args[1];
-    expect(placement).to.equal('/DfpAccount1/slot3');
-    expect(bid.bidderCode).to.equal('pulseLite');
+    const bid = bids[0];
     expect(bid.cpm).to.equal(1.25);
     expect(bid.adId).to.equal('bid12345');
     expect(bid.ad).to.be.undefined;
     expect(bid.mediaType).to.equal('native');
-    expect(bid.native).to.not.equal(null);
-    expect(bid.native.title).to.equal('Ad Title');
-    expect(bid.native.sponsoredBy).to.equal('Sponsored By: Brand');
-    expect(bid.native.image).to.equal('http://images.cdn.brand.com/123');
-    expect(bid.native.clickUrl).to.equal(encodeURIComponent('http://brand.clickme.com/'));
-    expect(bid.native.impressionTrackers).to.have.lengthOf(2);
-    expect(bid.native.impressionTrackers[0]).to.equal('http://imp1.trackme.com/');
-    expect(bid.native.impressionTrackers[1]).to.equal('http://imp1.contextweb.com/');
+    const nativeBid = bid['native'];
+    expect(nativeBid).to.not.equal(null);
+    expect(nativeBid.title).to.equal('Ad Title');
+    expect(nativeBid.sponsoredBy).to.equal('Sponsored By: Brand');
+    expect(nativeBid.image).to.equal('http://images.cdn.brand.com/123');
+    expect(nativeBid.clickUrl).to.equal(encodeURIComponent('http://brand.clickme.com/'));
+    expect(nativeBid.impressionTrackers).to.have.lengthOf(2);
+    expect(nativeBid.impressionTrackers[0]).to.equal('http://imp1.trackme.com/');
+    expect(nativeBid.impressionTrackers[1]).to.equal('http://imp1.contextweb.com/');
+  });
+
+  it('Verifies bidder code', () => {
+    expect(spec.code).to.equal('pulseLite');
+  });
+
+  it('Verifies bidder aliases', () => {
+    expect(spec.aliases).to.have.lengthOf(1);
+    expect(spec.aliases[0]).to.equal('pulsepointLite');
+  });
+
+  it('Verifies supported media types', () => {
+    expect(spec.supportedMediaTypes).to.have.lengthOf(1);
+    expect(spec.supportedMediaTypes[0]).to.equal('native');
+  });
+
+  it('Verifies if bid request valid', () => {
+    expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true);
+    expect(spec.isBidRequestValid(slotConfigs[1])).to.equal(true);
+    expect(spec.isBidRequestValid(nativeSlotConfig[0])).to.equal(true);
+    expect(spec.isBidRequestValid({})).to.equal(false);
+    expect(spec.isBidRequestValid({ params: {} })).to.equal(false);
+    expect(spec.isBidRequestValid({ params: { ct: 123 } })).to.equal(false);
+    expect(spec.isBidRequestValid({ params: { cp: 123 } })).to.equal(false);
+    expect(spec.isBidRequestValid({ params: { ct: 123, cp: 234 }})).to.equal(true);
   });
 
-  it('Verify adapter interface', function () {
-    const adapter = new PulsePointAdapter();
-    expect(adapter).to.have.property('callBids');
+  it('Verifies sync options', () => {
+    expect(spec.getUserSyncs({})).to.be.undefined;
+    expect(spec.getUserSyncs({ iframeEnabled: false})).to.be.undefined;
+    const options = spec.getUserSyncs({ iframeEnabled: true});
+    expect(options).to.not.be.undefined;
+    expect(options).to.have.lengthOf(1);
+    expect(options[0].type).to.equal('iframe');
+    expect(options[0].url).to.equal('//bh.contextweb.com/visitormatch');
+  });
+
+  it('Verify app requests', () => {
+    const request = spec.buildRequests(appSlotConfig);
+    const ortbRequest = JSON.parse(request.data);
+    // site object
+    expect(ortbRequest.site).to.equal(null);
+    expect(ortbRequest.app).to.not.be.null;
+    expect(ortbRequest.app.publisher).to.not.equal(null);
+    expect(ortbRequest.app.publisher.id).to.equal('p10000');
+    expect(ortbRequest.app.bundle).to.equal('com.pulsepoint.apps');
+    expect(ortbRequest.app.storeurl).to.equal('http://pulsepoint.com/apps');
+    expect(ortbRequest.app.domain).to.equal('pulsepoint.com');
   });
 });