import { expectedDeliveryMonitoringSteps, expectKafkaDeliveryMonitoringMessage, getKafkaChoicesMessage, } from "../../mocks/kafka.mock"; import { FakeRequestContractBuilder, getCampaignCandidate, hasCampaignWithIndex, hasLength, } from "../utils/helper"; import { IntegrationTestContext } from "../utils/integration-test-context"; import { Campaign, RequestType } from "../../../src/models"; import { expect } from "chai"; import sinon from "sinon"; import { RequestContract } from "../../../src/data-contract/request.contract"; import * as helper from "../../../src/utils/helper"; import { MockedFeatureConfig } from "@ogury/feature-config-sdk"; import { FeatureConfigService } from "../../../src/services/FeatureConfig.service"; const INTERNAL_PATH = "/bestCampaigns"; let campaigns: Campaign[]; let campaignsWithMarketplace: Campaign[]; let campaignsMultiCreatives: Campaign[]; let managedCampaigns: Campaign[]; let programmaticCampaigns: Campaign[]; let featureConfigService: FeatureConfigService; let mockedFeatureConf: MockedFeatureConfig; describe("Internal - integration", () => { let testContext: IntegrationTestContext; before(async () => { testContext = new IntegrationTestContext( INTERNAL_PATH, "test/integration/data/campaigns", "test/integration/data/s3", { "3": 10.1 }, // domain courbis.fr -> hash: 0x104AF593AB1AAA3C ["104AF593AB1AAA3C"], "8", "21" ); 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"), ]; campaignsWithMarketplace = [ getCampaignCandidate("1"), getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("5"), getCampaignCandidate("6"), getCampaignCandidate("7"), getCampaignCandidate("8"), getCampaignCandidate("9"), getCampaignCandidate("10"), getCampaignCandidate("20"), getCampaignCandidate("21"), ]; 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 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(...campaignsWithMarketplace) .withUserAgent("") .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { allowed_campaign_groups: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 21], 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) // speed_threshold step excludes campaign 3 // 2,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(), { allowed_campaign_groups: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dv_fraud: [2, 3, 4, 5, 6, 7, 8, 9, 10], device_fraud: [2, 3, 4, 5, 6, 7, 8, 9, 10], publisher_fraud: [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], brand_suitability_ias: [2, 3, 4, 5, 6, 7, 8, 9, 10], yield: [2, 3, 4, 5, 6, 7, 8, 9, 10], ordering: [2, 3, 4, 5, 6, 7, 8, 9, 10], speed_threshold: [2, 4, 5, 6, 7, 8, 9, 10], programmatic_ivt: [5, 7, 9], }); }); it("respond managed + programmatic campaigns", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // 8,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") .withSize(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 4); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); hasCampaignWithIndex(res.body, "1", 3); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 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], brand_safety_ias: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], brand_safety_dv: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], brand_suitability_ias: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], yield: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ordering: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], speed_threshold: [1, 2, 4, 5, 6, 7, 8, 9, 10], programmatic_response: [1, 5, 7, 8, 9], campaign_size: [1, 5, 7, 8], }); }); it("respond managed + programmatic campaigns (multi creatives)", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 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(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 4); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); hasCampaignWithIndex(res.body, "1", 3); // check kafka expectKafkaDeliveryMonitoringMessage(testContext.getKafkaSpy(), "2.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 () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // 8,9 is excluded because of campaign_size step (4) // => result: sorted campaigns 8, 5, 7, 3 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") .withSize(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 4); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 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], brand_safety_ias: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], brand_safety_dv: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], brand_suitability_ias: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ordering: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], speed_threshold: [1, 2, 4, 5, 6, 7, 8, 9, 10], programmatic_response: [1, 5, 7, 8, 9], campaign_size: [1, 5, 7, 8], }); }); it("respond marketplace campaign", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // => result: marketplace winner 21 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns( getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("6"), getCampaignCandidate("10"), getCampaignCandidate("20"), getCampaignCandidate("21") ) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withSize(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 1); hasCampaignWithIndex(res.body, "21", 0, 211); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [2, 3, 4, 6, 10, 20, 21], device_fraud_ias: [2, 3, 4, 6, 10, 20, 21], publisher_fraud_ias: [2, 3, 4, 6, 10, 20, 21], brand_safety_ias: [2, 3, 4, 6, 10, 20, 21], brand_safety_dv: [2, 3, 4, 6, 10, 20, 21], brand_suitability_ias: [2, 3, 4, 6, 10, 20, 21], ordering: [2, 3, 4, 6, 10, 20, 21], speed_threshold: [2, 4, 6, 10, 20, 21], programmatic_response: [20, 21], marketplace_selected: [21], }); }); it("after-ad: no campaign (no marketplace)", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // after_ad don't do marketplace // => result: no campaign const body: RequestContract = new FakeRequestContractBuilder() .withType(RequestType.AFTER_AD) .withCampaigns( getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("6"), getCampaignCandidate("10") ) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withBidfloor(10) .withSize(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 0); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { dv_fraud: [2, 3, 4, 6, 10], device_fraud_ias: [2, 3, 4, 6, 10], publisher_fraud_ias: [2, 3, 4, 6, 10], brand_safety_ias: [2, 3, 4, 6, 10], brand_safety_dv: [2, 3, 4, 6, 10], brand_suitability_ias: [2, 3, 4, 6, 10], ordering: [2, 3, 4, 6, 10], speed_threshold: [2, 4, 6, 10], programmatic_response: [], }); }); }); describe("Kafka topics", () => { it("send to DeliveryMonitoring topic - in-app", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // 8,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") .withSize(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 4); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); hasCampaignWithIndex(res.body, "1", 3); // check kafka expectKafkaDeliveryMonitoringMessage(testContext.getKafkaSpy(), "1.json"); }); it("send to DeliveryMonitoring topic - web", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // 8,9 is excluded because of campaign_size step (4) // => result: sorted campaigns 8, 5, 7, 3 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") .withSize(4) .build(); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 4); hasCampaignWithIndex(res.body, "8", 0); hasCampaignWithIndex(res.body, "5", 1); hasCampaignWithIndex(res.body, "7", 2); hasCampaignWithIndex(res.body, "1", 3); // check kafka expectKafkaDeliveryMonitoringMessage(testContext.getKafkaSpy(), "12.json"); }); }); 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 () => { // 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], brand_safety_ias: [1, 3, 5, 7, 9], brand_safety_dv: [1, 3, 5, 7, 9], brand_suitability_ias: [1, 3, 5, 7, 9], speed_threshold: [1, 5, 7, 9], campaign_size: [5, 7], }); }); }); 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 () => { // 10,8,6,4,2 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(), { programmatic_ivt: [], }); }); it("respond one programmatic campaign", async () => { // 10,6,4,2 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(), { programmatic_response: [8], }); }); }); describe("Test bypass normal flow feature config", () => { it("respond managed + programmatic campaigns", async () => { // bypass_normal_flow excludes every normal campaign other than 20, 21(marketplace) // => result: marketplace winner 21 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns( getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("6"), getCampaignCandidate("10"), getCampaignCandidate("20"), getCampaignCandidate("21") ) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withSize(4) .build(); mockedFeatureConf.setParam("ms-choices.bypass-normal-flow-assets", body.app?.bundle_id ?? ""); featureConfigService.setFeatureConfig(mockedFeatureConf); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); hasLength(res.body, 1); hasCampaignWithIndex(res.body, "21", 0, 211); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { bypass_normal_flow: [20, 21], marketplace_selected: [21], }); }); }); describe("Fallback campaigns", () => { it("responds with fallback campaign", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // => result: winner 22 const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns( getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("6"), getCampaignCandidate("10"), getCampaignCandidate("22") ) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withSize(4) .build(); body.app!.eecpm = 3.6; body.app!.ad_unit_type = "interstitial"; mockedFeatureConf.setParam("ms-choices.fallback-campaign-ids", "22"); featureConfigService.setFeatureConfig(mockedFeatureConf); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { speed_threshold: [2, 4, 6, 10, 22], programmatic_response: [22], }); hasLength(res.body, 1); hasCampaignWithIndex(res.body, "22", 0, 221); }); it("filters out fallback campaigns without adequate margin", async () => { // 3 is excluded because of speed_threshold step // 2,6,4,10 are excluded because of programmatic_response step (no response from auction) // 22 is filtered out because of margin // => result: no winner const body: RequestContract = new FakeRequestContractBuilder() .withCampaigns( getCampaignCandidate("2"), getCampaignCandidate("3"), getCampaignCandidate("4"), getCampaignCandidate("6"), getCampaignCandidate("10"), getCampaignCandidate("22") ) .withUserAgent("aUserAgent") .withIP("1.1.1.1") .withSize(4) .build(); body.app!.eecpm = 3.7; mockedFeatureConf.setParam("ms-choices.fallback-campaign-ids", "22"); featureConfigService.setFeatureConfig(mockedFeatureConf); const res = await testContext.mockHttp(body).expect(200); await helper.waitImmediate(); expectedDeliveryMonitoringSteps(testContext.getKafkaSpy(), { speed_threshold: [2, 4, 6, 10, 22], programmatic_response: [22], }); hasLength(res.body, 0); }); }); });