import { RedisClient, RedisService } from "../../../src/services/redis.service"; import { StepBuilder } from "../../../src/steps/stepBuilder"; import DVFraudStep from "../../../src/steps/dvfraud/dvFraud.step"; import { FakeRequestBuilder } from "../fakeRequestBuilder"; import constants from "../../../src/utils/constants"; import { MockedFeatureConfig } from "@ogury/feature-config-sdk"; import { FeatureConfigService } from "../../../src/services/FeatureConfig.service"; import { CampaignModelFake } from "../fake/CampaignModelFake"; import { CampaignData } from "../../../src/models/CampaignModel"; import { DVFraudService } from "../../../src/services/dvFraud.service"; import { FakeCampaign } from "../fake/Campaign"; import { StepResult } from "../../../src/steps/stepResult"; import { Step } from "../../../src/steps/step"; describe("DVFraud", function () { let innerFraudStep: DVFraudStep; let dvFraudStep: Step; let redisClientApp: RedisClient; let redisClientDevice: RedisClient; let mockedFeatureConf: MockedFeatureConfig; let featureConfigService: FeatureConfigService; let campaignData: Partial[]; let requestBuilder: FakeRequestBuilder; before(async () => { const dvFraudService = new DVFraudService(); innerFraudStep = new DVFraudStep(new CampaignModelFake(), dvFraudService); dvFraudStep = new StepBuilder(innerFraudStep).build(); redisClientApp = RedisService.getClient("dv_app", true, 0); redisClientDevice = RedisService.getClient("dv_device", true, 2); featureConfigService = FeatureConfigService.getInstance(); }); after(async () => { redisClientApp.disconnect(); redisClientDevice.disconnect(); }); beforeEach(async () => { mockedFeatureConf = new MockedFeatureConfig(); featureConfigService.setFeatureConfig(mockedFeatureConf); // feed redis // domain courbis.fr -> hash: 0x104AF593AB1AAA3C let buf = Buffer.from("104AF593AB1AAA3C", "hex"); await redisClientApp.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // domain test.somedomain.fr -> hash: 0xEB64965140E88278 buf = Buffer.from("EB64965140E88278", "hex"); await redisClientApp.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // bundle com.glukin.dolphins -> hash: D6A4AFAA1200CE77 buf = Buffer.from("D6A4AFAA1200CE77", "hex"); await redisClientApp.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // app store ID 1031187506 -> hash: 5C9E683B1C386F79 buf = Buffer.from("5C9E683B1C386F79", "hex"); await redisClientApp.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // domain www.académie-française.fr -> hash 31E19CFB66AA11A5 buf = Buffer.from("31E19CFB66AA11A5", "hex"); await redisClientApp.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // ip + ua 173.28.1.204 Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36 -> hash: 81A77BB159F49160 buf = Buffer.from("81A77BB159F49160", "hex"); await redisClientDevice.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // ip 97.127.181.63 -> hash: D48A41A531A3D9B6 buf = Buffer.from("D48A41A531A3D9B6", "hex"); await redisClientDevice.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // ip 23.28.125.198 -> hash: 66D73A565AAFCE97 buf = Buffer.from("66D73A565AAFCE97", "hex"); await redisClientDevice.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // ipv6 2001:0620:0000:0000:0211:24FF:FE80:C12C -> hash: 7A0890264B61A619 buf = Buffer.from("7A0890264B61A619", "hex"); await redisClientDevice.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); // ipv6 + ua 2001:0620:0000:0000:0211:24FF:FE80:C12CMozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36 -> hash: 989FF65B75C98982 buf = Buffer.from("989FF65B75C98982", "hex"); await redisClientDevice.client.hmset(buf.slice(0, 3), buf.slice(3, 8), 1); campaignData = [ { id: "10001", dv_strong_filtering: false }, { id: "10002", dv_strong_filtering: true }, ]; requestBuilder = new FakeRequestBuilder() .withCampaignData(campaignData) .withUserAgent(undefined) .withIP(undefined) .withIPv6(undefined); }); describe("DV app fraud ANDROID", function () { it("should flag this app bundle as fraud", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("com.glukin.dolphins") .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should flag this app bundle without prebid as not fraud", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("com.glukin.dolphins") .withDeviceOS("android") .build() ); shouldBeFraud(stepRes, true, false); }); it("should flag this app bundle with ias prebid as not fraud", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("com.glukin.dolphins") .withPrebid({ type: "publisher-fraud", provider: "ias" }) .withDeviceOS("android") .build() ); shouldBeFraud(stepRes, true, false); }); it("should flag this app bundle with device fraud prebid as not fraud", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("com.glukin.dolphins") .withDeviceOS("android") .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true, false); }); it("should bypass the step because of api key", async function () { mockedFeatureConf.setParam("ms-choices.dv-bypass-apikeys", "333,222"); const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withApiKey("222") .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); stepRes.passedCampaigns.length.should.be.above(0); }); it("should bypass the step because of api key (type number)", async function () { mockedFeatureConf.setParam("ms-choices.dv-bypass-apikeys", "333,222"); const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withApiKey(222) .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); stepRes.passedCampaigns.length.should.be.above(0); }); it("should flag this uppercase app bundle as fraud", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("COM.GLUKIN.DOLPHINS") .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should flag this special char app bundle as fraud", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("www.académie-française.fr") .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should NOT flag this valid app bundle", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("another.valid.appbundle") .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, false); }); }); describe("DV app fraud IOS", function () { it("should flag this app store ID as fraud (external)", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("1031187506", true) .withDeviceOS("ios") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .flagAsExternal() .build() ); shouldBeFraud(stepRes, true); }); }); describe("DV device fraud", function () { it("should NOT flag as fraud : valid ip + ua", async function () { const stepRes = await dvFraudStep.execute( requestBuilder.withValidAppBundleOrDomain().withValidIpAndUserAgent().build() ); shouldBeFraud(stepRes, false); }); it("should flag as fraud : invalid ip + ua", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIP("173.28.1.204") .withUserAgent( "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" ) .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should flag as fraud : invalid ipv6 + ua", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIP(undefined) .withIPv6("2001:0620:0000:0000:0211:24FF:FE80:C12C") .withUserAgent( "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" ) .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should flag as fraud : invalid ip + ua with GFE suffix", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIP("173.28.1.204") .withUserAgent( "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" + constants.DV_GFE_SUFFIX ) .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should NOT flag as fraud : valid ip only", async function () { const stepRes = await dvFraudStep.execute( requestBuilder.withValidAppBundleOrDomain().withValidIp().build() ); shouldBeFraud(stepRes, false); }); it("should NOT flag as fraud : valid ipv6 only", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIPv6("3001:0620:0000:0000:0211:24FF:FE80:C12C") .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, false); }); it("should flag as fraud : invalid ip only", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIP("97.127.181.63") .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); stepRes.passedCampaigns.length; }); it("should NOT flag as fraud : invalid ipv6 only", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withIPv6("2001:0620:0000:0000:0211:24FF:FE80:C12C") .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should flag as fraud : invalid ip only 2", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIP("23.28.125.198") .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); it("should flag as fraud : ua only", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withUserAgent("someUA") .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, true); }); }); describe("DV strong fraud", function () { it("should flag as strong fraud and remove 10002 campaign: no user-agent", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withCampaigns(new FakeCampaign("10001"), new FakeCampaign("10002")) .withValidAppBundleOrDomain() .withValidIp() .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); const isFraud = stepRes.getMetricLabel("is_fraud"); isFraud.should.be.equal("false"); stepRes.passedCampaigns.length.should.be.equal(1); stepRes.passedCampaigns[0].should.be.deep.equal({ id: "10001", category_iab: [], metadata: {}, metadataTmp: {}, stepsPassedStatus: { pass_dv_fraud: "true", }, }); }); it("should flag as strong fraud and remove all campaign: no user-agent", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withCampaigns(new FakeCampaign("10002")) .withValidAppBundleOrDomain() .withValidIp() .withPrebid({ type: "device-fraud", provider: "dv" }) .build() ); const isFraud = stepRes.getMetricLabel("is_fraud"); isFraud.should.be.equal("false"); stepRes.passedCampaigns.length.should.be.equal(0); }); it("should flag as not fraud with publisher-prebid: no user-agent", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withCampaigns(new FakeCampaign("10002")) .withValidAppBundleOrDomain() .withValidIp() .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); const isFraud = stepRes.getMetricLabel("is_fraud"); isFraud.should.be.equal("false"); stepRes.passedCampaigns.length.should.be.equal(1); }); }); describe("Test with multiple campaigns and prebids", function () { beforeEach(async () => { requestBuilder.withCampaigns( new FakeCampaign("10001"), new FakeCampaign("10002"), new FakeCampaign("10003"), new FakeCampaign("10004"), new FakeCampaign("10005"), new FakeCampaign("10006"), new FakeCampaign("10007"), new FakeCampaign("10008") ); const campaignData = [ { id: "10001", prebid: [{ type: "publisher-fraud", provider: "dv" }] }, { id: "10002", prebid: [{ type: "device-fraud", provider: "dv" }] }, { id: "10003", prebid: [ { type: "publisher-fraud", provider: "dv" }, { type: "device-fraud", provider: "dv" }, ], }, { id: "10004", prebid: [ { type: "publisher-fraud", provider: "dv" }, { type: "device-fraud", provider: "ias" }, ], }, { id: "10005", prebid: [ { type: "publisher-fraud", provider: "ias" }, { type: "device-fraud", provider: "dv" }, ], }, { id: "10006", prebid: [ { type: "publisher-fraud", provider: "ias" }, { type: "device-fraud", provider: "ias" }, ], }, { id: "10007" }, ]; requestBuilder.withCampaignData(campaignData); }); it("should NOT flag this valid app bundle", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("another.valid.appbundle") .withDeviceOS("android") .withPrebid({ type: "publisher-fraud", provider: "dv" }) .build() ); shouldBeFraud(stepRes, false); stepRes.passedCampaigns.length.should.be.equal(8); }); it("should filter publisher fraud campaign", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidIpAndUserAgent() .withAppBundleOrDomain("com.glukin.dolphins") .withDeviceOS("android") .build() ); shouldBeFraud(stepRes, true, false); stepRes.passedCampaigns.length.should.be.equal(5); const ids = stepRes.passedCampaigns.map((campaign) => campaign.id); ids.should.contain("10002"); ids.should.contain("10005"); ids.should.contain("10006"); ids.should.contain("10007"); ids.should.contain("10008"); }); it("should filter device fraud campaign", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withValidAppBundleOrDomain() .withIP("173.28.1.204") .withUserAgent( "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" ) .build() ); shouldBeFraud(stepRes, true, false); stepRes.passedCampaigns.length.should.be.equal(5); const ids = stepRes.passedCampaigns.map((campaign) => campaign.id); ids.should.contain("10001"); ids.should.contain("10004"); ids.should.contain("10006"); ids.should.contain("10007"); ids.should.contain("10008"); }); it("should filter both fraud campaign", async function () { const stepRes = await dvFraudStep.execute( requestBuilder .withAppBundleOrDomain("com.glukin.dolphins") .withDeviceOS("android") .withIP("173.28.1.204") .withUserAgent( "Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" ) .build() ); shouldBeFraud(stepRes, true, false); stepRes.passedCampaigns.length.should.be.equal(3); const ids = stepRes.passedCampaigns.map((campaign) => campaign.id); ids.should.contain("10006"); ids.should.contain("10007"); ids.should.contain("10008"); }); }); }); function shouldBeFraud(stepRes: StepResult, fraud: boolean, blockAll?: boolean) { const isFraud = stepRes.getMetricLabel("is_fraud"); const passedCampaignLength = stepRes.passedCampaigns.length; if (blockAll === undefined) blockAll = fraud; isFraud.should.be.equal(fraud ? "true" : "false"); if (blockAll) { passedCampaignLength.should.be.equal(0); } else { passedCampaignLength.should.be.above(0); } }