mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-08 22:46:41 +00:00
117 lines
9.7 KiB
Python
117 lines
9.7 KiB
Python
"""Backup-only maintainer contract at actual render/dispatch/email seams.
|
||
|
||
Scope-approved seams: receiver, template lookup, rich enrichment, inert queued
|
||
and manual delivery with an email capture sink. No host operations or sends.
|
||
"""
|
||
import unittest
|
||
from notification_fixture import templates, LANGUAGES
|
||
from notification_final_fixture import deliver
|
||
|
||
|
||
class BackupSplitTests(unittest.TestCase):
|
||
def test_recovery_default_is_upstream_resolved_without_proof(self):
|
||
for language in LANGUAGES:
|
||
data = {'hostname': 'alias', 'category': 'temperature',
|
||
'reason': 'Temperature high (recovered)', 'duration': '2m',
|
||
'original_severity': 'WARNING'}
|
||
rendered = templates.render_template('error_resolved', data, language)
|
||
expected = templates.runtime_message('templates.error_resolved.title', language,
|
||
hostname='alias', category='temperature', entity_suffix='')
|
||
self.assertEqual(rendered['title'], expected)
|
||
rich, _ = templates.enrich_with_emojis('error_resolved', rendered['title'], rendered['body'], data)
|
||
self.assertTrue(rich.startswith('✅ '))
|
||
result = deliver('error_resolved', data, 'OK', language)
|
||
self.assertIn('background:#f0fdf4;', result['html'])
|
||
|
||
def test_pre_guest_native_subject_keeps_only_cause_once_with_display_alias(self):
|
||
from notification_fixture import receive
|
||
raw_host = 'pve-production.internal.example'
|
||
message = ('Details\n=======\nVMID Name Status Time Size Filename\n'
|
||
'\nTotal running time: 0s\nTotal size: 0 B')
|
||
for raw in (message, 'ERROR: unable to activate storage PBS\n' + message):
|
||
event = receive(raw, 'error', 'vzdump backup status (' + raw_host +
|
||
'): backup failed: unable to activate storage PBS')
|
||
event.data['hostname'] = 'display-alias {rack.location}'
|
||
for language in LANGUAGES:
|
||
for manual in (False, True):
|
||
result = deliver(event.event_type, event.data, event.severity, language, manual=manual)
|
||
self.assertNotIn(raw_host, result['text'])
|
||
self.assertNotIn('vzdump backup status', result['text'])
|
||
self.assertEqual(result['text'].count('unable to activate storage PBS'), 1)
|
||
self.assertIn('display-alias {rack.location}', result['title'])
|
||
self.assertEqual(result['data']['pve_title'], event.data['pve_title'])
|
||
self.assertEqual(result['data']['pve_message'], raw)
|
||
# No global hostname or guest-name substitution: actual diagnostics are
|
||
# authoritative, even when they happen to contain the original hostname.
|
||
event = receive(message, 'error', 'vzdump backup status (' + raw_host +
|
||
'): backup failed: cannot connect to ' + raw_host)
|
||
result = deliver(event.event_type, {**event.data, 'hostname': 'alias'}, event.severity)
|
||
self.assertEqual(result['body'].count('cannot connect to ' + raw_host), 1)
|
||
self.assertNotIn('vzdump backup status', result['body'])
|
||
|
||
def test_backup_legacy_keys_keep_meaning_and_unknown_uses_new_keys(self):
|
||
# Frozen upstream contract; no Git history required in shipped tests.
|
||
legacy = {'en': {'title': '{hostname} → {storage}: Backup complete — {vmname} ({vmid})', 'body': 'Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}', 'label': 'Backup complete'}, 'de': {'title': '{hostname} → {storage}: Sicherung abgeschlossen – {vmname} ({vmid})', 'body': 'Die Sicherung von {vmname} (ID: {vmid}) wurde am {storage} erfolgreich abgeschlossen.\nGröße: {size}', 'label': 'Sicherung abgeschlossen'}, 'es': {'title': '{hostname} → {storage}: Backup completado — {vmname} ({vmid})', 'body': 'El backup de {vmname} (ID: {vmid}) se ha completado correctamente en {storage}.\nTamaño: {size}', 'label': 'Backup completado'}, 'fr': {'title': '{hostname} → {storage}\xa0: Sauvegarde terminée — {vmname} ({vmid})', 'body': "La sauvegarde de {vmname} (ID\xa0: {vmid}) s'est terminée avec succès le {storage}.\nTaille\xa0: {size}", 'label': 'Sauvegarde terminée'}, 'it': {'title': '{hostname} → {storage}: Backup completato — {vmname} ({vmid})', 'body': 'Backup di {vmname} (ID: {vmid}) completato con successo su {storage}.\nTaglia: {size}', 'label': 'Backup completato'}, 'pt': {'title': '{hostname} → {storage}: Backup concluído — {vmname} ({vmid})', 'body': 'Backup de {vmname} (ID: {vmid}) concluído com sucesso em {storage}.\nTamanho: {size}', 'label': 'Backup concluído'}, 'sk': {'title': '{hostname} → {storage}: Záloha dokončená — {vmname} ({vmid})', 'body': 'Záloha {vmname} (ID: {vmid}) na úložisku {storage} bola úspešne dokončená.\nVeľkosť: {size}', 'label': 'Záloha bola dokončená'}, 'sv': {'title': '{hostname} → {storage}: Säkerhetskopiering klar — {vmname} ({vmid})', 'body': 'Säkerhetskopiering av {vmname} (ID: {vmid}) slutfördes framgångsrikt på {storage}.\nStorlek: {size}', 'label': 'Säkerhetskopieringen är klar'}}
|
||
for language, expected in legacy.items():
|
||
self.assertEqual(templates._load_runtime_catalog(language)['templates']['backup_complete'], expected)
|
||
result = templates.render_template('backup_complete', {'hostname': 'alias {rack.location}'}, language)
|
||
self.assertIn('alias {rack.location}', result['title'])
|
||
self.assertNotIn('()', result['title'])
|
||
new_title = templates.runtime_message('backup.unconfirmedTitle', language, hostname='alias {rack.location}')
|
||
self.assertTrue(new_title)
|
||
self.assertEqual(result['title'], new_title)
|
||
self.assertIn(templates.runtime_message('backup.unconfirmedBody', language), result['body'])
|
||
# Absent/blank/non-string translation uses English per-key fallback.
|
||
from unittest.mock import patch
|
||
english = templates._load_runtime_catalog('en')
|
||
for value in (None, '', {}, []):
|
||
missing = {'backup': {'unconfirmedTitle': value, 'unconfirmedBody': value}}
|
||
with patch.object(templates, '_load_runtime_catalog', side_effect=lambda lang: english if lang == 'en' else missing):
|
||
result = templates.render_template('backup_complete', {'hostname': 'alias'}, 'it')
|
||
self.assertEqual(result['title'], 'alias: Backup outcome unconfirmed')
|
||
self.assertEqual(result['body'], 'The backup outcome is not confirmed.')
|
||
# Both catalogs missing: new outcome text still has explicit EN defaults.
|
||
with patch.object(templates, '_load_runtime_catalog', return_value={}):
|
||
result = templates.render_template('backup_complete', {'hostname': 'alias'}, 'it')
|
||
self.assertEqual(result['title'], 'alias: Backup outcome unconfirmed')
|
||
self.assertEqual(result['body'], 'The backup outcome is not confirmed.')
|
||
|
||
def test_restore_keeps_original_ready_line_and_success_icon_with_warnings(self):
|
||
from notification_final_fixture import restore_event
|
||
ready = {'en': 'The node is now fully ready to use.', 'de': 'Der Knoten ist nun vollständig einsatzbereit.', 'es': 'El nodo está listo para usarse.', 'fr': 'Le nœud est maintenant entièrement prêt à être utilisé.', 'it': "Il nodo è ora completamente pronto per l'uso.", 'pt': 'O nó agora está totalmente pronto para uso.', 'sk': 'Uzol je teraz úplne pripravený na použitie.', 'sv': 'Noden är nu helt redo att användas.'}
|
||
for warning in ('', 'missing module zfs'):
|
||
event = restore_event(warning)
|
||
for language in LANGUAGES:
|
||
result = deliver(event['event_type'], event['data'], event['severity'], language)
|
||
self.assertTrue(result['title'].startswith('✅ '))
|
||
self.assertIn(ready[language], result['body'])
|
||
self.assertIn(ready[language], result['text'])
|
||
self.assertIn('2m', result['text'])
|
||
if warning:
|
||
self.assertIn(warning, result['text'])
|
||
quiet = deliver(event['event_type'], event['data'], event['severity'], language, quiet=True)
|
||
self.assertIn('✅', quiet['body'])
|
||
self.assertIn(' ' + ready[language], quiet['body'])
|
||
self.assertIn('white-space:pre-wrap;', quiet['html'])
|
||
|
||
def test_spanish_outcome_and_restore_titles_are_capitalized_and_failure_is_exact(self):
|
||
expected = {'confirmed': 'Backup completado', 'completed_with_warnings': 'Backup completado con advertencias',
|
||
'unconfirmed': 'Resultado del backup sin confirmar', 'failed': 'Backup fallido'}
|
||
for outcome, title in expected.items():
|
||
result = templates.render_template('backup_complete', {'hostname': 'alias', 'backup_outcome': outcome}, 'es')
|
||
self.assertEqual(result['title'], 'alias: ' + title)
|
||
failure = templates.render_template('backup_fail', {'hostname': 'alias'}, 'es')
|
||
self.assertEqual(failure['title'], 'alias: Backup fallido')
|
||
restore = templates.render_template('system_restore_completed', {'hostname': 'alias'}, 'es')
|
||
self.assertEqual(restore['title'], 'alias: Restauración del host finalizada')
|
||
|
||
def test_recovery_only_quiet_release_keeps_upstream_summary_contract(self):
|
||
result = deliver('error_resolved', {'hostname': 'alias', 'category': 'temperature',
|
||
'reason': 'old (recovered)', 'duration': '2m'}, 'OK', quiet=True)
|
||
self.assertIn(templates.runtime_message('digest.lead', 'en', count=1).strip(), result['body'])
|
||
self.assertIn(templates.runtime_message('digest.footer', 'en'), result['body'])
|
||
self.assertIn('✅', result['body'])
|
||
|
||
|
||
if __name__ == '__main__': unittest.main()
|