|
20 | 20 | from django.core.files import File |
21 | 21 | from django.core.files.uploadedfile import SimpleUploadedFile |
22 | 22 | from django.core.management import call_command |
23 | | -from django.test import override_settings |
| 23 | +from django.test import SimpleTestCase, override_settings |
24 | 24 | from django.urls import reverse |
25 | 25 |
|
26 | 26 | from weblate.addons.webhooks import WebhookAddon |
|
33 | 33 | from weblate.screenshots.models import Screenshot |
34 | 34 | from weblate.trans.actions import ActionEvents |
35 | 35 | from weblate.trans.backups import ( |
| 36 | + CATEGORY_BACKUP_FIELDS, |
| 37 | + COMPONENT_BACKUP_FIELDS, |
| 38 | + PROJECT_BACKUP_FIELDS, |
36 | 39 | ProjectBackup, |
37 | 40 | get_project_backup_download_storage, |
38 | 41 | get_project_backup_download_url, |
39 | 42 | list_backups, |
40 | 43 | ) |
41 | 44 | from weblate.trans.change_display import get_change_history_context |
| 45 | +from weblate.trans.forms import ( |
| 46 | + CategorySettingsForm, |
| 47 | + ComponentSettingsForm, |
| 48 | + ProjectSettingsForm, |
| 49 | +) |
42 | 50 | from weblate.trans.models import ( |
43 | 51 | Category, |
44 | 52 | Change, |
|
50 | 58 | Unit, |
51 | 59 | Vote, |
52 | 60 | ) |
| 61 | +from weblate.trans.models.project import CommitPolicyChoices |
53 | 62 | from weblate.trans.tasks import ( |
54 | 63 | cleanup_project_backup_download, |
55 | 64 | cleanup_project_backups, |
@@ -81,6 +90,58 @@ def resolve_private_example(host, *args, **kwargs): |
81 | 90 | return [(0, 0, 0, "", (address, 443))] |
82 | 91 |
|
83 | 92 |
|
| 93 | +class BackupSettingCoverageTest(SimpleTestCase): |
| 94 | + def test_backup_setting_coverage(self) -> None: |
| 95 | + """Ensure newly introduced editable settings are not silently omitted.""" |
| 96 | + backup = ProjectBackup() |
| 97 | + backup_settings = ( |
| 98 | + # Workspace is selected by the restore caller. Machinery settings can |
| 99 | + # contain service credentials and are intentionally not exported. |
| 100 | + ( |
| 101 | + Project, |
| 102 | + ProjectSettingsForm, |
| 103 | + backup.project_schema["properties"]["project"], |
| 104 | + PROJECT_BACKUP_FIELDS, |
| 105 | + {"workspace", "machinery_settings"}, |
| 106 | + ), |
| 107 | + # Category hierarchy is represented by the backup object graph. |
| 108 | + ( |
| 109 | + Category, |
| 110 | + CategorySettingsForm, |
| 111 | + backup.project_schema["definitions"]["category"], |
| 112 | + CATEGORY_BACKUP_FIELDS, |
| 113 | + {"project", "category"}, |
| 114 | + ), |
| 115 | + # Component hierarchy is represented by the backup object graph; |
| 116 | + # generated and internal repository revisions are not settings. |
| 117 | + ( |
| 118 | + Component, |
| 119 | + ComponentSettingsForm, |
| 120 | + backup.component_schema["properties"]["component"], |
| 121 | + COMPONENT_BACKUP_FIELDS, |
| 122 | + {"project", "category", "git_export", "processed_revision"}, |
| 123 | + ), |
| 124 | + ) |
| 125 | + for model, form, schema, extra_fields, excluded_fields in backup_settings: |
| 126 | + with self.subTest(model=model.__name__): |
| 127 | + backup_fields = set(schema["required"]) | set(extra_fields) |
| 128 | + schema_fields = set(schema["properties"]) |
| 129 | + # ruff: ignore[private-member-access] |
| 130 | + editable_fields = { |
| 131 | + field.name |
| 132 | + for field in model._meta.fields |
| 133 | + if field.editable and not field.auto_created |
| 134 | + } |
| 135 | + |
| 136 | + self.assertSetEqual( |
| 137 | + editable_fields - excluded_fields, |
| 138 | + backup_fields & editable_fields, |
| 139 | + ) |
| 140 | + # ruff: ignore[private-member-access] |
| 141 | + self.assertLessEqual(set(form._meta.fields), backup_fields) |
| 142 | + self.assertLessEqual(backup_fields, schema_fields) |
| 143 | + |
| 144 | + |
84 | 145 | class BackupsTest(ViewTestCase): |
85 | 146 | CREATE_GLOSSARIES: bool = True |
86 | 147 |
|
@@ -850,6 +911,72 @@ def test_backup_inherited_settings(self) -> None: |
850 | 911 | assert isinstance(secondary_language, Language) |
851 | 912 | self.assertEqual(secondary_language.code, "de") |
852 | 913 |
|
| 914 | + def test_backup_settings(self) -> None: |
| 915 | + project = self.project |
| 916 | + project.autoclean_tm = not project.autoclean_tm |
| 917 | + project.enforced_2fa = True |
| 918 | + project.commit_policy = CommitPolicyChoices.APPROVED_ONLY |
| 919 | + project.save(update_fields=["autoclean_tm", "enforced_2fa", "commit_policy"]) |
| 920 | + component = self.create_po_mono(project=project, name="Backup-settings") |
| 921 | + component.hide_glossary_matches = True |
| 922 | + component.contribute_project_tm = False |
| 923 | + component.file_format_params = { |
| 924 | + "po_line_wrap": 65535, |
| 925 | + "po_set_language_team": True, |
| 926 | + } |
| 927 | + component.screenshot_filemask = "screenshots/*.png" |
| 928 | + component.key_filter = "^keep" |
| 929 | + component.save( |
| 930 | + update_fields=[ |
| 931 | + "hide_glossary_matches", |
| 932 | + "contribute_project_tm", |
| 933 | + "file_format_params", |
| 934 | + "screenshot_filemask", |
| 935 | + "key_filter", |
| 936 | + ] |
| 937 | + ) |
| 938 | + |
| 939 | + backup = ProjectBackup() |
| 940 | + backup.backup_project(project) |
| 941 | + with ZipFile(backup.filename, "r") as zipfile: |
| 942 | + project_data = json.loads(zipfile.read("weblate-backup.json"))["project"] |
| 943 | + component_data = json.loads( |
| 944 | + zipfile.read(f"components/{component.slug}.json") |
| 945 | + )["component"] |
| 946 | + |
| 947 | + self.assertEqual(project_data["autoclean_tm"], project.autoclean_tm) |
| 948 | + self.assertTrue(project_data["enforced_2fa"]) |
| 949 | + self.assertEqual( |
| 950 | + project_data["commit_policy"], CommitPolicyChoices.APPROVED_ONLY |
| 951 | + ) |
| 952 | + self.assertTrue(component_data["hide_glossary_matches"]) |
| 953 | + self.assertFalse(component_data["contribute_project_tm"]) |
| 954 | + self.assertEqual( |
| 955 | + component_data["file_format_params"], component.file_format_params |
| 956 | + ) |
| 957 | + self.assertEqual(component_data["screenshot_filemask"], "screenshots/*.png") |
| 958 | + self.assertEqual(component_data["key_filter"], "^keep") |
| 959 | + |
| 960 | + restore = ProjectBackup(backup.filename) |
| 961 | + restore.validate() |
| 962 | + restored = restore.restore( |
| 963 | + project_name="Restored settings", |
| 964 | + project_slug="restored-settings", |
| 965 | + user=self.user, |
| 966 | + ) |
| 967 | + restored_component = restored.component_set.get(slug=component.slug) |
| 968 | + |
| 969 | + self.assertEqual(restored.autoclean_tm, project.autoclean_tm) |
| 970 | + self.assertTrue(restored.enforced_2fa) |
| 971 | + self.assertEqual(restored.commit_policy, CommitPolicyChoices.APPROVED_ONLY) |
| 972 | + self.assertTrue(restored_component.hide_glossary_matches) |
| 973 | + self.assertFalse(restored_component.contribute_project_tm) |
| 974 | + self.assertEqual( |
| 975 | + restored_component.file_format_params, component.file_format_params |
| 976 | + ) |
| 977 | + self.assertEqual(restored_component.screenshot_filemask, "screenshots/*.png") |
| 978 | + self.assertEqual(restored_component.key_filter, "^keep") |
| 979 | + |
853 | 980 | def test_backup_team_members_prefetches_limit_languages(self) -> None: |
854 | 981 | team = Group.objects.create(name="Prefetch team", defining_project=self.project) |
855 | 982 | first_user = type(self.user).objects.create_user( |
|
0 commit comments