d3ro-voice/apps/desktop/tests/unit/AdMediationEngine.spec.ts
Yun Chan c8d802d78f feat(desktop): fill sponsor slots from direct house campaigns
When mediation had no programmatic fill, the banner and rewarded surfaces
collapsed to empty space. Direct house sponsors now serve their own copy and
click-through, with the same settlement accounting used by the mediated
network, and the mediation engine tests cover the added path.
2026-09-16 23:23:39 +09:00

277 lines
9.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AdMediationConfig } from '@d3ro/core/types'
import { getAdMediationEngine } from '../../src/main/services/ads/AdMediationEngine'
import { getAdSettlementService } from '../../src/main/services/ads/AdSettlementService'
import { EthicalAdsAdapter } from '../../src/main/services/ads/EthicalAdsAdapter'
import { CarbonAdsAdapter } from '../../src/main/services/ads/CarbonAdsAdapter'
import { PlaywireAdapter } from '../../src/main/services/ads/PlaywireAdapter'
import { UnityAdsAdapter } from '../../src/main/services/ads/UnityAdsAdapter'
import { AppLovinAdapter } from '../../src/main/services/ads/AppLovinAdapter'
import { GoogleAdManagerAdapter } from '../../src/main/services/ads/GoogleAdManagerAdapter'
import { InMobiAdapter } from '../../src/main/services/ads/InMobiAdapter'
import { PubMaticAdapter } from '../../src/main/services/ads/PubMaticAdapter'
import { MintegralAdapter } from '../../src/main/services/ads/MintegralAdapter'
import { DirectHouseSponsorAdapter } from '../../src/main/services/ads/DirectHouseSponsorAdapter'
const adapterCases = [
['EthicalAds', new EthicalAdsAdapter()],
['Carbon Ads', new CarbonAdsAdapter()],
['Playwire', new PlaywireAdapter()],
['Unity Ads', new UnityAdsAdapter()],
['AppLovin', new AppLovinAdapter()],
['Google Ad Manager', new GoogleAdManagerAdapter()],
['InMobi', new InMobiAdapter()],
['PubMatic', new PubMaticAdapter()],
['Mintegral', new MintegralAdapter()],
] as const
describe('desktop ad provider boundaries', () => {
it.each(adapterCases)('%s returns no bid until a real provider is integrated', async (_name, adapter) => {
await adapter.init()
const bid = await adapter.requestBid({
placement: adapter.supportedFormats.includes('rewarded_video')
? 'rewarded_video_quota'
: 'bottom_dock_banner',
format: adapter.supportedFormats[0],
})
expect(bid).toEqual({
hasBid: false,
bidEcpm: 0,
latencyMs: 0,
error: 'provider_not_integrated',
})
await expect(adapter.reportRewardCompletion('forged-ad-id')).resolves.toEqual({
success: false,
tokenReward: 0,
})
})
})
describe('DirectHouseSponsorAdapter live decision endpoint', () => {
it('is fail-closed until an endpoint is configured', async () => {
const adapter = new DirectHouseSponsorAdapter()
await adapter.init()
const bid = await adapter.requestBid({ placement: 'bottom_dock_banner', format: 'banner_dock' })
expect(bid).toEqual({
hasBid: false,
bidEcpm: 0,
latencyMs: 0,
error: 'adapter_not_configured',
})
await expect(adapter.reportRewardCompletion('forged-ad-id')).resolves.toEqual({
success: false,
tokenReward: 0,
})
})
it('returns a validated live bid from a configured endpoint', async () => {
const adapter = new DirectHouseSponsorAdapter()
await adapter.init({ endpointUrl: 'https://ads.example.com', apiSecret: 'secret' })
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
bidEcpm: 9.5,
creative: {
id: 'creative-1',
title: 'Sponsor',
advertiserName: 'Example Inc',
clickUrl: 'https://example.com/offer',
format: 'banner_dock',
},
}),
{ status: 200 },
),
)
vi.stubGlobal('fetch', fetchMock)
try {
const bid = await adapter.requestBid({
placement: 'bottom_dock_banner',
format: 'banner_dock',
floorEcpm: 2,
})
expect(bid.hasBid).toBe(true)
expect(bid.bidEcpm).toBe(9.5)
expect(bid.creative?.networkId).toBe('direct_sponsor')
expect(bid.creative?.clickUrl).toBe('https://example.com/offer')
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(String(url)).toBe('https://ads.example.com/bid')
expect(init.headers).toMatchObject({ Authorization: 'Bearer secret' })
} finally {
vi.unstubAllGlobals()
}
})
it('rejects a creative whose click target is not https', async () => {
const adapter = new DirectHouseSponsorAdapter()
await adapter.init({ endpointUrl: 'https://ads.example.com' })
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
bidEcpm: 9.5,
creative: {
id: 'c',
title: 't',
advertiserName: 'a',
clickUrl: 'http://insecure.example.com',
format: 'banner_dock',
},
}),
{ status: 200 },
),
),
)
try {
const bid = await adapter.requestBid({
placement: 'bottom_dock_banner',
format: 'banner_dock',
})
expect(bid.hasBid).toBe(false)
expect(bid.error).toBe('invalid_creative')
} finally {
vi.unstubAllGlobals()
}
})
})
describe('AdMediationEngine fail-closed auction and telemetry', () => {
const engine = getAdMediationEngine()
const settlement = getAdSettlementService()
const knownNetworks = engine.getConfig().networks
let disabledNetworks: AdMediationConfig['networks']
beforeEach(() => {
disabledNetworks = knownNetworks.map((network) => ({
...network,
enabled: false,
}))
engine.setConfig({ networks: disabledNetworks, houseAdFallback: false })
})
it('returns a real no-fill result instead of inventing a winner', async () => {
const result = await engine.runAuction({
placement: 'bottom_dock_banner',
format: 'banner_dock',
floorEcpm: 2,
auctionTimeoutMs: 800,
})
expect(result.winner).toBeNull()
expect(result.winningBidEcpm).toBe(0)
expect(result.participatingBids).toEqual([])
expect(result.totalAuctionLatencyMs).toBeGreaterThanOrEqual(0)
})
it('keeps provider shells no-bid even if an operator enables them prematurely', async () => {
engine.setConfig({
networks: disabledNetworks.map((network) => ({ ...network, enabled: true })),
})
const result = await engine.runAuction({
placement: 'bottom_dock_banner',
format: 'banner_dock',
floorEcpm: 0.01,
auctionTimeoutMs: 800,
})
expect(result.winner).toBeNull()
expect(result.winningBidEcpm).toBe(0)
expect(result.participatingBids.length).toBeGreaterThan(0)
expect(result.participatingBids.every((bid) => bid.status === 'no_bid')).toBe(true)
})
it('does not enable an adapter merely because its config entry is omitted', async () => {
engine.setConfig({ networks: [] })
const result = await engine.runAuction({
placement: 'bottom_dock_banner',
format: 'banner_dock',
})
expect(result.winner).toBeNull()
expect(result.participatingBids).toEqual([])
})
it('returns a defensive config copy', () => {
const exposedConfig = engine.getConfig()
exposedConfig.networks[0].enabled = true
expect(engine.getConfig().networks[0].enabled).toBe(false)
})
it('ignores renderer-forged impressions and clicks', () => {
const before = settlement.getRevenueStats('2026-08')
engine.recordImpression({
adId: 'forged-ad-id',
format: 'banner_dock',
network: 'ethical_ads',
earnedEcpm: 999,
})
engine.recordClick('forged-ad-id', 'ethical_ads')
const after = settlement.getRevenueStats('2026-08')
expect(after.totalImpressions).toBe(before.totalImpressions)
expect(after.totalClicks).toBe(before.totalClicks)
expect(after.totalRevenueUsd).toBe(before.totalRevenueUsd)
})
it('never grants desktop quota without server-verified completion', async () => {
await expect(engine.claimReward('forged-reward-id', 'unity_ads')).resolves.toEqual({
success: false,
tokensAdded: 0,
newTotalQuota: 0,
})
})
})
describe('AdSettlementService fail-closed ledger', () => {
const settlement = getAdSettlementService()
it('does not ship personal payout details or pretend providers are configured', () => {
expect(settlement.getPublisherAccount()).toEqual({
accountEmail: '',
beneficiaryName: '',
payoutBank: '',
payoutAccountNumber: '',
taxRegistrationNumber: '',
paypalEmail: '',
networksConfigured: 0,
})
})
it('does not synthesize revenue, fill rate, or settlement history', () => {
const stats = settlement.getRevenueStats('2026-08')
expect(stats.totalImpressions).toBe(0)
expect(stats.totalClicks).toBe(0)
expect(stats.totalCompletions).toBe(0)
expect(stats.totalRevenueUsd).toBe(0)
expect(stats.avgEcpm).toBe(0)
expect(stats.fillRatePercent).toBe(0)
expect(stats.networkBreakdown).toEqual([])
expect(stats.settlements).toEqual([])
})
it('rejects payout without an external settlement provider', () => {
expect(settlement.requestPayout('forged-settlement-id')).toEqual({
success: false,
message: 'external_settlement_not_configured',
})
})
it('ignores invalid or unattested settlement events', () => {
const before = settlement.getRevenueStats('2026-08')
settlement.recordImpression('', 10)
settlement.recordImpression('forged-network', Number.NaN)
settlement.recordImpression('forged-network', -10)
settlement.recordClick('forged-network')
settlement.recordCompletion('forged-network')
expect(settlement.getRevenueStats('2026-08')).toEqual(before)
})
})