d3ro-voice/apps/desktop/tests/unit/AdMediationEngine.spec.ts
2026-08-29 18:33:45 +09:00

188 lines
6.6 KiB
TypeScript

import { beforeEach, describe, expect, it } 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()],
['Direct sponsor', new DirectHouseSponsorAdapter()],
] 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('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)
})
})