191 lines
8.6 KiB
TypeScript
191 lines
8.6 KiB
TypeScript
|
|
/**
|
|||
|
|
* KIN-INFRA-012: Регрессионный тест — saveDeployConfig не теряет пустые строки
|
|||
|
|
*
|
|||
|
|
* Проверяет, что пустая строка в deploy-полях:
|
|||
|
|
* 1. Передаётся в patchProject как "" (не undefined)
|
|||
|
|
* 2. Переживает JSON.stringify — т.е. бэкенд получает ключ со значением ""
|
|||
|
|
* 3. Непустые значения тоже передаются корректно
|
|||
|
|
*
|
|||
|
|
* До фикса: `deploy_host: deployHosts.value[id] || undefined` → пустая строка
|
|||
|
|
* превращалась в undefined и JSON.stringify отбрасывал поле. Бэкенд не получал
|
|||
|
|
* сигнал очистки. После фикса: пустая строка передаётся как-есть.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|||
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|||
|
|
import SettingsView from '../views/SettingsView.vue'
|
|||
|
|
|
|||
|
|
vi.mock('../api', async (importOriginal) => {
|
|||
|
|
const actual = await importOriginal<typeof import('../api')>()
|
|||
|
|
return {
|
|||
|
|
...actual,
|
|||
|
|
api: {
|
|||
|
|
projects: vi.fn(),
|
|||
|
|
patchProject: vi.fn(),
|
|||
|
|
syncObsidian: vi.fn(),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
import { api } from '../api'
|
|||
|
|
|
|||
|
|
const BASE_PROJECT = {
|
|||
|
|
id: 'KIN',
|
|||
|
|
name: 'Kin',
|
|||
|
|
path: '/projects/kin',
|
|||
|
|
status: 'active',
|
|||
|
|
priority: 5,
|
|||
|
|
tech_stack: ['python'],
|
|||
|
|
execution_mode: null,
|
|||
|
|
autocommit_enabled: null,
|
|||
|
|
auto_test_enabled: null,
|
|||
|
|
obsidian_vault_path: null,
|
|||
|
|
deploy_command: null,
|
|||
|
|
test_command: null,
|
|||
|
|
deploy_host: null,
|
|||
|
|
deploy_path: null,
|
|||
|
|
deploy_runtime: null,
|
|||
|
|
deploy_restart_cmd: null,
|
|||
|
|
created_at: '2024-01-01',
|
|||
|
|
total_tasks: 0,
|
|||
|
|
done_tasks: 0,
|
|||
|
|
active_tasks: 0,
|
|||
|
|
blocked_tasks: 0,
|
|||
|
|
review_tasks: 0,
|
|||
|
|
project_type: null,
|
|||
|
|
ssh_host: null,
|
|||
|
|
ssh_user: null,
|
|||
|
|
ssh_key_path: null,
|
|||
|
|
ssh_proxy_jump: null,
|
|||
|
|
description: null,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
vi.clearAllMocks()
|
|||
|
|
vi.mocked(api.patchProject).mockResolvedValue(BASE_PROJECT as any)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function mountSettingsWithProject(overrides: Partial<typeof BASE_PROJECT> = {}) {
|
|||
|
|
const project = { ...BASE_PROJECT, ...overrides }
|
|||
|
|
vi.mocked(api.projects).mockResolvedValue([project as any])
|
|||
|
|
const wrapper = mount(SettingsView)
|
|||
|
|
await flushPromises()
|
|||
|
|
return wrapper
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function clickSaveDeployConfig(wrapper: ReturnType<typeof mount>) {
|
|||
|
|
const saveBtn = wrapper.findAll('button').find(b => b.text().includes('Save Deploy Config'))
|
|||
|
|
expect(saveBtn).toBeDefined()
|
|||
|
|
await saveBtn!.trigger('click')
|
|||
|
|
await flushPromises()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 1. Пустая строка передаётся как "" — не теряется как undefined
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('saveDeployConfig — пустые строки сохраняются в payload', () => {
|
|||
|
|
it('deploy_host="" передаётся как "" когда поле пустое (не undefined)', async () => {
|
|||
|
|
// null в проекте → инициализируется как '' в компоненте → должно передаться как ""
|
|||
|
|
const wrapper = await mountSettingsWithProject({ deploy_host: null })
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
expect(callArgs[1]).toHaveProperty('deploy_host')
|
|||
|
|
expect((callArgs[1] as any).deploy_host).toBe('')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('deploy_path="" передаётся как "" когда поле пустое (не undefined)', async () => {
|
|||
|
|
const wrapper = await mountSettingsWithProject({ deploy_path: null })
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
expect(callArgs[1]).toHaveProperty('deploy_path')
|
|||
|
|
expect((callArgs[1] as any).deploy_path).toBe('')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('deploy_runtime="" передаётся как "" когда поле пустое (не undefined)', async () => {
|
|||
|
|
const wrapper = await mountSettingsWithProject({ deploy_runtime: null })
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
expect(callArgs[1]).toHaveProperty('deploy_runtime')
|
|||
|
|
expect((callArgs[1] as any).deploy_runtime).toBe('')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('deploy_restart_cmd="" передаётся как "" когда поле пустое (не undefined)', async () => {
|
|||
|
|
const wrapper = await mountSettingsWithProject({ deploy_restart_cmd: null })
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
expect(callArgs[1]).toHaveProperty('deploy_restart_cmd')
|
|||
|
|
expect((callArgs[1] as any).deploy_restart_cmd).toBe('')
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 2. JSON.stringify не выбрасывает пустые строки — бэкенд их получит
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('saveDeployConfig — пустые строки выживают JSON.stringify', () => {
|
|||
|
|
it('все 4 поля присутствуют в JSON когда все значения пустые', async () => {
|
|||
|
|
// Проект с null во всех deploy-полях → инициализируются как ''
|
|||
|
|
const wrapper = await mountSettingsWithProject()
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
const payload = callArgs[1] as Record<string, unknown>
|
|||
|
|
|
|||
|
|
// Сериализуем как настоящий PATCH-запрос
|
|||
|
|
const jsonBody = JSON.parse(JSON.stringify(payload))
|
|||
|
|
|
|||
|
|
expect(jsonBody).toHaveProperty('deploy_host', '')
|
|||
|
|
expect(jsonBody).toHaveProperty('deploy_path', '')
|
|||
|
|
expect(jsonBody).toHaveProperty('deploy_runtime', '')
|
|||
|
|
expect(jsonBody).toHaveProperty('deploy_restart_cmd', '')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('пустая строка выживает JSON.stringify (регрессия: undefined исчезает)', async () => {
|
|||
|
|
const wrapper = await mountSettingsWithProject({ deploy_host: null })
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
const payload = callArgs[1] as Record<string, unknown>
|
|||
|
|
const jsonBody = JSON.parse(JSON.stringify(payload))
|
|||
|
|
|
|||
|
|
// "" должна присутствовать в JSON как пустая строка
|
|||
|
|
// undefined здесь отсутствовало бы — это был бы баг
|
|||
|
|
expect(Object.keys(jsonBody)).toContain('deploy_host')
|
|||
|
|
expect(jsonBody.deploy_host).toBe('')
|
|||
|
|
expect(jsonBody.deploy_host).not.toBeUndefined()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
// 3. Непустые значения передаются корректно
|
|||
|
|
// ─────────────────────────────────────────────────────────────
|
|||
|
|
describe('saveDeployConfig — непустые значения передаются корректно', () => {
|
|||
|
|
it('deploy_host с непустым значением передаётся без изменений', async () => {
|
|||
|
|
const wrapper = await mountSettingsWithProject({ deploy_host: 'prod.example.com' })
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
expect((callArgs[1] as any).deploy_host).toBe('prod.example.com')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('все 4 поля с непустыми значениями передаются корректно', async () => {
|
|||
|
|
const wrapper = await mountSettingsWithProject({
|
|||
|
|
deploy_host: 'prod.example.com',
|
|||
|
|
deploy_path: '/opt/kin',
|
|||
|
|
deploy_runtime: 'docker',
|
|||
|
|
deploy_restart_cmd: 'docker compose restart',
|
|||
|
|
})
|
|||
|
|
await clickSaveDeployConfig(wrapper)
|
|||
|
|
|
|||
|
|
const callArgs = vi.mocked(api.patchProject).mock.calls[0]
|
|||
|
|
const payload = callArgs[1] as any
|
|||
|
|
expect(payload.deploy_host).toBe('prod.example.com')
|
|||
|
|
expect(payload.deploy_path).toBe('/opt/kin')
|
|||
|
|
expect(payload.deploy_runtime).toBe('docker')
|
|||
|
|
expect(payload.deploy_restart_cmd).toBe('docker compose restart')
|
|||
|
|
})
|
|||
|
|
})
|