import { expect } from "chai"; import InternalFunnel from "../../../src/funnels/internal.funnel"; import { MockedFeatureConfig } from "@ogury/feature-config-sdk"; import { FeatureConfigService } from "../../../src/services/FeatureConfig.service"; import MetricService, { BucketSet } from "../../../src/services/metric.service"; import constants from "../../../src/utils/constants"; import { Response } from "express"; import { CampaignModelFake } from "../fake/CampaignModelFake"; import { DVFraudService } from "../../../src/services/dvFraud.service"; import nock from "nock"; import sinon = require("sinon"); import YieldStep from "../../../src/steps/yield/yield.step"; import { StepBuilder } from "../../../src/steps/stepBuilder"; import { FakeRequestContractBuilder } from "../../integration/utils/helper"; import { Campaign } from "../../../src/models"; import { FakeCampaign } from "../fake/Campaign"; import { delivery } from "../../../src/protos/protos"; import { MsAuctionService } from "../../../src/services/auction/MsAuction.service"; import SpeedService from "../../../src/services/speed.service"; import { StepFactory } from "../../../src/steps/stepFactory"; const ContextServiceFake = require("../fake/targeting/ContextServiceFake"); let contextService; const res = { status: () => res, json: () => {}, end: () => {}, }; describe("InternalFunnel", function () { let featureConfigService: FeatureConfigService; let mockedFeatureConf: MockedFeatureConfig; let campaignModel: CampaignModelFake; let candidateCampaigns: Campaign[]; let metricService: MetricService; let internalFunnel: InternalFunnel; let dvFraudService: DVFraudService; let campaignsClientMock: delivery.Campaigns; let msYieldResponse: delivery.RecommenderResponse; let yieldStep: YieldStep; let msAuctionService: MsAuctionService; before(() => { contextService = new ContextServiceFake({ "1000": { "1": { score: 0.01, avgAccRate: 1 }, }, "2000": { "2": { score: 0.02, avgAccRate: 1 }, }, "3000": { "3": { score: 0.03, avgAccRate: 1 }, }, }); featureConfigService = FeatureConfigService.getInstance(); mockedFeatureConf = new MockedFeatureConfig(); // mock campaignModel : // campaign 1000 and 3000 are prog // budget of each campaign are their id, so bid price will be: // 1000 => 10 // 2000 => 20 // 3000 => 30 campaignModel = new CampaignModelFake([ { id: "1000", budget: 1000, clicks_sold: 1000, margin: 0, ctr: 0.01, creatives: [{ id: 1, name: "cr1", format: { id: "format", ad_type: "display" } }], }, { id: "2000", budget: 1000, clicks_sold: 1000, margin: 0, ctr: 0.01, creatives: [{ id: 2, name: "cr2", format: { id: "format", ad_type: "display" } }], }, { id: "3000", budget: 1000, clicks_sold: 1000, margin: 0, ctr: 0.01, creatives: [{ id: 3, name: "cr3", format: { id: "format", ad_type: "display" } }], }, { id: "4000", budget: 1000, family: "marketplace", clicks_sold: 1000, margin: 0, ctr: 0.01, creatives: [{ id: 4, name: "cr4", format: { id: "format", ad_type: "display" } }], }, ]); metricService = MetricService.getInstance(); metricService.addSection( constants.CHOICES_REQUEST_SECTION, constants.CHOICES_REQUEST_SECTION_LABELS, BucketSet.zeroTo600ms ); dvFraudService = new DVFraudService(); msAuctionService = new MsAuctionService(new CampaignModelFake(), "https://nock", {} as any); msYieldResponse = delivery.RecommenderResponse.create({ id: "1", difference: 1, bound_lower: 2, bound_upper: 3, app_ecpm: 4, app_eecpm: 5, campaigns: [ { id: "1000", selected: true, metadata: { vast_hash: "something" } }, { id: "2000", selected: false, metadata: { vast_hash: "something" } }, { id: "3000", selected: true, metadata: { vast_hash: "something" } }, ], }); campaignsClientMock = { getRecommender: sinon.fake(async () => msYieldResponse), } as any; yieldStep = new YieldStep(campaignsClientMock); const stepFactory = new StepFactory( campaignModel, contextService, dvFraudService, new SpeedService() ); stepFactory.initProgrammatic(msAuctionService); internalFunnel = new InternalFunnel( campaignModel, stepFactory, null, new StepBuilder(yieldStep).kafka().metrics().build() ); }); beforeEach(() => { candidateCampaigns = [ new FakeCampaign("1000", undefined, undefined, [1]), new FakeCampaign("2000", undefined, undefined, [2]), new FakeCampaign("3000", undefined, undefined, [3]), ]; featureConfigService.setFeatureConfig(mockedFeatureConf); }); afterEach(() => { mockedFeatureConf.clearParams(); sinon.restore(); nock.cleanAll(); }); it("should respond 2 campaigns because yield filter out 1 incoming request campaign", async function () { const randomStub = sinon.stub(Math, "random").returns(0.001); const req = new FakeRequestContractBuilder() .withRandomId() .withCampaigns(...candidateCampaigns) .withBidfloor(124) .build(); const cacheKey = yieldStep.buildCacheKey(req.app?.ad_unit_id ?? "", "PL", candidateCampaigns); yieldStep.setResponseCache(cacheKey, msYieldResponse, 60000); const response = await internalFunnel.choose(req, res as unknown as Response); expect(response?.campaigns).to.have.lengthOf(2); randomStub.restore(); }); it("should respond 0 campaigns because by pass normal flow is enabled and no marketplace campaign in candidates", async function () { const req = new FakeRequestContractBuilder() .withRandomId() .withCampaigns(...candidateCampaigns) .withBidfloor(124) .build(); mockedFeatureConf.setParam("ms-choices.bypass-normal-flow-assets", req.app?.bundle_id ?? ""); featureConfigService.setFeatureConfig(mockedFeatureConf); const cacheKey = yieldStep.buildCacheKey(req.app?.ad_unit_id ?? "", "PL", candidateCampaigns); yieldStep.setResponseCache(cacheKey, msYieldResponse, 60000); const response = await internalFunnel.choose(req, res as unknown as Response); expect(response?.campaigns).to.have.lengthOf(0); }); it("should respond the marketplace campaign because by pass normal flow is enabled and marketplace campaign is selected by marketplace service", async function () { candidateCampaigns.push(new FakeCampaign("4000", undefined, undefined, [4])); const req = new FakeRequestContractBuilder() .withRandomId() .withCampaigns(...candidateCampaigns) .withBidfloor(124) .build(); mockedFeatureConf.setParam("ms-choices.bypass-normal-flow-assets", req.app?.bundle_id ?? ""); featureConfigService.setFeatureConfig(mockedFeatureConf); const cacheKey = yieldStep.buildCacheKey(req.app?.ad_unit_id ?? "", "PL", candidateCampaigns); yieldStep.setResponseCache(cacheKey, msYieldResponse, 60000); nock("https://nock") .post("/auction") .reply((_, requestBody: any) => { return [ 200, { advert_id: requestBody.advert_id, campaign: requestBody.campaigns[0].id, price: 0.1, advertiser_domain: "advertiser_domain", deal_id: requestBody.campaigns[0].deal_id, seat: "wseat1", }, ]; }); const response = await internalFunnel.choose(req, res as unknown as Response); expect(response?.campaigns).to.have.lengthOf(1); expect(response?.campaigns[0].id).to.equal("4000"); }); it("should respond 0 campaign because by pass normal flow is enabled and marketplace campaign is not selected by marketplace service", async function () { candidateCampaigns.push(new FakeCampaign("4000", undefined, undefined, [4])); const req = new FakeRequestContractBuilder() .withRandomId() .withCampaigns(...candidateCampaigns) .withBidfloor(124) .build(); mockedFeatureConf.setParam("ms-choices.bypass-normal-flow-assets", req.app?.bundle_id ?? ""); featureConfigService.setFeatureConfig(mockedFeatureConf); const cacheKey = yieldStep.buildCacheKey(req.app?.ad_unit_id ?? "", "PL", candidateCampaigns); yieldStep.setResponseCache(cacheKey, msYieldResponse, 60000); nock("https://nock") .post("/auction") .reply((_, requestBody: any) => { return [ 200, { advert_id: requestBody.advert_id, }, ]; }); const response = await internalFunnel.choose(req, res as unknown as Response); expect(response?.campaigns).to.have.lengthOf(0); }); });