WP-30/31/32: contract and failing tests for backup management; OS scan skips container dirs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -163,3 +163,71 @@ export interface VulnSummary {
|
||||
last_scan: string | null
|
||||
scanner: string
|
||||
}
|
||||
|
||||
export type StorageKind = 'smb' | 'ftp'
|
||||
|
||||
export interface BackupTargetView {
|
||||
id: string
|
||||
name: string
|
||||
kind: StorageKind
|
||||
host: string
|
||||
port: number | null
|
||||
share: string
|
||||
path: string
|
||||
username: string
|
||||
tls: boolean
|
||||
password_set: boolean
|
||||
}
|
||||
|
||||
export interface BackupTargetPayload {
|
||||
name: string
|
||||
kind: StorageKind
|
||||
host: string
|
||||
port: number | null
|
||||
share: string
|
||||
path: string
|
||||
username: string
|
||||
password: string
|
||||
tls: boolean
|
||||
}
|
||||
|
||||
export type BackupSource =
|
||||
| { type: 'volume_claim'; namespace: string; pvc: string }
|
||||
| { type: 'postgres_dump'; namespace: string; pod: string }
|
||||
| { type: 'kubernetes_manifests'; namespace: string }
|
||||
| { type: 'host_path'; path: string }
|
||||
|
||||
export interface BackupStrategyView {
|
||||
id: string
|
||||
name: string
|
||||
source: BackupSource
|
||||
schedule: string
|
||||
target_id: string
|
||||
retention: number
|
||||
encrypted: boolean
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface BackupStrategyPayload {
|
||||
name: string
|
||||
source: BackupSource
|
||||
schedule: string
|
||||
target_id: string
|
||||
retention: number
|
||||
passphrase: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface BackupRecord {
|
||||
id: string
|
||||
strategy_id: string
|
||||
filename: string
|
||||
size_bytes: number
|
||||
sha256: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface StrategyStatus extends BackupStrategyView {
|
||||
target_name: string
|
||||
last_backup: BackupRecord | null
|
||||
}
|
||||
|
||||
71
frontend/src/components/StrategyForm.test.ts
Normal file
71
frontend/src/components/StrategyForm.test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import StrategyForm from './StrategyForm.vue'
|
||||
|
||||
const targets = [
|
||||
{ id: 't1', name: 'NAS' },
|
||||
{ id: 't2', name: 'FTP' },
|
||||
]
|
||||
|
||||
describe('StrategyForm', () => {
|
||||
it('builds a postgres dump strategy', async () => {
|
||||
const w = mount(StrategyForm, { props: { targets } })
|
||||
await w.find('input[name=name]').setValue('Gitea DB')
|
||||
await w.find('select[name=source_type]').setValue('postgres_dump')
|
||||
await w.find('input[name=namespace]').setValue('gitea')
|
||||
await w.find('input[name=pod]').setValue('gitea-postgresql-0')
|
||||
await w.find('input[name=schedule]').setValue('0 0 2 * * *')
|
||||
await w.find('select[name=target_id]').setValue('t2')
|
||||
await w.find('input[name=retention]').setValue('7')
|
||||
await w.find('input[name=passphrase]').setValue('a-long-passphrase!')
|
||||
await w.find('form').trigger('submit')
|
||||
expect(w.emitted('submit')![0][0]).toEqual({
|
||||
name: 'Gitea DB',
|
||||
source: { type: 'postgres_dump', namespace: 'gitea', pod: 'gitea-postgresql-0' },
|
||||
schedule: '0 0 2 * * *',
|
||||
target_id: 't2',
|
||||
retention: 7,
|
||||
passphrase: 'a-long-passphrase!',
|
||||
enabled: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('switches source fields by type', async () => {
|
||||
const w = mount(StrategyForm, { props: { targets } })
|
||||
await w.find('select[name=source_type]').setValue('host_path')
|
||||
expect(w.find('input[name=path]').exists()).toBe(true)
|
||||
expect(w.find('input[name=namespace]').exists()).toBe(false)
|
||||
await w.find('select[name=source_type]').setValue('volume_claim')
|
||||
expect(w.find('input[name=pvc]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('sends null passphrase when empty on create and keeps it on edit', async () => {
|
||||
const w = mount(StrategyForm, { props: { targets } })
|
||||
await w.find('input[name=name]').setValue('x')
|
||||
await w.find('input[name=path]').exists()
|
||||
await w.find('select[name=source_type]').setValue('kubernetes_manifests')
|
||||
await w.find('input[name=namespace]').setValue('gitea')
|
||||
await w.find('form').trigger('submit')
|
||||
expect((w.emitted('submit')![0][0] as { passphrase: unknown }).passphrase).toBeNull()
|
||||
|
||||
const e = mount(StrategyForm, {
|
||||
props: {
|
||||
targets,
|
||||
current: {
|
||||
id: 's1',
|
||||
name: 'Old',
|
||||
source: { type: 'host_path', path: '/srv' },
|
||||
schedule: '0 0 1 * * *',
|
||||
target_id: 't1',
|
||||
retention: 2,
|
||||
encrypted: true,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(e.text()).toContain('leave empty to keep')
|
||||
await e.find('form').trigger('submit')
|
||||
const p = e.emitted('submit')![0][0] as { passphrase: unknown; enabled: boolean }
|
||||
expect(p.passphrase).toBe('')
|
||||
expect(p.enabled).toBe(false)
|
||||
})
|
||||
})
|
||||
57
frontend/src/components/TargetForm.test.ts
Normal file
57
frontend/src/components/TargetForm.test.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TargetForm from './TargetForm.vue'
|
||||
|
||||
describe('TargetForm', () => {
|
||||
it('emits an smb target payload', async () => {
|
||||
const w = mount(TargetForm, { props: {} })
|
||||
await w.find('input[name=name]').setValue('NAS')
|
||||
await w.find('select[name=kind]').setValue('smb')
|
||||
await w.find('input[name=host]').setValue('nas.local')
|
||||
await w.find('input[name=share]').setValue('backups')
|
||||
await w.find('input[name=path]').setValue('softvisor')
|
||||
await w.find('input[name=username]').setValue('backup')
|
||||
await w.find('input[name=password]').setValue('secret')
|
||||
await w.find('form').trigger('submit')
|
||||
expect(w.emitted('submit')![0][0]).toEqual({
|
||||
name: 'NAS',
|
||||
kind: 'smb',
|
||||
host: 'nas.local',
|
||||
port: null,
|
||||
share: 'backups',
|
||||
path: 'softvisor',
|
||||
username: 'backup',
|
||||
password: 'secret',
|
||||
tls: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('shows ftp-only fields and hides share for ftp', async () => {
|
||||
const w = mount(TargetForm, { props: {} })
|
||||
await w.find('select[name=kind]').setValue('ftp')
|
||||
expect(w.find('input[name=share]').exists()).toBe(false)
|
||||
expect(w.find('input[name=tls]').exists()).toBe(true)
|
||||
expect(w.find('input[name=port]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('prefills an existing target and keeps the password empty', () => {
|
||||
const w = mount(TargetForm, {
|
||||
props: {
|
||||
current: {
|
||||
id: '1',
|
||||
name: 'NAS',
|
||||
kind: 'smb',
|
||||
host: 'nas.local',
|
||||
port: null,
|
||||
share: 'backups',
|
||||
path: 'x',
|
||||
username: 'u',
|
||||
tls: false,
|
||||
password_set: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect((w.find('input[name=host]').element as HTMLInputElement).value).toBe('nas.local')
|
||||
expect((w.find('input[name=password]').element as HTMLInputElement).value).toBe('')
|
||||
expect(w.text()).toContain('leave empty to keep')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user