import * as fs from 'node:fs' import * as http from 'node:http' import * as os from 'node:os' import * as path from 'node:path' import { sql } from 'kysely' import { nanoid } from 'nanoid' import { zeroAddress, type Address } from 'viem' import * as TestCli from '../../../test/cli.js' import * as Runtime from '../../../test/runtime.js' import * as Analytics from '../../analytics/Analytics.js' import * as Db from '../../db/Db.js' import * as core_EarnVaults from '../../db/tables/earnVaults.js' import * as RewardAccounts from '../../db/tables/rewardAccounts.js' import * as RewardCampaigns from '../../db/tables/rewardCampaigns.js' import * as RewardEligibilityAssociations from '../../db/tables/rewardEligibilityAssociations.js' import * as RewardRuns from '../../db/tables/rewardRuns.js' import type * as Campaigns from '../../internal/rewards/Campaigns.js' afterEach(() => vi.unstubAllGlobals()) describe('admin', () => { describe('import-reward-eligibility', () => { test('reviews a normalized dry-run and applies only after confirmation', async () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'tempo-reward-import-')) const file = path.join(directory, 'associations.json') fs.writeFileSync( file, JSON.stringify([ { chainId: 4217, transactionHash: `0x${'33'.repeat(32)}`, vaultAddress: `0x${'Aa'.repeat(20)}`, walletAddress: `0x${'Bb'.repeat(20)}`, }, ]), ) try { const dryRun = await TestCli.run(['admin', 'import-reward-eligibility', '--file', file], { DATABASE_URL: undefined, }) expect(dryRun).toMatchInlineSnapshot(` { "code": 0, "envelope": { "data": { "applied": false, "associations": [ { "chainId": 4217, "transactionHash": "0x3333333333333333333333333333333333333333333333333333333333333333", "vaultAddress": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "walletAddress": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", }, ], }, "ok": true, }, } `) const schema = `t_${nanoid()}` expect((await TestCli.run(['admin', 'migrate', '--db-schema', schema])).code).toBe(0) const apply = () => TestCli.run([ 'admin', 'import-reward-eligibility', '--apply', '--db-schema', schema, '--file', file, ]) expect((await apply()).envelope).toEqual({ data: { applied: true, created: 1, replayed: 0 }, ok: true, }) expect((await apply()).envelope).toEqual({ data: { applied: true, created: 0, replayed: 1 }, ok: true, }) } finally { fs.rmSync(directory, { force: true, recursive: true }) } }) test('rejects invalid input before connecting', async () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'tempo-reward-import-')) const file = path.join(directory, 'associations.json') fs.writeFileSync(file, JSON.stringify([{ chainId: 0 }])) try { const result = await TestCli.run( ['admin', 'import-reward-eligibility', '--apply', '--file', file], { DATABASE_URL: undefined }, ) expect(result).toMatchObject({ code: 1, envelope: { error: { code: 'invalid_reward_eligibility_import' }, ok: false }, }) } finally { fs.rmSync(directory, { force: true, recursive: true }) } }) test('rolls back every association when one import row fails', async () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'tempo-reward-import-')) const file = path.join(directory, 'associations.json') const schema = `t_${nanoid().replaceAll('-', '_')}` const blockedWallet = `0x${'cc'.repeat(20)}` fs.writeFileSync( file, JSON.stringify([ { chainId: 4217, vaultAddress: `0x${'aa'.repeat(20)}`, walletAddress: `0x${'bb'.repeat(20)}`, }, { chainId: 4217, vaultAddress: `0x${'aa'.repeat(20)}`, walletAddress: blockedWallet, }, ]), ) const db = Db.postgres({ connectionString: Runtime.postgresUrl, schema }) try { await db.migrate() await db.kysely.schema .alterTable('rewardEligibilityAssociations') .addCheckConstraint( 'reward_eligibility_import_test', sql`wallet_address <> ${sql.lit(blockedWallet)}`, ) .execute() const result = await TestCli.run([ 'admin', 'import-reward-eligibility', '--apply', '--db-schema', schema, '--file', file, ]) const count = await db.kysely .selectFrom('reward_eligibility_associations') .select((eb) => eb.fn.countAll().as('count')) .executeTakeFirstOrThrow() expect({ count: count.count, result }).toMatchObject({ count: '0', result: { code: 1, envelope: { error: { code: 'reward_eligibility_import_failed' }, ok: false }, }, }) } finally { await db.close() fs.rmSync(directory, { force: true, recursive: true }) } }) }) describe('migrate', () => { test('behavior: applies migrations', async () => { const { code, envelope } = await TestCli.run([ 'admin', 'migrate', '--db-schema', `t_${nanoid()}`, ]) expect(code).toBe(0) expect(envelope).toMatchInlineSnapshot(` { "data": { "migrated": true, }, "ok": true, } `) }) test('behavior: applies migrations from DATABASE_URL without a schema option', async () => { const { code, envelope } = await TestCli.run(['admin', 'migrate']) expect(code).toBe(0) expect(envelope).toEqual({ data: { migrated: true }, ok: true }) }) test('behavior: is idempotent across runs', async () => { const schema = `t_${nanoid()}` expect((await TestCli.run(['admin', 'migrate', '--db-schema', schema])).code).toBe(0) const { code, envelope } = await TestCli.run(['admin', 'migrate', '--db-schema', schema]) expect(code).toBe(0) expect(envelope).toEqual({ data: { migrated: true }, ok: true }) }) test('error: a missing connection string exits 1', async () => { const { code, envelope } = await TestCli.run(['admin', 'migrate'], { DATABASE_URL: undefined, }) expect(code).toBe(1) expect(envelope).toMatchInlineSnapshot(` { "error": { "code": "missing_database_url", "message": "Set --database-url or DATABASE_URL to the Postgres connection string.", }, "ok": false, } `) }) test('error: a migration failure exits 1', async () => { const { code, envelope } = await TestCli.run( [ 'admin', 'migrate', '--database-url', 'postgres://postgres:postgres@127.0.0.1:1/tempo-api', '--db-schema', `t_${nanoid()}`, ], { DATABASE_URL: undefined }, ) expect(code).toBe(1) expect(envelope.error.code).toBe('migrate_failed') }) test('behavior: applies ClickHouse analytics migrations when configured', async () => { const fetch = vi .fn() .mockResolvedValue(new Response('', { status: 200 })) vi.stubGlobal('fetch', fetch) const { code, envelope } = await TestCli.run( ['admin', 'migrate', '--db-schema', `t_${nanoid()}`], { CLICKHOUSE_DATABASE: 'tempo_api', CLICKHOUSE_MIGRATE_PASSWORD: 'ddl-secret', CLICKHOUSE_MIGRATE_USER: 'ddl', CLICKHOUSE_URL: 'https://clickhouse.test', }, ) expect(code).toBe(0) expect(envelope).toEqual({ data: { analytics: true, migrated: true }, ok: true }) const url = new URL(String(fetch.mock.calls[0]![0])) expect(url.searchParams.get('database')).toBe('tempo_api') const bodies = fetch.mock.calls.flatMap((call) => typeof call[1]?.body === 'string' ? [call[1].body] : [], ) for (const migration of Analytics.migrations) expect(bodies).toContain(migration.sql) }) test('behavior: skips analytics migrations when ClickHouse env is unset or empty', async () => { const fetch = vi.fn() vi.stubGlobal('fetch', fetch) const { code, envelope } = await TestCli.run( ['admin', 'migrate', '--db-schema', `t_${nanoid()}`], { CLICKHOUSE_DATABASE: '', CLICKHOUSE_MIGRATE_PASSWORD: '', CLICKHOUSE_MIGRATE_USER: '', CLICKHOUSE_URL: '', }, ) expect(code).toBe(0) expect(envelope).toEqual({ data: { migrated: true }, ok: true }) expect(fetch).not.toHaveBeenCalled() }) test('error: partial ClickHouse config exits 1', async () => { const { code, envelope } = await TestCli.run(['admin', 'migrate'], { CLICKHOUSE_MIGRATE_PASSWORD: 'ddl-secret', }) expect(code).toBe(1) expect(envelope).toEqual({ error: { code: 'missing_clickhouse_config', message: 'ClickHouse analytics migration is partially configured; set CLICKHOUSE_DATABASE, CLICKHOUSE_MIGRATE_USER, CLICKHOUSE_URL.', }, ok: false, }) }) test('error: a ClickHouse migration failure exits 1', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue(new Response('denied', { status: 403 })), ) const { code, envelope } = await TestCli.run([ 'admin', 'migrate', '--clickhouse-database', 'tempo_api', '--clickhouse-migrate-password', 'ddl-secret', '--clickhouse-migrate-user', 'ddl', '--clickhouse-url', 'https://clickhouse.test', '--db-schema', `t_${nanoid()}`, ]) expect(code).toBe(1) expect(envelope.error.code).toBe('migrate_failed') }) }) describe('audit-reward-accounts', () => { test('reports missed deposits, drift, and unprojected registrations, then resets only missed deposits', async () => { // Public schema: `RewardEligibilityAssociations.list` locks via raw SQL that schema-scoped // pools cannot qualify. Isolation comes from the unique vault address. const db = Db.postgres({ connectionString: Runtime.postgresUrl }) const tidx = await tidxServer([ // Wallet A's first deposit is stored; its second (block 105) never reached the projection. ...deposit({ block: 100, log: 1, recipient: walletA, assets: '500000' }), ...deposit({ block: 105, log: 2, recipient: walletA, assets: '400000' }), ...deposit({ block: 103, log: 4, recipient: walletB, assets: '250000' }), // Same block as the committed cursor but a later log: the delivered run never applied it. ...deposit({ block: 110, log: 7, recipient: walletA, assets: '50000' }), ]) try { await db.migrate() await seedCampaign(db, { blockNumber: 110, logIndex: 5, transactionIndex: 0 }) for (const walletAddress of [walletA, walletB, walletC]) await RewardEligibilityAssociations.upsert(db, { chainId: 4217, vaultAddress: rewardVault, walletAddress, }) await RewardAccounts.replace(db, { accounts: [ account({ allocated: '500000', recipient: walletA, shares: shares('500000') }), { ...account({ allocated: '250000', recipient: walletB, shares: shares('250000') }), publicEarnShares: shares('300000'), }, ], chainId: 4217, vaultAddress: rewardVault, }) const audit = (...argv: string[]) => TestCli.run([ 'admin', 'audit-reward-accounts', '--tidx-url', tidx.url, '--vault-address', rewardVault, ...argv, ]) const dryRun = await audit() expect(dryRun).toMatchInlineSnapshot(` { "code": 0, "envelope": { "data": { "allocatedPrincipalAssets": { "expected": "1150000", "stored": "750000", }, "applied": false, "campaign": { "chainId": 4217, "eventCursor": { "blockNumber": 110, "logIndex": 5, "transactionIndex": 0, }, "paused": false, "perUserPrincipalCapAssets": "1000000", "totalPrincipalCapAssets": "10000000", "vaultAddress": "0xa7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7", }, "drift": [ { "allocatedPrincipalAssets": { "expected": "250000", "stored": "250000", }, "publicEarnShares": { "expected": "250000000000000000", "stored": "300000000000000000", }, "qualifiedEarnShares": { "expected": "250000000000000000", "stored": "250000000000000000", }, "recipient": "0x2222222222222222222222222222222222222222", }, ], "missingDeposits": [ { "allocatedPrincipalAssets": { "expected": "900000", "stored": "500000", }, "publicEarnShares": { "expected": "900000000000000000", "stored": "500000000000000000", }, "qualifiedEarnShares": { "expected": "900000000000000000", "stored": "500000000000000000", }, "recipient": "0x1111111111111111111111111111111111111111", }, ], "registrations": 3, "storedAccounts": 2, "unprojected": [ "0x3333333333333333333333333333333333333333", ], }, "ok": true, }, } `) expect(tidx.queries.every((query) => query.includes(`'${walletC}'`))).toBe(true) await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletA, vaultAddress: rewardVault }), ).resolves.toMatchObject({ allocatedPrincipalAssets: '500000' }) const applied = await audit('--apply') expect(applied.code).toBe(0) expect(applied.envelope.data).toMatchObject({ applied: true, reset: 1 }) await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletA, vaultAddress: rewardVault }), ).resolves.toMatchObject({ allocatedPrincipalAssets: '0', eligibilityRegisteredAt: null, lots: [], publicEarnShares: shares('500000'), qualifiedEarnShares: '0', }) // Drift without an allocation shortfall is reported, never reset. await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletB, vaultAddress: rewardVault }), ).resolves.toMatchObject({ allocatedPrincipalAssets: '250000', lots: [{}] }) } finally { await tidx.close() } }) test('error: a campaign without a delivered run cannot be audited', async () => { const schema = `t_${nanoid().replaceAll('-', '_')}` const db = Db.postgres({ connectionString: Runtime.postgresUrl, schema }) await db.migrate() await seedCampaign(db) const { code, envelope } = await TestCli.run([ 'admin', 'audit-reward-accounts', '--db-schema', schema, '--tidx-url', 'http://127.0.0.1:1', '--vault-address', rewardVault, ]) expect(code).toBe(1) expect(envelope).toMatchInlineSnapshot(` { "error": { "code": "reward_audit_unavailable", "message": "Reward campaign has not delivered a run yet.", }, "ok": false, } `) }) test('error: rejects a malformed vault address', async () => { const { code, envelope } = await TestCli.run([ 'admin', 'audit-reward-accounts', '--vault-address', '0x1234', ]) expect(code).toBe(1) expect(envelope.error.code).toBe('invalid_vault_address') }) }) describe('credit-reward-accounts', () => { test('reviews, refuses while unpaused, applies once, and replays as a no-op', async () => { const schema = `t_${nanoid().replaceAll('-', '_')}` const db = Db.postgres({ connectionString: Runtime.postgresUrl, schema }) await db.migrate() await seedCampaign(db, { blockNumber: 110, logIndex: 5, transactionIndex: 0 }) await RewardAccounts.replace(db, { accounts: [ { ...account({ allocated: '500000', recipient: walletA, shares: shares('500000') }), pendingRewardAssets: '1500', }, account({ allocated: '250000', recipient: walletB, shares: shares('250000') }), ], chainId: 4217, vaultAddress: rewardVault, }) const file = creditFile([ { assets: '34455381', recipient: walletA }, { assets: '2000000', recipient: walletB }, ]) const credit = (...argv: string[]) => TestCli.run([ 'admin', 'credit-reward-accounts', '--db-schema', schema, '--file', file, '--reference', 'boost-shortfall-test', '--vault-address', rewardVault, ...argv, ]) const dryRun = await credit() expect(dryRun).toMatchInlineSnapshot(` { "code": 0, "envelope": { "data": { "applied": false, "appliedAssets": "0", "blockers": [ "Campaign is not paused.", ], "latestRunPhase": null, "lines": [ { "assets": "34455381", "recipient": "0x1111111111111111111111111111111111111111", "status": "pending", }, { "assets": "2000000", "recipient": "0x2222222222222222222222222222222222222222", "status": "pending", }, ], "missing": [], "paused": false, "pendingAssets": "36455381", "ready": false, }, "ok": true, }, } `) // Applying while settlement may still commit would lose the credit; refuse. const refused = await credit('--apply') expect(refused.code).toBe(1) expect(refused.envelope).toMatchInlineSnapshot(` { "error": { "code": "reward_credit_unavailable", "message": "Cannot apply credits: Campaign is not paused.", }, "ok": false, } `) await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletA, vaultAddress: rewardVault }), ).resolves.toMatchObject({ pendingRewardAssets: '1500' }) await RewardCampaigns.setPaused(db, { chainId: 4217, paused: true, vaultAddress: rewardVault, }) const applied = await credit('--apply') expect(applied.code).toBe(0) expect(applied.envelope.data).toMatchObject({ applied: true, credited: 2, creditedAssets: '36455381', ready: true, replayed: 0, }) // Credits add to existing pending rewards instead of replacing them. await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletA, vaultAddress: rewardVault }), ).resolves.toMatchObject({ pendingRewardAssets: '34456881' }) await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletB, vaultAddress: rewardVault }), ).resolves.toMatchObject({ pendingRewardAssets: '2000000' }) const replayed = await credit('--apply') expect(replayed.code).toBe(0) expect(replayed.envelope.data).toMatchObject({ applied: true, appliedAssets: '36455381', credited: 0, lines: [{ status: 'applied' }, { status: 'applied' }], pendingAssets: '0', replayed: 2, }) await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletA, vaultAddress: rewardVault }), ).resolves.toMatchObject({ pendingRewardAssets: '34456881' }) }) test('reports recipients without accounts and refuses to apply', async () => { const schema = `t_${nanoid().replaceAll('-', '_')}` const db = Db.postgres({ connectionString: Runtime.postgresUrl, schema }) await db.migrate() await seedCampaign(db) await RewardCampaigns.setPaused(db, { chainId: 4217, paused: true, vaultAddress: rewardVault, }) await RewardAccounts.replace(db, { accounts: [account({ allocated: '500000', recipient: walletA, shares: shares('500000') })], chainId: 4217, vaultAddress: rewardVault, }) const file = creditFile([ { assets: '100', recipient: walletA }, { assets: '200', recipient: walletC }, ]) const { code, envelope } = await TestCli.run([ 'admin', 'credit-reward-accounts', '--apply', '--db-schema', schema, '--file', file, '--reference', 'missing-test', '--vault-address', rewardVault, ]) expect(code).toBe(1) expect(envelope).toMatchInlineSnapshot(` { "error": { "code": "reward_credit_unavailable", "message": "Cannot apply credits: 1 recipient(s) have no reward account.", }, "ok": false, } `) // A partial batch must not write anything. await expect( RewardAccounts.get(db, { chainId: 4217, recipient: walletA, vaultAddress: rewardVault }), ).resolves.toMatchObject({ pendingRewardAssets: '0' }) }) test('refuses while a run is in flight', async () => { const schema = `t_${nanoid().replaceAll('-', '_')}` const db = Db.postgres({ connectionString: Runtime.postgresUrl, schema }) await db.migrate() await seedCampaign(db) await RewardCampaigns.setPaused(db, { chainId: 4217, paused: true, vaultAddress: rewardVault, }) await RewardAccounts.replace(db, { accounts: [account({ allocated: '500000', recipient: walletA, shares: shares('500000') })], chainId: 4217, vaultAddress: rewardVault, }) const campaign = await RewardCampaigns.get(db, { chainId: 4217, vaultAddress: rewardVault }) if (!campaign) throw new Error('campaign missing') await RewardRuns.ensure(db, { chainId: 4217, config: campaign.config, endsAt: Number(campaign.deliveredThrough) + 300, startsAfter: Number(campaign.deliveredThrough), vaultAddress: rewardVault, }) const file = creditFile([{ assets: '100', recipient: walletA }]) const { code, envelope } = await TestCli.run([ 'admin', 'credit-reward-accounts', '--apply', '--db-schema', schema, '--file', file, '--reference', 'in-flight-test', '--vault-address', rewardVault, ]) expect(code).toBe(1) expect(envelope.error.message).toMatchInlineSnapshot( `"Cannot apply credits: Latest run is still in phase "indexing"."`, ) }) test('error: rejects a malformed credit file', async () => { const file = creditFile([{ assets: '0', recipient: walletA }]) const { code, envelope } = await TestCli.run([ 'admin', 'credit-reward-accounts', '--file', file, '--reference', 'bad', '--vault-address', rewardVault, ]) expect(code).toBe(1) expect(envelope.error.code).toBe('invalid_reward_credit_batch') }) }) }) function creditFile(credits: readonly { assets: string; recipient: Address }[]) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'tempo-reward-credit-')) const file = path.join(directory, 'credits.json') fs.writeFileSync(file, JSON.stringify(credits)) return file } const rewardVault = `0x${'a7'.repeat(20)}` as const const rewardShare = `0x${'ee'.repeat(20)}` as const const walletA = `0x${'11'.repeat(20)}` as const const walletB = `0x${'22'.repeat(20)}` as const const walletC = `0x${'33'.repeat(20)}` as const /** One EarnShare unit (18 decimals) per base-asset unit (6 decimals). */ function shares(assets: string) { return (BigInt(assets) * 10n ** 12n).toString() } function account(options: { allocated: string; recipient: Address; shares: string }) { return { accrualRemainder: '0', allocatedPrincipalAssets: options.allocated, cumulativeEntitlement: '0', cumulativePaid: '0', deferral: null, eligibilityRegisteredAt: null, lots: [ { allocatedPrincipalAssets: options.allocated, depositedAssets: options.allocated, depositedEarnShares: options.shares, event: { blockNumber: 100, logIndex: 1, transactionIndex: 0 }, remainingEarnShares: options.shares, }, ], pendingRewardAssets: '0', publicEarnShares: options.shares, qualifiedEarnShares: options.shares, recipient: options.recipient, registrationOrder: '1', updatedAt: new Date(0).toISOString(), } } async function seedCampaign(db: Db.Db, eventCursor?: Campaigns.EventCursor) { const now = 1_700_000_000 await core_EarnVaults.upsert(db, { chainId: 4217, description: null, label: 'Audit test vault', privateInputTokens: [], privateOutputTokens: [], vaultAddress: rewardVault, zones: [], }) const campaign = await RewardCampaigns.upsert(db, { assetAddress: `0x${'cc'.repeat(20)}`, assetDecimals: 6, chainId: 4217, config: { boostRewards: { endTimestamp: now + 1_200, excludedAddresses: [], funding: { wallet: `0x${'dd'.repeat(20)}` }, intervalSeconds: 300, perUserPrincipalCapAssets: '1000000', startTimestamp: now, targetAnnualRateBps: 700, totalPrincipalCapAssets: '10000000', treasury: `0x${'bb'.repeat(20)}`, }, } satisfies Campaigns.Config, earnShareAddress: rewardShare, earnShareDecimals: 18, now, vaultAddress: rewardVault, }) if (!eventCursor) return const delivered = Number(campaign.deliveredThrough) await RewardCampaigns.advance(db, { chainId: 4217, deliveredThrough: delivered + 300, eventCursor, expectedDeliveredThrough: delivered, vaultAddress: rewardVault, }) } type EventRow = readonly [ kind: string, block_num: number, tx_idx: number, log_idx: number, block_timestamp: string, sender: string, recipient: string, amount: string, assets: string, earn_shares: string, ] /** One vault deposit as TIDX surfaces it: the EarnShare mint transfer, then the `Deposited` log. */ function deposit(options: { assets: string block: number log: number recipient: Address }): readonly EventRow[] { const timestamp = '2026-09-01 00:00:00' return [ [ 'transfer', options.block, 0, options.log - 1, timestamp, zeroAddress, options.recipient, shares(options.assets), '0', '0', ], [ 'deposit', options.block, 0, options.log, timestamp, '', options.recipient, '0', options.assets, shares(options.assets), ], ] } /** Serves the TIDX `/query` contract from fixed rows, filtered and ordered like the real UNION query. */ async function tidxServer(rows: readonly EventRow[]) { const queries: string[] = [] const columns = [ 'kind', 'block_num', 'tx_idx', 'log_idx', 'block_timestamp', 'sender', 'recipient', 'amount', 'assets', 'earn_shares', ] const server = http.createServer((request, response) => { const sql = new URL(request.url ?? '/', 'http://127.0.0.1').searchParams.get('sql') ?? '' queries.push(sql) const matched = rows .filter((row) => sql.includes(`'${row[6].toLowerCase()}'`)) .toSorted((left, right) => left[1] - right[1] || left[2] - right[2] || left[3] - right[3]) response.writeHead(200, { 'Content-Type': 'application/json' }) response.end( JSON.stringify({ columns, engine: 'clickhouse', ok: true, row_count: matched.length, rows: matched, }), ) }) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const address = server.address() if (!address || typeof address === 'string') throw new Error('Missing test server address.') return { close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())), ), queries, url: `http://127.0.0.1:${address.port}`, } }