fix(tests): enhance fetch mocks and type safety in settings and audit tests
Docker / build (push) Failing after 30s

Updated fetch mocks in settings and audit test files to improve type safety by specifying the fetch type. Adjusted handling of mock call parameters to ensure proper access to request options. This enhances test reliability and clarity in error handling scenarios.
This commit is contained in:
Denozordec
2026-08-02 19:52:18 +07:00
parent 0177f5d539
commit c14cf3953e
6 changed files with 36 additions and 40 deletions
+14 -13
View File
@@ -26,7 +26,7 @@ describe('settings telegram test', () => {
it('returns telegram API error with hint', async () => { it('returns telegram API error with hint', async () => {
vi.stubGlobal( vi.stubGlobal(
'fetch', 'fetch',
vi.fn(async () => vi.fn<typeof fetch>(async () =>
Response.json({ ok: false, description: 'Bad Request: chat not found' }), Response.json({ ok: false, description: 'Bad Request: chat not found' }),
), ),
) )
@@ -39,7 +39,7 @@ describe('settings telegram test', () => {
}) })
it('uses body overrides and falls back to db token', async () => { it('uses body overrides and falls back to db token', async () => {
const fetchMock = vi.fn(async () => Response.json({ ok: true })) const fetchMock = vi.fn<typeof fetch>(async () => Response.json({ ok: true }))
vi.stubGlobal('fetch', fetchMock) vi.stubGlobal('fetch', fetchMock)
const res = await app.inject({ const res = await app.inject({
@@ -54,9 +54,9 @@ describe('settings telegram test', () => {
const body = res.json() as { ok: boolean } const body = res.json() as { ok: boolean }
expect(body.ok).toBe(true) expect(body.ok).toBe(true)
const call = fetchMock.mock.calls[0] as [string, RequestInit] | undefined const call = fetchMock.mock.calls[0]
expect(call).toBeDefined() expect(call).toBeDefined()
const sent = JSON.parse(String(call![1].body)) as { const sent = JSON.parse(String(call![1]?.body)) as {
chat_id: string chat_id: string
message_thread_id: number message_thread_id: number
} }
@@ -65,7 +65,7 @@ describe('settings telegram test', () => {
}) })
it('uses body token when provided', async () => { it('uses body token when provided', async () => {
const fetchMock = vi.fn(async () => Response.json({ ok: true })) const fetchMock = vi.fn<typeof fetch>(async () => Response.json({ ok: true }))
vi.stubGlobal('fetch', fetchMock) vi.stubGlobal('fetch', fetchMock)
await app.inject({ await app.inject({
@@ -77,7 +77,7 @@ describe('settings telegram test', () => {
}, },
}) })
const url = String((fetchMock.mock.calls[0] as [string])[0]) const url = String(fetchMock.mock.calls[0]![0])
expect(url).toContain('botoverride-token/') expect(url).toContain('botoverride-token/')
}) })
}) })
@@ -115,7 +115,7 @@ describe('settings cfdm sync', () => {
}) })
it('requests full sync from CFDM', async () => { it('requests full sync from CFDM', async () => {
const fetchMock = vi.fn(async () => const fetchMock = vi.fn<typeof fetch>(async () =>
Response.json({ Response.json({
ok: true, ok: true,
count: 1, count: 1,
@@ -141,9 +141,9 @@ describe('settings cfdm sync', () => {
expect(res.json()).toMatchObject({ ok: true }) expect(res.json()).toMatchObject({ ok: true })
expect((res.json() as { count: number }).count).toBeGreaterThanOrEqual(1) expect((res.json() as { count: number }).count).toBeGreaterThanOrEqual(1)
const call = fetchMock.mock.calls[0] as [string, RequestInit] | undefined const call = fetchMock.mock.calls[0]
expect(call?.[0]).toBe('http://cfdm.test/api/v1/integrations/vps-tracker/sync') expect(call?.[0]).toBe('http://cfdm.test/api/v1/integrations/vps-tracker/sync')
expect((call?.[1].headers as Record<string, string>).Authorization).toBe( expect((call?.[1]?.headers as Record<string, string>).Authorization).toBe(
'Bearer shared-token', 'Bearer shared-token',
) )
}) })
@@ -154,7 +154,7 @@ describe('settings cfdm sync', () => {
integrationToken: 'shared-token', integrationToken: 'shared-token',
cfdmApiUrl: 'http://cfdm.test', cfdmApiUrl: 'http://cfdm.test',
}) })
const fetchMock = vi.fn(async () => const fetchMock = vi.fn<typeof fetch>(async () =>
Response.json({ ok: true, count: 0, bindings: [], fullSync: true }), Response.json({ ok: true, count: 0, bindings: [], fullSync: true }),
) )
vi.stubGlobal('fetch', fetchMock) vi.stubGlobal('fetch', fetchMock)
@@ -173,7 +173,7 @@ describe('settings cfdm sync', () => {
}) })
vi.stubGlobal( vi.stubGlobal(
'fetch', 'fetch',
vi.fn(async () => { vi.fn<typeof fetch>(async () => {
throw new TypeError('fetch failed') throw new TypeError('fetch failed')
}), }),
) )
@@ -211,18 +211,19 @@ describe('settings cfdm sync', () => {
id: 'cfdm', id: 'cfdm',
name: 'CFDM', name: 'CFDM',
url: 'http://192.168.100.67:6363', url: 'http://192.168.100.67:6363',
icon: 'cloud',
}, },
], ],
}, },
}) })
const fetchMock = vi.fn(async () => const fetchMock = vi.fn<typeof fetch>(async () =>
Response.json({ ok: true, count: 0, bindings: [], fullSync: true }), Response.json({ ok: true, count: 0, bindings: [], fullSync: true }),
) )
vi.stubGlobal('fetch', fetchMock) vi.stubGlobal('fetch', fetchMock)
const res = await app.inject({ method: 'POST', url: '/api/settings/cfdm/sync' }) const res = await app.inject({ method: 'POST', url: '/api/settings/cfdm/sync' })
expect(res.statusCode).toBe(200) expect(res.statusCode).toBe(200)
const call = fetchMock.mock.calls[0] as [string] | undefined const call = fetchMock.mock.calls[0]
expect(call?.[0]).toBe('https://cfdm.prod.example/api/v1/integrations/vps-tracker/sync') expect(call?.[0]).toBe('https://cfdm.prod.example/api/v1/integrations/vps-tracker/sync')
}) })
}) })
+8 -6
View File
@@ -46,7 +46,9 @@ describe('audit dual-write', () => {
}) })
it('fire-and-forgets portal ingest with vps action key', async () => { it('fire-and-forgets portal ingest with vps action key', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ accepted: 1, duplicates: 0 }))) const fetchMock = vi.fn<typeof fetch>(
async () => new Response(JSON.stringify({ accepted: 1, duplicates: 0 })),
)
vi.stubGlobal('fetch', fetchMock) vi.stubGlobal('fetch', fetchMock)
auditCreate( auditCreate(
@@ -63,15 +65,15 @@ describe('audit dual-write', () => {
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)) await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit] const [url, init] = fetchMock.mock.calls[0]!
expect(url).toBe('http://portal.test/api/v1/ingest/audit') expect(url).toBe('http://portal.test/api/v1/ingest/audit')
expect(init.method).toBe('POST') expect(init?.method).toBe('POST')
expect(init.headers).toMatchObject({ expect(init?.headers).toMatchObject({
Authorization: 'Bearer test-ingest-secret', Authorization: 'Bearer test-ingest-secret',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}) })
const body = JSON.parse(String(init.body)) as { const body = JSON.parse(String(init?.body)) as {
events: Array<{ events: Array<{
event_id: string event_id: string
source_app: string source_app: string
@@ -94,7 +96,7 @@ describe('audit dual-write', () => {
it('does not throw when portal ingest fails', () => { it('does not throw when portal ingest fails', () => {
vi.stubGlobal( vi.stubGlobal(
'fetch', 'fetch',
vi.fn(async () => { vi.fn<typeof fetch>(async () => {
throw new Error('network down') throw new Error('network down')
}), }),
) )
@@ -3,18 +3,15 @@
*/ */
import type { BillmanagerSyncAccount } from './context.js' import type { BillmanagerSyncAccount } from './context.js'
import type { SyncFromBillmanagerOptions, SyncFromBillmanagerResult } from './sync.js' import type { SyncFromBillmanagerOptions } from './sync.js'
import { billmanagerAdapter } from '../providers/billmanager-adapter.js' import { billmanagerAdapter } from '../providers/billmanager-adapter.js'
import { runAccountSync } from '../providers/sync-job.js' import { runAccountSync, type RunAccountSyncResult } from '../providers/sync-job.js'
export interface RunBillmanagerAccountSyncResult extends SyncFromBillmanagerResult { export type RunBillmanagerAccountSyncResult = RunAccountSyncResult
ok: true
logId: string
}
export async function runBillmanagerAccountSync( export async function runBillmanagerAccountSync(
account: BillmanagerSyncAccount, account: BillmanagerSyncAccount,
opts: SyncFromBillmanagerOptions = {}, opts: SyncFromBillmanagerOptions = {},
): Promise<RunBillmanagerAccountSyncResult> { ): Promise<RunAccountSyncResult> {
return runAccountSync(billmanagerAdapter, account, opts) return runAccountSync(billmanagerAdapter, account, opts)
} }
+6 -1
View File
@@ -9,7 +9,12 @@ export interface SyncResult {
vpsCount: number vpsCount: number
paymentsCount: number paymentsCount: number
tariffsCount: number tariffsCount: number
balance: { balance?: number; currency?: string } | null balance: {
balance?: number
currency?: string
enoughmoneyto?: string
realbalance?: string
} | null
syncSummary: SyncSummary syncSummary: SyncSummary
newTariffs: { name: string; price: string; providerId: string }[] newTariffs: { name: string; price: string; providerId: string }[]
} }
+1 -11
View File
@@ -30,20 +30,10 @@ function roundRate(n: number): number {
return Math.round(n * 100) / 100 return Math.round(n * 100) / 100
} }
function mapBillingCycle(cycle: string | undefined): {
tariffType: string
dailyRate: number | null
monthlyRate: number | null
} {
const c = String(cycle ?? '').toLowerCase()
return { tariffType: c.includes('day') ? 'daily' : 'monthly', dailyRate: null, monthlyRate: null }
}
function ratesFromTotal( function ratesFromTotal(
total: number, total: number,
cycle: string | undefined, cycle: string | undefined,
): { tariffType: string; dailyRate: number | null; monthlyRate: number | null } { ): { tariffType: string; dailyRate: number | null; monthlyRate: number | null } {
const base = mapBillingCycle(cycle)
const c = String(cycle ?? '').toLowerCase() const c = String(cycle ?? '').toLowerCase()
if (c.includes('day')) { if (c.includes('day')) {
return { tariffType: 'daily', dailyRate: roundRate(total), monthlyRate: null } return { tariffType: 'daily', dailyRate: roundRate(total), monthlyRate: null }
@@ -253,7 +243,7 @@ export function mapVpsRecordToVps(
currency, currency,
dailyRate: rates.dailyRate, dailyRate: rates.dailyRate,
monthlyRate: rates.monthlyRate, monthlyRate: rates.monthlyRate,
createdAt: dateToIso(detail.date_created), createdAt: dateToIso(serviceDetail?.date_created),
paidUntil: dateToIso(detail.next_due ?? service.next_due), paidUntil: dateToIso(detail.next_due ?? service.next_due),
notes: label ? `${label} [veesp-${externalId}]` : `veesp-${externalId}`, notes: label ? `${label} [veesp-${externalId}]` : `veesp-${externalId}`,
} }
+3 -2
View File
@@ -62,6 +62,7 @@ export interface VeespVmDetail extends VeespVmListItem {
} }
export interface VeespIpItem { export interface VeespIpItem {
id?: string | number
ip?: string ip?: string
address?: string address?: string
ipaddress?: string ipaddress?: string
@@ -207,7 +208,7 @@ export function isVpsService(service: VeespServiceListItem, vpsCategoryIds?: Set
return false return false
} }
function unwrapKeyedList<T extends Record<string, unknown>>(raw: unknown): T[] { function unwrapKeyedList<T>(raw: unknown): T[] {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return [] if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []
const out: T[] = [] const out: T[] = []
for (const [key, item] of Object.entries(raw as Record<string, unknown>)) { for (const [key, item] of Object.entries(raw as Record<string, unknown>)) {
@@ -218,7 +219,7 @@ function unwrapKeyedList<T extends Record<string, unknown>>(raw: unknown): T[] {
} }
if (typeof item !== 'object') continue if (typeof item !== 'object') continue
const row = item as Record<string, unknown> const row = item as Record<string, unknown>
out.push({ ...row, id: row.id ?? key } as T) out.push({ ...row, id: row.id ?? key } as unknown as T)
} }
return out return out
} }