WP-30/31/32: contract and failing tests for backup management; OS scan skips container dirs
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 23:05:22 +02:00
parent 5026ce22de
commit 39af18b336
16 changed files with 1456 additions and 1 deletions

View File

@ -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
}

View 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)
})
})

View 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')
})
})