import AudienceHashMapFake from "../fake/targeting/AudienceHashMapFake"; import createS3Stub from "../../mocks/s3.mock"; import sinon from "sinon"; import { Campaign, ChoiceRequest } from "../../../src/models"; import { AudienceMetadata, AudienceScoreInfo, } from "../../../src/services/scoringSystem/audienceHashMap.service"; import { BucketMapping } from "../../../src/services/scoringSystem/abstractScoreHashMap.service"; import { CampaignCreative } from "../../../src/steps/ordering/ordering.model"; import { FakeRequestBuilder } from "../fakeRequestBuilder"; import { FakeCampaign } from "../fake/Campaign"; const chai = require("chai"); const expect = chai.expect; describe("AudienceHashMap Service", () => { let s3Stub: sinon.SinonStub; let expectedCampaignBrief: Map; let expectedAppIndex: Map; let expectedBriefScores: Map>; before(() => { expectedCampaignBrief = new Map([ ["c1", "b1"], ["c2", "b2"], ["c3", "b2"], ["c4", "b1"], ]); expectedAppIndex = new Map([ ["a1", 1], ["a2", 2], ["a3", 4], ["a4", 3], ["https://a/4", 5], ]); expectedBriefScores = new Map([ [ "b1", new Map([ [-1, { score: 0.7, bucketsMap: [] }], [1, { score: 0, bucketsMap: [] }], [2, { score: 1, bucketsMap: [] }], [3, { score: 0.6, bucketsMap: [] }], [4, { score: 0.5, bucketsMap: [] }], [5, { score: 0.55, bucketsMap: [] }], ]), ], [ "b2", new Map([ [ -1, { score: 0.8, bucketsMap: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, ], [1, { score: 0.4, bucketsMap: [] }], [2, { score: 0.3, bucketsMap: [] }], [3, { score: 0.2, bucketsMap: [] }], [4, { score: 0.1, bucketsMap: [] }], [5, { score: 0.11, bucketsMap: [] }], ]), ], [ "-1", new Map([ [ -1, { score: 0.9, bucketsMap: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, ], ]), ], ]); }); beforeEach(() => { s3Stub = createS3Stub(); }); afterEach(() => { s3Stub.restore(); }); function checkAudienceScores(audience: AudienceHashMapFake) { const { campaigns, apps, similarities } = audience.getAudienceScores(); expect(campaigns).to.deep.equal(expectedCampaignBrief); expect(apps).to.deep.equal(expectedAppIndex); expect(similarities).to.deep.equal(expectedBriefScores); } describe("refresh", () => { it("should load scores with the correct values", async () => { const audience = new AudienceHashMapFake(); await audience.runRefresh(); checkAudienceScores(audience); }); it("should not modify scores in case of refresh error", async () => { const audience = new AudienceHashMapFake(); await audience.runRefresh(); audience.setS3Prefix("?"); await audience.runRefresh(); checkAudienceScores(audience); }); it("should respect maxRetries parameter", async () => { const audience = new AudienceHashMapFake(); audience.setS3Prefix("?"); audience.setMaxRetries(100); const spy = sinon.spy(audience, "refresh" as any); await audience.runRefresh(); spy.restore(); expect(spy.callCount).to.equal(101); }); }); describe("addScores", () => { interface TestCase { app: string; url?: string; campaigns: { id: string; meta: { sc: number; prior: number; bf: string; fb: boolean; bm: BucketMapping[]; }; }[]; } it("should add the correct score/metadata to each campaign", async () => { const testCases: TestCase[] = [ { app: "a1", campaigns: [ { id: "c1", meta: { sc: 0, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "c2", meta: { sc: 0.4, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.4, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 0, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, { app: "a2", campaigns: [ { id: "c1", meta: { sc: 1.0, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "c2", meta: { sc: 0.3, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.3, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 1.0, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, { app: "a3", campaigns: [ { id: "c1", meta: { sc: 0.5, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "c2", meta: { sc: 0.1, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.1, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 0.5, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, { app: "a4", campaigns: [ { id: "c1", meta: { sc: 0.6, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "c2", meta: { sc: 0.2, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.2, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 0.6, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, { app: "a4", url: "https://a/4", // Url should have precedence over app campaigns: [ { id: "c1", meta: { sc: 0.55, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "c2", meta: { sc: 0.11, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.11, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 0.55, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, { app: "a4", url: "https://unknown", // Should fallback to app if unknown url campaigns: [ { id: "c1", meta: { sc: 0.6, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "c2", meta: { sc: 0.2, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.2, prior: 0.8, bf: "b2", fb: false, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 0.6, prior: 0.7, bf: "b1", fb: false, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, { app: "?", campaigns: [ { id: "c1", meta: { sc: 0.7, prior: 0.7, bf: "b1", fb: true, bm: [] }, }, { id: "c2", meta: { sc: 0.8, prior: 0.8, bf: "b2", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c3", meta: { sc: 0.8, prior: 0.8, bf: "b2", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.92 }, { inputUpper: 1, outputLower: 0.92, outputUpper: 1 }, ], }, }, { id: "c4", meta: { sc: 0.7, prior: 0.7, bf: "b1", fb: true, bm: [] }, }, { id: "?", meta: { sc: 0.9, prior: 0.9, bf: "-1", fb: true, bm: [ { inputUpper: 0.1, outputLower: 0, outputUpper: 0.76 }, { inputUpper: 0.785, outputLower: 0.76, outputUpper: 0.91 }, { inputUpper: 1, outputLower: 0.91, outputUpper: 1 }, ], }, }, ], }, ]; const audience = new AudienceHashMapFake(); await audience.runRefresh(); testCases.forEach((testCase) => { const campaignCreatives: CampaignCreative[] = testCase.campaigns.map((c) => { return { campaign: new FakeCampaign(c.id), creativeId: 0, metadata: {}, metadataTmp: {}, }; }); const request: ChoiceRequest = new FakeRequestBuilder().build(); audience.addScores(request, campaignCreatives, testCase.app, testCase.url); for (let i = 0; i < campaignCreatives.length; i++) { const expectedScore = testCase.campaigns[i].meta.sc; const resultScore = campaignCreatives[i].metadata.audience_score; const expectedMetadata: AudienceMetadata = { brief: testCase.campaigns[i].meta.bf, score: testCase.campaigns[i].meta.sc, prior: testCase.campaigns[i].meta.prior, fallback: testCase.campaigns[i].meta.fb, bucketsMap: testCase.campaigns[i].meta.bm, }; const resultMetadata: AudienceMetadata = { ...campaignCreatives[i].metadataTmp.audienceMetadata!, }; expect(resultScore).to.equal(expectedScore); expect(resultMetadata).to.deep.equal(expectedMetadata); } }); }); }); });