import { beforeEach, describe, it } from "mocha"; import { expect } from "chai"; import nock from "nock"; import FeatureConfigFake from "../fake/FeatureConfigFake"; import { FeatureConfigService } from "../../../src/services/FeatureConfig.service"; import SpeedService, { PrioScoreCacheKey, ThresholdIsolatedCacheKey, } from "../../../src/services/speed.service"; import { FakeRequestBuilder } from "../fakeRequestBuilder"; import CampaignsClientFake from "../fake/CampaignsClientFake"; import { FakeCampaign } from "../fake/Campaign"; import * as FeatureApi from "@ogury/feature-config-api-client"; import { sleep, waitImmediate } from "../../../src/utils/helper"; import { config } from "../../../src/utils/config"; describe("SpeedService", function () { let featureConfigService: FeatureConfigService; let mockedFeatureConf: FeatureConfigFake = new FeatureConfigFake(); let campaignsClientMock: CampaignsClientFake; let speedService: SpeedService; beforeEach(() => { featureConfigService = FeatureConfigService.getInstance(); mockedFeatureConf = new FeatureConfigFake(); featureConfigService.setFeatureConfig(mockedFeatureConf); campaignsClientMock = new CampaignsClientFake(); speedService = new SpeedService(campaignsClientMock); }); afterEach(() => { mockedFeatureConf.clearParams(); }); describe("addThresholdMetadata", () => { it("should retrieve metadata found in cache", async function () { speedService.campaignCache.forceSetItem("12", { delay: 1, speed_threshold: 0.5 }); speedService.campaignCache.forceSetItem("13", { delay: 1, speed_threshold: null }); const req = new FakeRequestBuilder() .withCampaigns(...["12", "13"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 1, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); expect(req.campaigns[1]).to.deep.equal({ id: "13", category_iab: [], metadata: { vast_hash: "1", delay: 1, speed_threshold: null, }, metadataTmp: {}, stepsPassedStatus: {}, }); expect(campaignsClientMock.getSpeedCalls()).to.equal(0); }); it("should contact ms-speed when not found in cache", async function () { campaignsClientMock.setSpeedResponse({ id: 1, campaigns: [ { id: "12", metadata: { delay: "0.2838", threshold: "0.7", }, }, ], }); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // first call should not find cache but trigger call to API speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", }, metadataTmp: {}, stepsPassedStatus: {}, }); await waitImmediate(); expect(campaignsClientMock.getSpeedCalls()).to.equal(1); // second call should get newly cache but not re-call the API speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.2838, speed_threshold: 0.7, }, metadataTmp: {}, stepsPassedStatus: {}, }); // should not have increase expect(campaignsClientMock.getSpeedCalls()).to.equal(1); }); it("should retrieve isolated metadata found in cache if isolated experiment is running", async function () { const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName); speedService.campaignCache.forceSetItem("12", { delay: 0.9, speed_threshold: 0.7 }); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 1), { delay: 0.3, speed_threshold: 0.5, } ); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(0); }); it("should retrieve isolated metadata with ODID split param", async function () { const experimentName = "isolated experiment with ODID"; mockedFeatureConf.activateIsolatedExperiment( experimentName, ["true", "true"], [50, 50], FeatureApi.SplitParameter.ODID ); speedService.campaignCache.forceSetItem("12", { delay: 0.9, speed_threshold: 0.7 }); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 0), { delay: 0.18, speed_threshold: 0.4, } ); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 1), { delay: 0.3, speed_threshold: 0.5, } ); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.18, speed_threshold: 0.4, }, metadataTmp: {}, stepsPassedStatus: {}, }); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(0); }); it("should retrieve isolated metadata with other ODID split param", async function () { const experimentName = "isolated experiment with ODID 2"; mockedFeatureConf.activateIsolatedExperiment( experimentName, ["true", "true"], [50, 50], FeatureApi.SplitParameter.ODID ); speedService.campaignCache.forceSetItem("12", { delay: 0.67, speed_threshold: 0.23 }); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 0), { delay: 0.42, speed_threshold: 0.666, } ); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 1), { delay: 0.47, speed_threshold: 0.298, } ); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .withODID("71ac0e11-1619-4e5f-8f3e-a797978ce705") .build(); // will belong to pop 1 in experiment speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.47, speed_threshold: 0.298, }, metadataTmp: {}, stepsPassedStatus: {}, }); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(0); }); it("should retrieve standard metadata found in cache if isolated experiment is running but no isolated entry in cache", async function () { const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName); speedService.campaignCache.forceSetItem("12", { delay: 0.3, speed_threshold: 0.5 }); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); await waitImmediate(); expect(campaignsClientMock.getSpeedCalls()).to.equal(0); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(1); }); it("should retrieve standard metadata found in cache if isolated experiment is running but isolated entry is not valid", async function () { const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName); speedService.campaignCache.forceSetItem("12", { delay: 0.3, speed_threshold: 0.5 }); campaignsClientMock.setSpeedIsolatedResponse({ id: 1, experiment_name: "isolated experiment", campaigns: [ { id: "12", pops_metadata: [ { delay: "0.5", threshold: "0.6", }, { delay: null, threshold: "0.2", }, ], }, ], }); // init cache await speedService.isolatedCampaignCache.get( new ThresholdIsolatedCacheKey("12", experimentName, 1) ); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); campaignsClientMock.setSpeedIsolatedResponse({ id: 1, experiment_name: "isolated experiment", campaigns: [ { id: "12", pops_metadata: [ { delay: "0.5", threshold: "0.6", }, { delay: "0.2", threshold: null, }, ], }, ], }); // init cache speedService.isolatedCampaignCache.forceExpire( new ThresholdIsolatedCacheKey("12", experimentName, 1) ); await speedService.isolatedCampaignCache.get( new ThresholdIsolatedCacheKey("12", experimentName, 1) ); const req2 = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment speedService.addThresholdMetadata(req2); expect(req2.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); }); it("should retrieve standard metadata found in cache if isolated experiment is running but requests's pop is not isolated", async function () { const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName, ["true", "false"]); speedService.campaignCache.forceSetItem("12", { delay: 0.3, speed_threshold: 0.5 }); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 0), { delay: 1.1, speed_threshold: 0.9, } ); speedService.isolatedCampaignCache.forceSetItem( new ThresholdIsolatedCacheKey("12", experimentName, 1), { delay: 1.2, speed_threshold: 1.1, } ); const req = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment, but pop 1 is not isolated speedService.addThresholdMetadata(req); expect(req.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); await waitImmediate(); expect(campaignsClientMock.getSpeedCalls()).to.equal(0); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(0); }); it("should get values put in cache at second call if isolated experiment is running but values not in cache first", async function () { campaignsClientMock.setSpeedIsolatedResponse({ id: 1, experiment_name: "isolated experiment", campaigns: [ { id: "12", pops_metadata: [ { delay: "0.5", threshold: "0.6", }, { delay: "0.7", threshold: "0.8", }, ], }, ], }); const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName); speedService.campaignCache.forceSetItem("12", { delay: 0.3, speed_threshold: 0.5 }); const req1 = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment, but pop 1 is not isolated speedService.addThresholdMetadata(req1); expect(req1.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }); await waitImmediate(); // because getting data from API expect(campaignsClientMock.getSpeedCalls()).to.equal(0); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(1); const req2 = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will get isolated metadata of pop 1 at second call speedService.addThresholdMetadata(req2); expect(req2.campaigns[0]).to.deep.equal({ id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.7, speed_threshold: 0.8, }, metadataTmp: {}, stepsPassedStatus: {}, }); }); it("should not use isolated values returned by API if experiment_name is not the same", async function () { campaignsClientMock.setSpeedIsolatedResponse({ id: 1, experiment_name: "isolated experiment 2", campaigns: [ { id: "12", pops_metadata: [ { delay: "0.5", threshold: "0.6", }, { delay: "0.7", threshold: "0.8", }, ], }, ], }); const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName); speedService.campaignCache.forceSetItem("12", { delay: 0.3, speed_threshold: 0.5 }); const req1 = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will belong to pop 1 in experiment, but pop 1 is not isolated speedService.addThresholdMetadata(req1); const expected_res = { id: "12", category_iab: [], metadata: { vast_hash: "1", delay: 0.3, speed_threshold: 0.5, }, metadataTmp: {}, stepsPassedStatus: {}, }; expect(req1.campaigns[0]).to.deep.equal(expected_res); await waitImmediate(); // because getting data from API expect(campaignsClientMock.getSpeedCalls()).to.equal(0); expect(campaignsClientMock.getSpeedIsolatedCalls()).to.equal(1); const req2 = new FakeRequestBuilder() .withCampaigns(...["12"].map((id) => new FakeCampaign(id, { vast_hash: "1" }))) .build(); // will get isolated metadata of pop 1 at second call speedService.addThresholdMetadata(req2); expect(req2.campaigns[0]).to.deep.equal(expected_res); }); }); describe("addPrioScoreMetadata", () => { afterEach(() => { nock.cleanAll(); }); it("should retrieve metadata found in cache", async function () { speedService.prioScoreCache.forceSetItem( new PrioScoreCacheKey("1", 3, "PL", "web", "interstitial"), { delay: 1, priority_score: 0.5 } ); speedService.prioScoreCache.forceSetItem( new PrioScoreCacheKey("2", 3, "PL", "web", "interstitial"), { delay: 0.5, priority_score: 0.7 } ); const req = new FakeRequestBuilder().build(); req.app = { ...req.app, app_api_key: "2", app_publisher_id: 3, ad_unit_type: "interstitial", }; req.context = { ...req.context, country: "PL", }; req.environment_type = "web"; speedService.addPrioScoreMetadata(req); expect(req.metadata.supply_obj_prio_score).to.equal(0.7); expect(req.metadata.supply_obj_delay).to.equal(0.5); }); it("should contact ms-speed when not found in cache", async function () { const scope = nock(`http://${config.MS_SPEED_HTTP_HOST}`) .post("/speed/supply") .reply(200, async function (uri: string, body: any) { expect(body).to.include({ app_api_key: "2", publisher_id: 3, country: "PL", environment_type: "web", format: "interstitial", }); return { delay: 0.1, priority_score: 2, }; }) .persist(); speedService.prioScoreCache.forceSetItem( new PrioScoreCacheKey("1", 3, "PL", "web", "interstitial"), { delay: 1, priority_score: 0.5 } ); const req = new FakeRequestBuilder().build(); req.app = { ...req.app, app_api_key: "2", app_publisher_id: 3, ad_unit_type: "interstitial", }; req.context = { ...req.context, country: "PL", }; req.environment_type = "web"; speedService.addPrioScoreMetadata(req); expect(req.metadata.supply_obj_prio_score).to.equal(undefined); expect(req.metadata.supply_obj_delay).to.equal(undefined); while (!scope.isDone()) { await sleep(10); } await waitImmediate(); await waitImmediate(); speedService.addPrioScoreMetadata(req); expect(req.metadata.supply_obj_prio_score).to.equal(2); expect(req.metadata.supply_obj_delay).to.equal(0.1); }); it("should contact ms-speed when isolated score not found in cache", async function () { const experimentName = "isolated experiment"; mockedFeatureConf.activateIsolatedExperiment(experimentName); const scope = nock(`http://${config.MS_SPEED_HTTP_HOST}`) .post("/isolated-speed/supply") .reply(200, async function (uri: string, body: any) { expect(body).to.include({ app_api_key: "2", publisher_id: 3, country: "PL", environment_type: "web", format: "interstitial", }); return { experiment_name: experimentName, pops_metadata: [ { delay: 0.1, priority_score: 1 }, { delay: 0.2, priority_score: 2 }, ], }; }) .persist(); speedService.prioScoreCache.forceSetItem( new PrioScoreCacheKey("2", 3, "PL", "web", "interstitial"), { delay: 1, priority_score: 0.5 } ); const req = new FakeRequestBuilder().build(); req.app = { ...req.app, app_api_key: "2", app_publisher_id: 3, ad_unit_type: "interstitial", }; req.context = { ...req.context, country: "PL", }; req.environment_type = "web"; speedService.addPrioScoreMetadata(req); expect(req.metadata.supply_obj_prio_score).to.equal(0.5); expect(req.metadata.supply_obj_delay).to.equal(1); while (!scope.isDone()) { await sleep(10); } await waitImmediate(); await waitImmediate(); speedService.addPrioScoreMetadata(req); expect(req.metadata.supply_obj_prio_score).to.equal(2); expect(req.metadata.supply_obj_delay).to.equal(0.2); }); }); describe("updateCacheMetrics", () => { it("should not crash when updating metrics", () => { speedService.updateCacheMetrics(speedService.campaignCache, false, "campaign_threshold"); }); }); });