Добавить apiLogin из credentials, сводку и health-индикаторы на /accounts, фильтры, раздельную форму логина и пароля, безопасное удаление с 409 и тесты API/repository. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { closeDb } from '@cfdm/db'
|
||||
import { resetTestDb, seedTestProvider } from '@cfdm/db/test-setup'
|
||||
import { providerAccountsRepository } from '@cfdm/db/repositories/provider-accounts'
|
||||
import { buildApp } from '../index.js'
|
||||
import { getSqlite } from '@cfdm/db'
|
||||
|
||||
describe('provider-accounts routes', () => {
|
||||
let app: Awaited<ReturnType<typeof buildApp>>
|
||||
|
||||
beforeEach(async () => {
|
||||
resetTestDb()
|
||||
seedTestProvider()
|
||||
app = await buildApp()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close()
|
||||
closeDb()
|
||||
})
|
||||
|
||||
it('creates account and returns apiLogin', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/provider-accounts',
|
||||
payload: {
|
||||
providerId: 'prov-1',
|
||||
name: 'Primary',
|
||||
apiCredentials: 'login:password',
|
||||
billingMode: 'monthly',
|
||||
},
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
const body = res.json() as { apiLogin?: string; apiCredentialsSet?: boolean }
|
||||
expect(body.apiLogin).toBe('login')
|
||||
expect(body.apiCredentialsSet).toBe(true)
|
||||
})
|
||||
|
||||
it('returns 409 when deleting account with VPS', async () => {
|
||||
providerAccountsRepository.create({
|
||||
id: 'acc-del',
|
||||
providerId: 'prov-1',
|
||||
name: 'Bound',
|
||||
})
|
||||
getSqlite()
|
||||
.prepare(
|
||||
`INSERT INTO vps (id, ip, providerId, providerAccountId, status) VALUES ('vps-x', '2.2.2.2', 'prov-1', 'acc-del', 'active')`,
|
||||
)
|
||||
.run()
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/provider-accounts/acc-del',
|
||||
})
|
||||
expect(res.statusCode).toBe(409)
|
||||
const body = res.json() as { error?: { code?: string; dependencies?: { vps?: number } } }
|
||||
expect(body.error?.code).toBe('CONFLICT')
|
||||
expect(body.error?.dependencies?.vps).toBe(1)
|
||||
})
|
||||
|
||||
it('deletes account without dependencies', async () => {
|
||||
providerAccountsRepository.create({
|
||||
id: 'acc-free',
|
||||
providerId: 'prov-1',
|
||||
name: 'Free',
|
||||
})
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/provider-accounts/acc-free',
|
||||
})
|
||||
expect(res.statusCode).toBe(204)
|
||||
expect(providerAccountsRepository.get('acc-free')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,16 @@ import type { FastifyPluginAsync } from 'fastify'
|
||||
import { providerAccountsRepository } from '@cfdm/db/repositories/provider-accounts'
|
||||
import { providerAccountSchema } from '@cfdm/shared/contracts/provider-account'
|
||||
|
||||
function formatDependencyMessage(deps: ReturnType<typeof providerAccountsRepository.getDependencyCounts>): string {
|
||||
const parts: string[] = []
|
||||
if (deps.vps) parts.push(`VPS: ${deps.vps}`)
|
||||
if (deps.payments) parts.push(`платежи: ${deps.payments}`)
|
||||
if (deps.balanceLedger) parts.push(`журнал баланса: ${deps.balanceLedger}`)
|
||||
if (deps.activeTariffs) parts.push(`тарифы: ${deps.activeTariffs}`)
|
||||
if (deps.syncLog) parts.push(`записи синка: ${deps.syncLog}`)
|
||||
return parts.length ? `Аккаунт используется (${parts.join(', ')})` : 'Аккаунт используется связанными записями'
|
||||
}
|
||||
|
||||
export const providerAccountsRoutes: FastifyPluginAsync = async (app) => {
|
||||
app.get('/api/provider-accounts', async () => providerAccountsRepository.list())
|
||||
|
||||
@@ -27,10 +37,27 @@ export const providerAccountsRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
|
||||
app.delete<{ Params: { id: string } }>('/api/provider-accounts/:id', async (req, reply) => {
|
||||
const ok = providerAccountsRepository.delete(req.params.id)
|
||||
if (!ok) {
|
||||
const existing = providerAccountsRepository.get(req.params.id)
|
||||
if (!existing) {
|
||||
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
|
||||
}
|
||||
const dependencies = providerAccountsRepository.getDependencyCounts(req.params.id)
|
||||
const total =
|
||||
dependencies.vps +
|
||||
dependencies.payments +
|
||||
dependencies.balanceLedger +
|
||||
dependencies.activeTariffs +
|
||||
dependencies.syncLog
|
||||
if (total > 0) {
|
||||
return reply.code(409).send({
|
||||
error: {
|
||||
code: 'CONFLICT',
|
||||
message: formatDependencyMessage(dependencies),
|
||||
dependencies,
|
||||
},
|
||||
})
|
||||
}
|
||||
providerAccountsRepository.delete(req.params.id)
|
||||
return reply.code(204).send()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user