import { expectedDeliveryMonitoringSteps, expectKafkaDeliveryMonitoringMessage, getKafkaChoicesMessage, } from "../../mocks/kafka.mock"; import { Campaign, HeaderBiddingType } from "../../../src/models"; import { FakeRequestContractBuilder, getCampaignCandidate, hasCampaignWithIndex, hasLength, } from "../utils/helper"; import { IntegrationTestContext } from "../utils/integration-test-context"; import { RequestContract } from "../../../src/data-contract/request.contract"; import { expect } from "chai"; import sinon from "sinon"; import * as helper from "../../../src/utils/helper"; import { FeatureConfigService } from "../../../src/services/FeatureConfig.service"; import { MockedFeatureConfig } from "@ogury/feature-config-sdk"; const HEADER_BIDDING_PATH = "/header-bidding"; let campaigns: Campaign[]; let campaignsMultiCreatives: Campaign[]; let managedCampaigns: Campaign[]; let programmaticCampaigns: Campaign[]; let featureConfigService: FeatureConfigService; let mockedFeatureConf: MockedFeatureConfig; describe("Header bidding - integration", () => { let testContext: IntegrationTestContext; before(async () => { testContext = new IntegrationTestContext( HEADER_BIDDING_PATH, "test/integration/data/campaigns", "test/integration/data/s3", { "3": 10.1 }, // domain courbis.fr -> hash: 0x104AF593AB1AAA3C ["104AF593AB1AAA3C"], "8", "0" ); await testContext.init(); featureConfigService = FeatureConfigService.getInstance(); mockedFeatureConf = new MockedFeatureConfig(); }); beforeEach(() => { campaigns = [ getCampaignCandidate("1"), getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("5"), getCampaignCandidate("6"), getCampaignCandidate("7"), getCampaignCandidate("8"), getCampaignCandidate("9"), getCampaignCandidate("10"), ]; campaignsMultiCreatives = [ getCampaignCandidate("1"), getCampaignCandidate("2"), getCampaignCandidate("3", [31]), getCampaignCandidate("4", [41]), getCampaignCandidate("5", [51]), getCampaignCandidate("6", [61, 62]), getCampaignCandidate("7", [71, 72]), getCampaignCandidate("8", [81, 82]), getCampaignCandidate("9", [91, 92, 93]), getCampaignCandidate("10", [101, 102, 103]), ]; managedCampaigns = [ getCampaignCandidate("1"), getCampaignCandidate("3"), getCampaignCandidate("5"), getCampaignCandidate("7"), getCampaignCandidate("9"), ]; programmaticCampaigns = [ getCampaignCandidate("2"), getCampaignCandidate("4"), getCampaignCandidate("6"), getCampaignCandidate("8"), getCampaignCandidate("10"), ]; }); afterEach(() => { mockedFeatureConf.clearParams(); testContext.afterEachRestore(); }); after(() => { testContext.afterRestore(); }); describe("Request validation", () => { beforeEach(() => { mockedFeatureConf.setParam("ms-choices.validation-mode", "enabled"); featureConfigService.setFeatureConfig(mockedFeatureConf); }); after(() => { mockedFeatureConf.clearParams(); }); it("not valid: no campaign provided", async () => { const body: RequestContract = new FakeRequestContractBuilder().withCampaigns().build(); const res = await testContext.mockHttp(body).expect(400); expect(res.body.error).to.eql('"campaigns" must contain at least 1 items'); }); it("not valid: invalid bidfloor", async () => { const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withBidfloor(-1) .build(); const res = await testContext.mockHttp(body).expect(400); expect(res.body.error).to.eql('"bidFloor" must be greater than or equal to 0'); }); it("not valid: invalid size", async () => { let body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withSize(0) .build(); let res = await testContext.mockHttp(body).expect(400); expect(res.body.error).to.eql('"size" must be greater than or equal to 1'); body = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withSize(-1) .build(); res = await testContext.mockHttp(body).expect(400); expect(res.body.error).to.eql('"size" must be greater than or equal to 1'); }); it("not valid: invalid IP", async () => { let body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withIP("12.3") .build(); let res = await testContext.mockHttp(body).expect(400); expect(res.body.error).to.eql( '"device.ip" must be a valid ip address of one of the following versions [ipv4] with a optional CIDR' ); body = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withIP("") .build(); res = await testContext.mockHttp(body).expect(400); expect(res.body.error).to.eql('"device.ip" is not allowed to be empty'); }); }); describe("Both managed and programmatic campaigns are candidates", () => { it("all campaign are stopped because request is fraud", async () => { const body: RequestContract = new FakeRequestContractBuilder() .withAppBundleOrDomain("courbis.fr") .withCampaigns(...campaigns) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [], }); }); it("respond only managed (prog IVT)", async () => { // DV step excludes campaign 1 (request is not DV strict, but campaign requires it to be) // Bidfloor step excludes campaign 2 (bidprice is under bidfloor = $10) // speed_threshold step excludes campaign 3 // 10,8,6,4 prog campaigns campaigns are excluded because of prog IVT step // => result: sorted campaigns 5, 7, 9 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withUserAgent("") .withBidfloor(10) .withSize(6) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 3); hasCampaignWithIndex(res.body, "5", 0); hasCampaignWithIndex(res.body, "7", 1); hasCampaignWithIndex(res.body, "9", 2); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [2, 3, 4, 5, 6, 7, 8, 9, 10], device_fraud_ias: [2, 3, 4, 5, 6, 7, 8, 9, 10], publisher_fraud_ias: [2, 3, 4, 5, 6, 7, 8, 9, 10], brand_safety_dv: [2, 3, 4, 5, 6, 7, 8, 9, 10], brand_safety_ias: [2, 3, 4, 5, 6, 7, 8, 9, 10], bidfloor: [3, 4, 5, 6, 7, 8, 9, 10], speed_threshold: [4, 5, 6, 7, 8, 9, 10], programmatic_ivt: [5, 7, 9], }); }); it("respond managed + programmatic campaigns", async () => { // Bidfloor step excludes campaign 1 and 2 (bidprice is under bidfloor = $10) // 6,4 are excluded because of programmatic_response step (no response from auction) // 9 is excluded because of campaign_size step (4) // => result: sorted campaigns 8, 5, 7, 3 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...campaigns) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withBidfloor(10) .withSize(3) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 3); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { device_fraud_ias: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], publisher_fraud_ias: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], bidfloor: [3, 4, 5, 6, 7, 8, 9, 10], speed_threshold: [4, 5, 6, 7, 8, 9, 10], programmatic_response: [5, 7, 8, 9], campaign_size: [5, 7, 8], }); }); it("respond managed + programmatic campaigns (multi creatives)", async () => { // Bidfloor step excludes campaign 1 and 2 (bidprice is under bidfloor = $10) // 6,4 are excluded because of programmatic_response step (no response from auction) // 9 is excluded because of campaign_size step (4) // => result: sorted campaigns 8, 5, 7, 3 // first creative is selected sinon.stub(helper, "getRandomItem").callsFake((array) => array[0]); const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...campaignsMultiCreatives) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withBidfloor(10) .withSize(3) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 3); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); expectKafkaDeliveryMonitoringMessage(testContext.getKafkaSpy(), "header-bidding-1.json"); // check kafka choices message const report = getKafkaChoicesMessage(testContext.getKafkaSpy()); const campaign1 = report.campaigns.find((c) => c.campaign_id === "1"); const campaign2 = report.campaigns.find((c) => c.campaign_id === "2"); const campaign3 = report.campaigns.find((c) => c.campaign_id === "3"); const campaign4 = report.campaigns.find((c) => c.campaign_id === "4"); const campaign5 = report.campaigns.find((c) => c.campaign_id === "5"); const campaign6 = report.campaigns.find((c) => c.campaign_id === "6"); const campaign7 = report.campaigns.find((c) => c.campaign_id === "7"); const campaign8 = report.campaigns.find((c) => c.campaign_id === "8"); const campaign9 = report.campaigns.find((c) => c.campaign_id === "9"); const campaign10 = report.campaigns.find((c) => c.campaign_id === "10"); expect(campaign1!.creative_id).to.be.undefined; expect(campaign2!.creative_id).to.be.undefined; expect(campaign3!.creative_id).to.be.equal(31); expect(campaign4!.creative_id).to.be.equal(41); expect(campaign5!.creative_id).to.be.equal(51); expect(campaign6!.creative_id).to.be.equal(61); expect(campaign7!.creative_id).to.be.equal(71); expect(campaign8!.creative_id).to.be.equal(81); expect(campaign9!.creative_id).to.be.equal(91); expect(campaign10!.creative_id).to.be.equal(101); }); // it("respond managed + programmatic campaigns - web", async () => { // // Bidfloor step excludes campaign 1 and 2 (bidprice is under bidfloor = $10) // // // 6,4 are excluded because of programmatic_response step (no response from auction) // // 9 is excluded because of campaign_size step (4) // // // => result: sorted campaigns 8, 5, 7, 3 // mockedFeatureConf.setParam("ms-choices.brand-safety.allow-unknown", "true"); // featureConfigService.setFeatureConfig(mockedFeatureConf); // const body: RequestContract = new FakeRequestContractBuilder() // .withCampaigns(...campaigns) // .withSourceType("WEBMOBILE") // .withSite({ // name: "appName", // domain: "a.valid.bundle", // page: "https://a.valid.bundle", // }) // .withUserAgent("aUserAgent") // .withIP("1.1.1.1") // .withBidfloor(10) // .withSize(3) // .build(); // // const res = await testContext.mockHttp(body).expect(200); // await helper.waitImmediate(); // hasLength(res.body, 3); // hasCampaignWithIndex(res.body, "8", 0); // hasCampaignWithIndex(res.body, "5", 1); // hasCampaignWithIndex(res.body, "7", 2); // // // check kafka // expectKafkaDeliveryMessages( // testContext.getKafkaSpy(), // "dv_fraud", // ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], // [] // ); // expectKafkaDeliveryMessages( // testContext.getKafkaSpy(), // "bidfloor", // ["3", "4", "5", "6", "7", "8", "9", "10"], // ["1", "2"] // ); // expectKafkaDeliveryMessages( // testContext.getKafkaSpy(), // "speed_threshold", // ["4", "5", "6", "7", "8", "9", "10"], // ["3"] // ); // expectKafkaDeliveryMessages( // testContext.getKafkaSpy(), // "programmatic_response", // ["5", "7", "8", "9"], // ["4", "6", "10"] // ); // expectKafkaDeliveryMessages( // testContext.getKafkaSpy(), // "campaign_size", // ["5", "7", "8"], // ["9"] // ); // }); // // it("marketplace campaigns are dropped", async () => { // const body: RequestContract = new FakeRequestContractBuilder() // .withCampaigns(getCampaignCandidate("20"), getCampaignCandidate("21")) // .withUserAgent("") // .withBidfloor(10) // .withSize(6) // .build(); // // const res = await testContext.mockHttp(body).expect(200); // await helper.waitImmediate(); // hasLength(res.body, 0); // // // check kafka // expectKafkaDeliveryMessages( // testContext.getKafkaSpy(), // "allowed_campaign_groups", // [], // ["20", "21"] // ); // }); }); describe("Only managed campaigns are candidates", () => { it("all campaign are stopped because request if fraud", async () => { const body: RequestContract = new FakeRequestContractBuilder() .withAppBundleOrDomain("courbis.fr") .withCampaigns(...managedCampaigns) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [], }); }); it("respond managed campaigns", async () => { // Bidfloor step excludes campaign 1 (bidprice is under bidfloor = $10) // speed_threshold step excludes campaign 3 // 9 is excluded because of campaign_size step (3) // => result: sorted campaigns 5, 7 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...managedCampaigns) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withBidfloor(10) .withSize(2) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 2); hasCampaignWithIndex(res.body, "5", 0); hasCampaignWithIndex(res.body, "7", 1); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [1, 3, 5, 7, 9], device_fraud_ias: [1, 3, 5, 7, 9], publisher_fraud_ias: [1, 3, 5, 7, 9], bidfloor: [3, 5, 7, 9], speed_threshold: [5, 7, 9], programmatic_response: [5, 7, 9], campaign_size: [5, 7], }); }); it("respond managed campaigns with third party fee", async () => { // Bidfloor step excludes campaign 1 (bidprice is under bidfloor = $4 with third party fee) // speed_threshold step excludes campaign 3 // 7, 9 is excluded because of campaign_size step (1) // => result: sorted campaigns 5 mockedFeatureConf.setParam( "ms-choices.header-bidding.third-party-extra-fee", `{"Amazon TAM":0.00001}` ); featureConfigService.setFeatureConfig(mockedFeatureConf); const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...managedCampaigns) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withBidfloor(4) .withHeaderBiddingType("TAM") .withSize(1) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 1); hasCampaignWithIndex(res.body, "5", 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [1, 3, 5, 7, 9], device_fraud_ias: [1, 3, 5, 7, 9], publisher_fraud_ias: [1, 3, 5, 7, 9], bidfloor: [3, 5, 7, 9], speed_threshold: [5, 7, 9], programmatic_response: [5, 7, 9], campaign_size: [5], }); mockedFeatureConf.clearParams(); }); }); describe("Only programmatic campaigns are candidates", () => { it("all campaign are stopped because request if fraud", async () => { const body: RequestContract = new FakeRequestContractBuilder() .withAppBundleOrDomain("courbis.fr") .withCampaigns(...programmaticCampaigns) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [], }); }); it("all campaign are stopped because request if programmatic IVT", async () => { // Bidfloor step excludes campaign 2 (bidprice is under bidfloor = $10) // speed_threshold step excludes campaign 3 // 10,8,6,4 prog campaigns campaigns are excluded because of prog IVT step const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...programmaticCampaigns) .withUserAgent("") .withBidfloor(10) .withSize(6) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [2, 4, 6, 8, 10], device_fraud_ias: [2, 4, 6, 8, 10], publisher_fraud_ias: [2, 4, 6, 8, 10], bidfloor: [4, 6, 8, 10], programmatic_ivt: [], }); }); it("respond one programmatic campaign", async () => { // Bidfloor step excludes campaign 2 (bidprice is under bidfloor = $10) // 6,4 are excluded because of programmatic_response step (no response from auction) // => result: campaign 8 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns(...programmaticCampaigns) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withBidfloor(10) .withSize(3) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 1); hasCampaignWithIndex(res.body, "8", 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [2, 4, 6, 8, 10], device_fraud: [2, 4, 6, 8, 10], publisher_fraud: [2, 4, 6, 8, 10], bidfloor: [4, 6, 8, 10], programmatic_response: [8], }); }); }); });