Files
vps-tracker/apps/api/src/services/fourvps/sync.test.ts
T
DenozordecandClaude Opus 5.5 78f413324a fix(sync): не стирать тарифы при ошибке загрузки прайс-листа
Ошибка запроса тарифов превращалась в пустой список: active_tariffs
удалялись, а следующий успешный синк объявлял все тарифы новыми.
Пустой ответ теперь оставляет старый список, замена идёт в транзакции.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:32:50 +07:00

175 lines
5.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { closeDb, getSqlite, runWithSpaceAsync } from '@cfdm/db'
import { resetTestDb } from '@cfdm/db/test-setup'
import { providerAccountsRepository } from '@cfdm/db/repositories/provider-accounts'
import { syncFromFourvps } from './sync.js'
import type { FourvpsSyncAccount } from './context.js'
vi.mock('./operations.js', () => ({
fetchMyServers: vi.fn(),
fetchUserBalance: vi.fn(),
fetchTarifList: vi.fn(),
fetchDcList: vi.fn(),
}))
import {
fetchDcList,
fetchMyServers,
fetchTarifList,
fetchUserBalance,
} from './operations.js'
const account: FourvpsSyncAccount = {
id: 'acc-4vps',
spaceId: 'space-main',
providerId: 'prov-4vps',
name: '4VPS Account',
panelUrl: '',
currency: 'RUB',
billingMode: 'monthly',
notes: '',
apiType: '4vps',
apiBaseUrl: 'https://4vps.su/api',
apiCredentials: '1:secret',
panelId: 1,
apiKey: 'secret',
balanceApi: null,
balanceCurrency: null,
balanceUpdatedAt: null,
enoughmoneyto: '',
balanceAlertBelow: null,
}
describe('syncFromFourvps', () => {
beforeEach(() => {
resetTestDb()
getSqlite()
.prepare(
`INSERT INTO providers (id, name, apiType, apiBaseUrl) VALUES ('prov-4vps', '4VPS', '4vps', 'https://4vps.su/api')`,
)
.run()
providerAccountsRepository.create({
id: 'acc-4vps',
providerId: 'prov-4vps',
name: '4VPS Account',
apiCredentials: '1:secret',
})
vi.mocked(fetchMyServers).mockResolvedValue([
{
id: 100,
name: 'srv1',
price: 420,
dc: 1,
image: 'Debian 11',
mem: 2,
cpu: 2,
disk: 20,
ipv4: '1.2.3.4',
status: 'active',
tname: 'cx01',
time: 1664275501,
expired: 1666781101,
},
])
vi.mocked(fetchUserBalance).mockResolvedValue({ balance: 77825, currency: 'RUB' })
vi.mocked(fetchDcList).mockResolvedValue(
new Map([[1, { id: 1, dc_name: 'AE DC1', flag: 'ae' }]]),
)
vi.mocked(fetchTarifList).mockResolvedValue([
{
externalId: '13',
datacenterKey: '1',
datacenterName: 'AE DC1',
name: 'AE-cx01',
desc: '',
vcpu: 1,
ramGb: 1,
diskGb: 10,
diskType: 'NVME',
virtualization: 'KVM',
channel: '1Gbit/s',
location: 'AE DC1',
country: 'AE',
cpuModel: 'E5',
orderAvailable: true,
price: '420',
},
])
})
afterEach(() => {
closeDb()
vi.clearAllMocks()
})
it('upserts VPS, balance and tariffs', async () => {
const result = await syncFromFourvps(account)
expect(result.vpsCount).toBe(1)
expect(result.paymentsCount).toBe(0)
expect(result.tariffsCount).toBe(1)
expect(result.balance?.balance).toBe(77825)
const vps = getSqlite()
.prepare(`SELECT id, ip, country, city, datacenter FROM vps WHERE providerAccountId = ?`)
.get('acc-4vps') as {
id: string
ip: string
country: string
city: string
datacenter: string
}
expect(vps.id).toBe('vps-4vps-acc-4vps-100')
expect(vps.ip).toBe('1.2.3.4')
expect(vps.country).toBe('ОАЭ')
expect(vps.city).toBe('')
expect(vps.datacenter).toBe('AE DC1')
const tariffs = getSqlite()
.prepare(`SELECT COUNT(*) as c FROM active_tariffs WHERE providerAccountId = ?`)
.get('acc-4vps') as { c: number }
expect(tariffs.c).toBe(1)
const bal = getSqlite()
.prepare(`SELECT balance_api FROM provider_accounts WHERE id = ?`)
.get('acc-4vps') as { balance_api: number }
expect(bal.balance_api).toBe(77825)
})
it('writes synced rows into the current space', async () => {
getSqlite()
.prepare(`INSERT INTO spaces (id, name, slug, kind, createdAt) VALUES ('space-x', 'X', 'x', 'personal', '2026-01-01')`)
.run()
await runWithSpaceAsync('space-x', () => syncFromFourvps(account))
const rows = getSqlite()
.prepare(`SELECT spaceId FROM vps UNION ALL SELECT spaceId FROM active_tariffs`)
.all() as { spaceId: string }[]
expect(rows.length).toBe(2)
expect(rows.every((r) => r.spaceId === 'space-x')).toBe(true)
})
it('does not merge a server into another by note prefix or empty IP', async () => {
getSqlite()
.prepare(
`INSERT INTO vps (id, spaceId, ip, providerId, providerAccountId, notes) VALUES ('manual-12', 'space-main', '', 'prov-4vps', 'acc-4vps', '4vps-1000')`,
)
.run()
vi.mocked(fetchMyServers).mockResolvedValue([
{ ...(await fetchMyServers('', ''))[0]!, id: 100, ipv4: '' },
])
await syncFromFourvps(account)
const ids = (getSqlite().prepare(`SELECT id FROM vps ORDER BY id`).all() as { id: string }[]).map((r) => r.id)
expect(ids).toEqual(['manual-12', 'vps-4vps-acc-4vps-100'])
})
it('keeps tariffs when the tariff fetch fails and does not re-announce them', async () => {
await syncFromFourvps(account)
vi.mocked(fetchTarifList).mockRejectedValueOnce(new Error('timeout'))
await syncFromFourvps(account)
const count = getSqlite().prepare(`SELECT COUNT(*) as c FROM active_tariffs`).get() as { c: number }
expect(count.c).toBe(1)
const third = await syncFromFourvps(account)
expect(third.newTariffs).toEqual([])
})
})