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.
277 lines
9.3 KiB
TypeScript
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)
|
|
})
|
|
})
|