Files
ProxMenux/.github/scripts/tests/test_notification_backup_split.py
T

117 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()