udata 13.0.1.dev12__py3-none-any.whl → 14.4.1.dev7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api/__init__.py +2 -8
- udata/api_fields.py +35 -4
- udata/app.py +30 -50
- udata/auth/__init__.py +29 -6
- udata/auth/forms.py +8 -6
- udata/auth/views.py +6 -3
- udata/commands/__init__.py +2 -14
- udata/commands/db.py +13 -25
- udata/commands/info.py +0 -16
- udata/commands/serve.py +3 -11
- udata/commands/tests/test_fixtures.py +9 -9
- udata/core/access_type/api.py +1 -1
- udata/core/access_type/constants.py +12 -8
- udata/core/activity/api.py +5 -6
- udata/core/avatars/api.py +43 -0
- udata/core/avatars/test_avatar_api.py +30 -0
- udata/core/badges/tests/test_commands.py +6 -6
- udata/core/csv.py +5 -0
- udata/core/dataservices/models.py +15 -3
- udata/core/dataservices/tasks.py +7 -0
- udata/core/dataset/api.py +2 -0
- udata/core/dataset/models.py +2 -2
- udata/core/dataset/permissions.py +31 -0
- udata/core/dataset/tasks.py +50 -10
- udata/core/discussions/models.py +1 -0
- udata/core/metrics/__init__.py +0 -6
- udata/core/organization/api.py +8 -5
- udata/core/organization/mails.py +1 -1
- udata/core/organization/models.py +9 -1
- udata/core/organization/notifications.py +84 -0
- udata/core/organization/permissions.py +1 -1
- udata/core/organization/tasks.py +3 -0
- udata/core/pages/tests/test_api.py +32 -0
- udata/core/post/api.py +24 -69
- udata/core/post/models.py +84 -16
- udata/core/post/tests/test_api.py +24 -1
- udata/core/reports/api.py +18 -0
- udata/core/reports/models.py +42 -2
- udata/core/reuse/models.py +1 -1
- udata/core/reuse/tasks.py +7 -0
- udata/core/site/models.py +2 -6
- udata/core/spatial/commands.py +2 -4
- udata/core/spatial/forms.py +2 -2
- udata/core/spatial/models.py +0 -10
- udata/core/spatial/tests/test_api.py +1 -36
- udata/core/user/models.py +15 -2
- udata/cors.py +2 -5
- udata/db/migrations.py +279 -0
- udata/features/notifications/api.py +7 -18
- udata/features/notifications/models.py +56 -0
- udata/features/notifications/tasks.py +25 -0
- udata/flask_mongoengine/engine.py +0 -4
- udata/frontend/__init__.py +3 -122
- udata/frontend/markdown.py +2 -1
- udata/harvest/actions.py +24 -9
- udata/harvest/api.py +30 -22
- udata/harvest/backends/__init__.py +21 -9
- udata/harvest/backends/base.py +29 -3
- udata/harvest/backends/ckan/harvesters.py +13 -2
- udata/harvest/backends/dcat.py +3 -0
- udata/harvest/backends/maaf.py +1 -0
- udata/harvest/commands.py +39 -4
- udata/harvest/filters.py +17 -6
- udata/harvest/forms.py +9 -6
- udata/harvest/models.py +16 -0
- udata/harvest/permissions.py +27 -0
- udata/harvest/tasks.py +3 -5
- udata/harvest/tests/ckan/test_ckan_backend.py +35 -2
- udata/harvest/tests/ckan/test_ckan_backend_errors.py +1 -1
- udata/harvest/tests/ckan/test_ckan_backend_filters.py +1 -1
- udata/harvest/tests/ckan/test_dkan_backend.py +1 -1
- udata/harvest/tests/dcat/udata.xml +6 -6
- udata/harvest/tests/factories.py +1 -1
- udata/harvest/tests/test_actions.py +63 -8
- udata/harvest/tests/test_api.py +278 -123
- udata/harvest/tests/test_base_backend.py +88 -1
- udata/harvest/tests/test_dcat_backend.py +60 -13
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +11 -273
- udata/mail.py +5 -1
- udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
- udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
- udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
- udata/models/__init__.py +0 -8
- udata/mongo/slug_fields.py +1 -1
- udata/rdf.py +45 -6
- udata/routing.py +2 -10
- udata/sentry.py +4 -10
- udata/settings.py +23 -17
- udata/tasks.py +4 -3
- udata/templates/mail/message.html +5 -31
- udata/tests/__init__.py +28 -12
- udata/tests/api/__init__.py +108 -21
- udata/tests/api/test_activities_api.py +36 -0
- udata/tests/api/test_auth_api.py +121 -95
- udata/tests/api/test_base_api.py +7 -4
- udata/tests/api/test_dataservices_api.py +29 -1
- udata/tests/api/test_datasets_api.py +45 -21
- udata/tests/api/test_organizations_api.py +192 -197
- udata/tests/api/test_reports_api.py +157 -0
- udata/tests/api/test_reuses_api.py +147 -147
- udata/tests/api/test_security_api.py +12 -12
- udata/tests/api/test_swagger.py +4 -4
- udata/tests/api/test_tags_api.py +8 -8
- udata/tests/api/test_user_api.py +13 -1
- udata/tests/apiv2/test_swagger.py +4 -4
- udata/tests/apiv2/test_topics.py +1 -1
- udata/tests/cli/test_cli_base.py +8 -9
- udata/tests/dataset/test_dataset_commands.py +4 -4
- udata/tests/dataset/test_dataset_model.py +66 -26
- udata/tests/dataset/test_dataset_rdf.py +99 -5
- udata/tests/dataset/test_resource_preview.py +0 -1
- udata/tests/frontend/test_auth.py +24 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +37 -27
- udata/tests/organization/test_notifications.py +67 -2
- udata/tests/plugin.py +6 -261
- udata/tests/site/test_site_csv_exports.py +22 -10
- udata/tests/test_activity.py +9 -9
- udata/tests/test_cors.py +1 -1
- udata/tests/test_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- udata/tests/test_migrations.py +181 -481
- udata/tests/test_notifications.py +15 -57
- udata/tests/test_notifications_task.py +43 -0
- udata/tests/test_owned.py +81 -1
- udata/tests/test_storages.py +25 -19
- udata/tests/test_topics.py +77 -61
- udata/tests/test_uris.py +33 -0
- udata/tests/workers/test_jobs_commands.py +23 -23
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +187 -108
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +187 -108
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +187 -108
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +188 -109
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +187 -108
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +187 -108
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +187 -108
- udata/translations/udata.pot +215 -106
- udata/uris.py +0 -2
- udata/utils.py +5 -0
- udata-14.4.1.dev7.dist-info/METADATA +109 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +153 -166
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +3 -5
- udata/core/followers/views.py +0 -15
- udata/core/post/forms.py +0 -30
- udata/entrypoints.py +0 -93
- udata/features/identicon/__init__.py +0 -0
- udata/features/identicon/api.py +0 -13
- udata/features/identicon/backends.py +0 -131
- udata/features/identicon/tests/__init__.py +0 -0
- udata/features/identicon/tests/test_backends.py +0 -18
- udata/features/territories/__init__.py +0 -49
- udata/features/territories/api.py +0 -25
- udata/features/territories/models.py +0 -51
- udata/flask_mongoengine/json.py +0 -38
- udata/migrations/__init__.py +0 -367
- udata/templates/mail/base.html +0 -105
- udata/templates/mail/base.txt +0 -6
- udata/templates/mail/button.html +0 -3
- udata/templates/mail/layouts/1-column.html +0 -19
- udata/templates/mail/layouts/2-columns.html +0 -20
- udata/templates/mail/layouts/center-panel.html +0 -16
- udata/tests/cli/test_db_cli.py +0 -68
- udata/tests/features/territories/__init__.py +0 -20
- udata/tests/features/territories/test_territories_api.py +0 -185
- udata/tests/frontend/test_hooks.py +0 -149
- udata-13.0.1.dev12.dist-info/METADATA +0 -133
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
udata/tests/test_migrations.py
CHANGED
|
@@ -1,520 +1,220 @@
|
|
|
1
|
-
import importlib.util
|
|
2
1
|
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
3
|
from textwrap import dedent
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
from mongoengine.connection import get_db
|
|
7
7
|
|
|
8
|
-
from udata import migrations
|
|
8
|
+
from udata.db import migrations
|
|
9
9
|
from udata.tests.api import PytestOnlyDBTestCase
|
|
10
|
-
from udata.tests.helpers import assert_equal_dates
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class MigrationsMock:
|
|
14
|
-
def __init__(self, root):
|
|
15
|
-
self.root = root
|
|
16
|
-
self.plugins = set()
|
|
17
|
-
self.enabled = set()
|
|
18
|
-
self.build_module("udata")
|
|
19
|
-
|
|
20
|
-
def add_migration(self, plugin, filename, content="", enable=True):
|
|
21
|
-
module = self.ensure_plugin(plugin, enabled=enable)
|
|
22
|
-
module.ensure_dir("migrations")
|
|
23
|
-
migration = module / "migrations" / filename
|
|
24
|
-
migration.write(dedent(content))
|
|
25
|
-
|
|
26
|
-
def build_module(self, name):
|
|
27
|
-
root = self.root.ensure_dir(name)
|
|
28
|
-
root.ensure("__init__.py")
|
|
29
|
-
|
|
30
|
-
def ensure_plugin(self, plugin, enabled=True):
|
|
31
|
-
if plugin not in self.plugins and plugin != "udata":
|
|
32
|
-
self.build_module(plugin)
|
|
33
|
-
self.plugins.add(plugin)
|
|
34
|
-
if enabled and plugin != "udata":
|
|
35
|
-
self.enabled.add(plugin)
|
|
36
|
-
else:
|
|
37
|
-
self.enabled.discard(plugin)
|
|
38
|
-
return self.root / plugin
|
|
39
|
-
|
|
40
|
-
def _load_module(self, name, path):
|
|
41
|
-
# See: https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
42
|
-
spec = importlib.util.spec_from_file_location(name, str(path / "__init__.py"))
|
|
43
|
-
module = importlib.util.module_from_spec(spec)
|
|
44
|
-
return module
|
|
45
|
-
|
|
46
|
-
def _resource_path(self, name, path):
|
|
47
|
-
return self.root / name / path
|
|
48
|
-
|
|
49
|
-
def mock_resource_listdir(self, name, dirname):
|
|
50
|
-
target = self._resource_path(name, dirname)
|
|
51
|
-
return [f.relto(target) for f in target.listdir()]
|
|
52
|
-
|
|
53
|
-
def mock_resource_string(self, name, filename):
|
|
54
|
-
target = self._resource_path(name, filename)
|
|
55
|
-
return target.read()
|
|
56
|
-
|
|
57
|
-
def mock_resource_filename(self, name, filename):
|
|
58
|
-
return str(self._resource_path(name, filename))
|
|
59
|
-
|
|
60
|
-
def mock_resource_isdir(self, name, dirname):
|
|
61
|
-
return self._resource_path(name, dirname).check(dir=1, exists=1)
|
|
62
|
-
|
|
63
|
-
def mock_get_enabled_entrypoints(self, entrypoint, app):
|
|
64
|
-
return {plugin: self._load_module(plugin, self.root / plugin) for plugin in self.enabled}
|
|
65
|
-
|
|
66
|
-
def mock_get_plugin_module(self, entrypoint, app, plugin):
|
|
67
|
-
return self._load_module(plugin, self.root / plugin)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.fixture
|
|
71
|
-
def mock(app, tmpdir, mocker):
|
|
72
|
-
"""
|
|
73
|
-
Mock migrations files
|
|
74
|
-
"""
|
|
75
|
-
m = MigrationsMock(tmpdir)
|
|
76
|
-
mocker.patch("udata.migrations.resource_listdir", side_effect=m.mock_resource_listdir)
|
|
77
|
-
mocker.patch("udata.migrations.resource_isdir", side_effect=m.mock_resource_isdir)
|
|
78
|
-
mocker.patch("udata.migrations.resource_string", side_effect=m.mock_resource_string)
|
|
79
|
-
mocker.patch("udata.migrations.resource_filename", side_effect=m.mock_resource_filename)
|
|
80
|
-
mocker.patch("udata.entrypoints.get_enabled", side_effect=m.mock_get_enabled_entrypoints)
|
|
81
|
-
mocker.patch("udata.entrypoints.get_plugin_module", side_effect=m.mock_get_plugin_module)
|
|
82
|
-
yield m
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class MigrationsTest(PytestOnlyDBTestCase):
|
|
86
|
-
@pytest.fixture
|
|
87
|
-
def db(self):
|
|
88
|
-
return get_db()
|
|
89
|
-
|
|
90
|
-
@pytest.mark.parametrize(
|
|
91
|
-
"args",
|
|
92
|
-
[
|
|
93
|
-
("udata", "test.py"),
|
|
94
|
-
("udata", "test"),
|
|
95
|
-
("udata:test.py", None),
|
|
96
|
-
("udata:test.py", None),
|
|
97
|
-
],
|
|
98
|
-
)
|
|
99
|
-
def test_get_migration(self, args):
|
|
100
|
-
migration = migrations.get(*args)
|
|
101
|
-
|
|
102
|
-
assert isinstance(migration, migrations.Migration)
|
|
103
|
-
assert migration.plugin == "udata"
|
|
104
|
-
assert migration.filename == "test.py"
|
|
105
|
-
|
|
106
|
-
def test_list_available_migrations(self, mock):
|
|
107
|
-
mock.add_migration("udata", "01_core_migration.py")
|
|
108
|
-
mock.add_migration("test", "02_test_migration.py")
|
|
109
|
-
mock.add_migration("other", "03_other_migration.py")
|
|
110
|
-
# Should not list `__*.py` files
|
|
111
|
-
mock.add_migration("test", "__private.py")
|
|
112
|
-
# Should not list migrations for disabled plugin
|
|
113
|
-
mock.add_migration("disabled", "should_not_be_there.py", enable=False)
|
|
114
|
-
# Should not fail on plugins without migrations dir
|
|
115
|
-
mock.ensure_plugin("nomigrations")
|
|
116
|
-
|
|
117
|
-
availables = migrations.list_available()
|
|
118
|
-
|
|
119
|
-
assert len(availables) == 3
|
|
120
|
-
assert availables == [
|
|
121
|
-
migrations.Migration(p, f)
|
|
122
|
-
for p, f in (
|
|
123
|
-
("udata", "01_core_migration.py"),
|
|
124
|
-
("test", "02_test_migration.py"),
|
|
125
|
-
("other", "03_other_migration.py"),
|
|
126
|
-
)
|
|
127
|
-
]
|
|
128
|
-
|
|
129
|
-
def test_get_record(self, db):
|
|
130
|
-
inserted = {
|
|
131
|
-
"plugin": "test",
|
|
132
|
-
"filename": "filename.py",
|
|
133
|
-
"ops": [
|
|
134
|
-
{
|
|
135
|
-
"date": datetime.utcnow(),
|
|
136
|
-
"type": "migrate",
|
|
137
|
-
"script": "script",
|
|
138
|
-
"output": "output",
|
|
139
|
-
"success": True,
|
|
140
|
-
}
|
|
141
|
-
],
|
|
142
|
-
}
|
|
143
|
-
db.migrations.insert_one(inserted)
|
|
144
|
-
|
|
145
|
-
record = migrations.get("test", "filename.py").record
|
|
146
|
-
|
|
147
|
-
assert record["plugin"] == inserted["plugin"]
|
|
148
|
-
assert record["filename"] == inserted["filename"]
|
|
149
|
-
|
|
150
|
-
op = record["ops"][0]
|
|
151
|
-
assert op["script"] == inserted["ops"][0]["script"]
|
|
152
|
-
assert op["output"] == inserted["ops"][0]["output"]
|
|
153
|
-
assert op["type"] == inserted["ops"][0]["type"]
|
|
154
|
-
assert op["success"]
|
|
155
|
-
assert_equal_dates(op["date"], inserted["ops"][0]["date"])
|
|
156
|
-
|
|
157
|
-
def test_migration_execute(self, mock, db):
|
|
158
|
-
mock.add_migration(
|
|
159
|
-
"udata",
|
|
160
|
-
"migration.py",
|
|
161
|
-
"""\
|
|
162
|
-
import logging
|
|
163
10
|
|
|
164
|
-
log = logging.getLogger(__name__)
|
|
165
|
-
|
|
166
|
-
def migrate(db):
|
|
167
|
-
db.test.insert_one({'key': 'value'})
|
|
168
|
-
log.info('test')
|
|
169
|
-
""",
|
|
170
|
-
)
|
|
171
11
|
|
|
172
|
-
|
|
12
|
+
class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
13
|
+
"""Test migration commands without mocking, using real migration files"""
|
|
173
14
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
assert output == [["info", "test"]]
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def db(self):
|
|
17
|
+
return get_db()
|
|
178
18
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def migration_file(self, tmp_path):
|
|
21
|
+
"""Create a temporary migration file in udata/migrations"""
|
|
22
|
+
migrations_dir = Path(__file__).parent.parent / "migrations"
|
|
23
|
+
migration_path = migrations_dir / "test_migration_temp.py"
|
|
184
24
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
assert op["output"] == [["info", "test"]]
|
|
188
|
-
assert op["state"] == {}
|
|
189
|
-
assert isinstance(op["date"], datetime)
|
|
190
|
-
assert op["success"]
|
|
191
|
-
|
|
192
|
-
def test_migration_add_record(self, mock, db):
|
|
193
|
-
mock.add_migration(
|
|
194
|
-
"test",
|
|
195
|
-
"filename.py",
|
|
25
|
+
# Create a simple migration
|
|
26
|
+
migration_content = dedent(
|
|
196
27
|
"""\
|
|
197
|
-
|
|
28
|
+
'''Test migration for integration testing'''
|
|
29
|
+
import logging
|
|
198
30
|
|
|
199
|
-
|
|
200
|
-
pass
|
|
201
|
-
""",
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
expected_output = [["info", "Recorded only"]]
|
|
205
|
-
|
|
206
|
-
output = migrations.get("test", "filename.py").execute(recordonly=True)
|
|
207
|
-
|
|
208
|
-
assert output == expected_output
|
|
209
|
-
|
|
210
|
-
migration = db.migrations.find_one()
|
|
211
|
-
assert migration["plugin"] == "test"
|
|
212
|
-
assert migration["filename"] == "filename.py"
|
|
213
|
-
|
|
214
|
-
op = migration["ops"][0]
|
|
215
|
-
assert op["script"].startswith("# whatever\n")
|
|
216
|
-
assert op["output"] == expected_output
|
|
217
|
-
assert op["type"] == "migrate"
|
|
218
|
-
assert op["success"]
|
|
219
|
-
|
|
220
|
-
def test_record_migration(self, mock, db):
|
|
221
|
-
mock.add_migration(
|
|
222
|
-
"test",
|
|
223
|
-
"filename.py",
|
|
224
|
-
"""\
|
|
225
|
-
# whatever
|
|
31
|
+
log = logging.getLogger(__name__)
|
|
226
32
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
33
|
+
def migrate(db):
|
|
34
|
+
db.test_collection.insert_one({'test': 'value'})
|
|
35
|
+
log.info('Migration executed successfully')
|
|
36
|
+
"""
|
|
230
37
|
)
|
|
38
|
+
migration_path.write_text(migration_content)
|
|
231
39
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
output = migrations.get("test", "filename.py").execute(recordonly=True)
|
|
40
|
+
yield "test_migration_temp.py"
|
|
235
41
|
|
|
236
|
-
|
|
42
|
+
# Cleanup
|
|
43
|
+
if migration_path.exists():
|
|
44
|
+
migration_path.unlink()
|
|
237
45
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
46
|
+
def test_list_available_migrations(self):
|
|
47
|
+
"""Test that we can list available migrations"""
|
|
48
|
+
result = self.cli("db status")
|
|
49
|
+
assert result.exit_code == 0
|
|
50
|
+
# Should contain at least some output (may be empty if no migrations)
|
|
241
51
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
assert op["output"] == expected_output
|
|
245
|
-
assert op["type"] == "migrate"
|
|
246
|
-
assert op["success"]
|
|
52
|
+
def test_migration_workflow(self, db, migration_file):
|
|
53
|
+
"""Test complete migration workflow: info, migrate, status, unrecord"""
|
|
247
54
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
assert
|
|
55
|
+
# 1. Test info command on non-executed migration
|
|
56
|
+
result = self.cli(f"db info {migration_file}")
|
|
57
|
+
assert result.exit_code == 0
|
|
58
|
+
assert "Test migration for integration testing" in result.output
|
|
59
|
+
assert "Not applied" in result.output
|
|
252
60
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
migrations.get("test", "filename.py").execute()
|
|
257
|
-
assert db.migrations.find_one() is None
|
|
258
|
-
|
|
259
|
-
def test_execute_migration_error(self, mock, db):
|
|
260
|
-
mock.add_migration(
|
|
261
|
-
"udata",
|
|
262
|
-
"migration.py",
|
|
263
|
-
"""\
|
|
264
|
-
import logging
|
|
265
|
-
|
|
266
|
-
log = logging.getLogger(__name__)
|
|
267
|
-
|
|
268
|
-
def migrate(db):
|
|
269
|
-
db.test.insert_one({'key': 'value'})
|
|
270
|
-
log.info('test')
|
|
271
|
-
raise ValueError('error')
|
|
272
|
-
""",
|
|
273
|
-
)
|
|
61
|
+
# 2. Test migrate command
|
|
62
|
+
result = self.cli("db migrate")
|
|
63
|
+
assert result.exit_code == 0
|
|
274
64
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
exc = excinfo.value
|
|
279
|
-
assert isinstance(exc, migrations.MigrationError)
|
|
280
|
-
assert isinstance(exc.exc, ValueError)
|
|
281
|
-
assert exc.msg == "Error while executing migration"
|
|
282
|
-
assert exc.output == [["info", "test"]]
|
|
283
|
-
|
|
284
|
-
# Without rollback DB is left as it is
|
|
285
|
-
assert db.test.count_documents({}) == 1
|
|
286
|
-
inserted = db.test.find_one()
|
|
65
|
+
# Verify migration was executed
|
|
66
|
+
inserted = db.test_collection.find_one()
|
|
287
67
|
assert inserted is not None
|
|
288
|
-
assert inserted["
|
|
68
|
+
assert inserted["test"] == "value"
|
|
289
69
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
record
|
|
293
|
-
assert record["
|
|
294
|
-
assert record["filename"] == "migration.py"
|
|
70
|
+
# Verify migration was recorded
|
|
71
|
+
record = db.migrations.find_one({"filename": migration_file})
|
|
72
|
+
assert record is not None
|
|
73
|
+
assert record["filename"] == migration_file
|
|
295
74
|
assert len(record["ops"]) == 1
|
|
75
|
+
assert record["ops"][0]["type"] == "migrate"
|
|
76
|
+
assert record["ops"][0]["success"] is True
|
|
296
77
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
assert
|
|
300
|
-
assert
|
|
301
|
-
assert isinstance(op["date"], datetime)
|
|
302
|
-
assert not op["success"]
|
|
303
|
-
|
|
304
|
-
def test_execute_migration_error_with_rollback(self, mock, db):
|
|
305
|
-
mock.add_migration(
|
|
306
|
-
"udata",
|
|
307
|
-
"migration.py",
|
|
308
|
-
"""\
|
|
309
|
-
def migrate(db):
|
|
310
|
-
db.test.insert_one({'key': 'value'})
|
|
311
|
-
raise ValueError('error')
|
|
312
|
-
|
|
313
|
-
def rollback(db):
|
|
314
|
-
db.rollback_test.insert_one({'key': 'value'})
|
|
315
|
-
""",
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
with pytest.raises(migrations.MigrationError) as excinfo:
|
|
319
|
-
migrations.get("udata", "migration.py").execute()
|
|
78
|
+
# 3. Test status command after migration
|
|
79
|
+
result = self.cli("db status")
|
|
80
|
+
assert result.exit_code == 0
|
|
81
|
+
assert migration_file.replace(".py", "") in result.output
|
|
320
82
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
assert
|
|
324
|
-
assert
|
|
83
|
+
# 4. Test info command on executed migration
|
|
84
|
+
result = self.cli(f"db info {migration_file}")
|
|
85
|
+
assert result.exit_code == 0
|
|
86
|
+
assert "Test migration for integration testing" in result.output
|
|
325
87
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
assert
|
|
88
|
+
# 5. Test unrecord command
|
|
89
|
+
result = self.cli(f"db unrecord {migration_file}")
|
|
90
|
+
assert result.exit_code == 0
|
|
329
91
|
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
assert db.rollback_test.count_documents({}) == 1
|
|
92
|
+
# Verify migration record was removed
|
|
93
|
+
record = db.migrations.find_one({"filename": migration_file})
|
|
94
|
+
assert record is None
|
|
334
95
|
|
|
335
|
-
#
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
assert
|
|
339
|
-
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
assert
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
assert
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"""
|
|
96
|
+
# But data inserted by migration should still be there
|
|
97
|
+
inserted = db.test_collection.find_one()
|
|
98
|
+
assert inserted is not None
|
|
99
|
+
assert inserted["test"] == "value"
|
|
100
|
+
|
|
101
|
+
# Cleanup test data
|
|
102
|
+
db.test_collection.delete_many({})
|
|
103
|
+
|
|
104
|
+
def test_migrate_recordonly(self, db, migration_file):
|
|
105
|
+
"""Test migrate with --record flag"""
|
|
106
|
+
|
|
107
|
+
result = self.cli("db migrate --record")
|
|
108
|
+
assert result.exit_code == 0
|
|
109
|
+
|
|
110
|
+
# Migration should be recorded
|
|
111
|
+
record = db.migrations.find_one({"filename": migration_file})
|
|
112
|
+
assert record is not None
|
|
113
|
+
assert record["ops"][0]["output"] == [["info", "Recorded only"]]
|
|
114
|
+
|
|
115
|
+
# But data should NOT be inserted
|
|
116
|
+
inserted = db.test_collection.find_one()
|
|
117
|
+
assert inserted is None
|
|
118
|
+
|
|
119
|
+
# Cleanup
|
|
120
|
+
db.migrations.delete_one({"filename": migration_file})
|
|
121
|
+
|
|
122
|
+
def test_migrate_dry_run(self, db, migration_file):
|
|
123
|
+
"""Test migrate with --dry-run flag"""
|
|
124
|
+
|
|
125
|
+
result = self.cli("db migrate --dry-run")
|
|
126
|
+
assert result.exit_code == 0
|
|
127
|
+
|
|
128
|
+
# Migration should NOT be recorded
|
|
129
|
+
record = db.migrations.find_one({"filename": migration_file})
|
|
130
|
+
assert record is None
|
|
131
|
+
|
|
132
|
+
# Data should NOT be inserted
|
|
133
|
+
inserted = db.test_collection.find_one()
|
|
134
|
+
assert inserted is None
|
|
135
|
+
|
|
136
|
+
def test_migrate_already_applied(self, db, migration_file):
|
|
137
|
+
"""Test that already applied migrations are skipped"""
|
|
138
|
+
|
|
139
|
+
# First migration
|
|
140
|
+
result = self.cli("db migrate")
|
|
141
|
+
assert result.exit_code == 0
|
|
142
|
+
|
|
143
|
+
# Count records
|
|
144
|
+
count_before = db.test_collection.count_documents({})
|
|
145
|
+
|
|
146
|
+
# Second migration attempt
|
|
147
|
+
result = self.cli("db migrate")
|
|
148
|
+
assert result.exit_code == 0
|
|
149
|
+
assert "Skipped" in result.output
|
|
150
|
+
|
|
151
|
+
# No new records should be inserted
|
|
152
|
+
count_after = db.test_collection.count_documents({})
|
|
153
|
+
assert count_before == count_after
|
|
154
|
+
|
|
155
|
+
# Cleanup
|
|
156
|
+
db.test_collection.delete_many({})
|
|
157
|
+
db.migrations.delete_one({"filename": migration_file})
|
|
158
|
+
|
|
159
|
+
def test_unrecord_with_complete_filename(self, db):
|
|
160
|
+
"""Should unrecord migration with complete filename"""
|
|
161
|
+
db.migrations.insert_one(
|
|
162
|
+
{
|
|
163
|
+
"filename": "test.py",
|
|
164
|
+
"ops": [
|
|
165
|
+
{
|
|
166
|
+
"date": datetime.utcnow(),
|
|
167
|
+
"type": "migrate",
|
|
168
|
+
"script": 'print("ok")',
|
|
169
|
+
"output": "ok",
|
|
170
|
+
"success": True,
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
}
|
|
377
174
|
)
|
|
175
|
+
result = self.cli("db unrecord test.py")
|
|
176
|
+
assert result.exit_code == 0
|
|
177
|
+
assert db.migrations.count_documents({}) == 0
|
|
378
178
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
assert db.rollback_test.count_documents({}) == 1
|
|
395
|
-
|
|
396
|
-
# DB is rollbacked if possible
|
|
397
|
-
assert db.migrations.count_documents({}) == 1
|
|
398
|
-
record = db.migrations.find_one()
|
|
399
|
-
assert record["plugin"] == "udata"
|
|
400
|
-
assert record["filename"] == "migration.py"
|
|
401
|
-
# Both failed migration and rollback are recorded
|
|
402
|
-
assert len(record["ops"]) == 2
|
|
403
|
-
|
|
404
|
-
# First the migration
|
|
405
|
-
op = record["ops"][0]
|
|
406
|
-
assert op["type"] == "migrate"
|
|
407
|
-
assert op["output"] == []
|
|
408
|
-
assert op["state"] == {"first": True}
|
|
409
|
-
assert isinstance(op["date"], datetime)
|
|
410
|
-
assert not op["success"]
|
|
411
|
-
|
|
412
|
-
# Then the rollback
|
|
413
|
-
op = record["ops"][1]
|
|
414
|
-
assert op["type"] == "rollback"
|
|
415
|
-
assert op["output"] == []
|
|
416
|
-
assert op["state"] == {"first": True}
|
|
417
|
-
assert isinstance(op["date"], datetime)
|
|
418
|
-
assert op["success"]
|
|
419
|
-
|
|
420
|
-
def test_execute_migration_error_with_rollback_error(self, mock, db):
|
|
421
|
-
mock.add_migration(
|
|
422
|
-
"udata",
|
|
423
|
-
"migration.py",
|
|
424
|
-
"""\
|
|
425
|
-
def migrate(db):
|
|
426
|
-
db.test.insert_one({'key': 'value'})
|
|
427
|
-
raise ValueError('error')
|
|
428
|
-
|
|
429
|
-
def rollback(db):
|
|
430
|
-
db.rollback_test.insert_one({'key': 'value'})
|
|
431
|
-
raise ValueError('error')
|
|
432
|
-
""",
|
|
179
|
+
def test_unrecord_without_parameters(self, db):
|
|
180
|
+
"""Should fail when no filename is provided"""
|
|
181
|
+
db.migrations.insert_one(
|
|
182
|
+
{
|
|
183
|
+
"filename": "test.py",
|
|
184
|
+
"ops": [
|
|
185
|
+
{
|
|
186
|
+
"date": datetime.utcnow(),
|
|
187
|
+
"type": "migrate",
|
|
188
|
+
"script": 'print("ok")',
|
|
189
|
+
"output": "ok",
|
|
190
|
+
"success": True,
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
}
|
|
433
194
|
)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
migrations.get("udata", "migration.py").execute()
|
|
437
|
-
|
|
438
|
-
exc = excinfo.value
|
|
439
|
-
assert isinstance(exc, migrations.RollbackError)
|
|
440
|
-
assert isinstance(exc.exc, ValueError)
|
|
441
|
-
assert exc.msg == "Error while executing migration rollback"
|
|
442
|
-
|
|
443
|
-
assert isinstance(exc.migrate_exc, migrations.MigrationError)
|
|
444
|
-
assert isinstance(exc.migrate_exc.exc, ValueError)
|
|
445
|
-
assert exc.migrate_exc.msg == "Error while executing migration"
|
|
446
|
-
|
|
447
|
-
# Migrate value is inserted
|
|
448
|
-
assert db.test.count_documents({}) == 1
|
|
449
|
-
# Rollback should not be recorded
|
|
450
|
-
assert db.rollback_test.count_documents({}) == 1
|
|
451
|
-
|
|
452
|
-
# DB is rollbacked if possible
|
|
195
|
+
result = self.cli("db unrecord", expect_error=True)
|
|
196
|
+
assert result.exit_code != 0
|
|
453
197
|
assert db.migrations.count_documents({}) == 1
|
|
454
|
-
record = db.migrations.find_one()
|
|
455
|
-
assert record["plugin"] == "udata"
|
|
456
|
-
assert record["filename"] == "migration.py"
|
|
457
|
-
# Both failed migration and rollback are recorded
|
|
458
|
-
assert len(record["ops"]) == 2
|
|
459
|
-
|
|
460
|
-
# First the migration
|
|
461
|
-
op = record["ops"][0]
|
|
462
|
-
assert op["type"] == "migrate"
|
|
463
|
-
assert op["output"] == []
|
|
464
|
-
assert op["state"] == {}
|
|
465
|
-
assert isinstance(op["date"], datetime)
|
|
466
|
-
assert not op["success"]
|
|
467
|
-
|
|
468
|
-
# Then the rollback
|
|
469
|
-
op = record["ops"][1]
|
|
470
|
-
assert op["type"] == "rollback"
|
|
471
|
-
assert op["output"] == []
|
|
472
|
-
assert op["state"] == {}
|
|
473
|
-
assert isinstance(op["date"], datetime)
|
|
474
|
-
assert not op["success"]
|
|
475
|
-
|
|
476
|
-
def test_execute_migration_dry_run(self, mock, db):
|
|
477
|
-
mock.add_migration(
|
|
478
|
-
"udata",
|
|
479
|
-
"migration.py",
|
|
480
|
-
"""\
|
|
481
|
-
import logging
|
|
482
198
|
|
|
483
|
-
|
|
199
|
+
def test_all_existing_migrations_can_run(self, db):
|
|
200
|
+
"""Test that all existing migrations can be executed without errors on a clean database"""
|
|
201
|
+
# Get all available migrations
|
|
202
|
+
all_migrations = migrations.list_available()
|
|
484
203
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
""",
|
|
489
|
-
)
|
|
204
|
+
# Run migrations
|
|
205
|
+
result = self.cli("db migrate")
|
|
206
|
+
assert result.exit_code == 0
|
|
490
207
|
|
|
491
|
-
|
|
208
|
+
# Verify all migrations were recorded successfully
|
|
209
|
+
for migration in all_migrations:
|
|
210
|
+
record = db.migrations.find_one({"filename": migration.filename})
|
|
211
|
+
assert record is not None, f"Migration {migration.filename} was not recorded"
|
|
212
|
+
assert len(record["ops"]) > 0, f"Migration {migration.filename} has no operations"
|
|
492
213
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
"filename": "filename.py",
|
|
501
|
-
"ops": [
|
|
502
|
-
{
|
|
503
|
-
"date": datetime.utcnow(),
|
|
504
|
-
"type": "migrate",
|
|
505
|
-
"script": "script",
|
|
506
|
-
"output": "output",
|
|
507
|
-
"state": {},
|
|
508
|
-
"success": True,
|
|
509
|
-
}
|
|
510
|
-
],
|
|
511
|
-
}
|
|
512
|
-
db.migrations.insert_one(inserted)
|
|
513
|
-
|
|
514
|
-
migration = migrations.get("test", "filename.py")
|
|
515
|
-
|
|
516
|
-
# Remove the migration record, return True
|
|
517
|
-
assert migration.unrecord()
|
|
518
|
-
assert db.migrations.find_one() is None
|
|
519
|
-
# Already removed, return False
|
|
520
|
-
assert not migration.unrecord()
|
|
214
|
+
last_op = record["ops"][-1]
|
|
215
|
+
assert last_op["success"], (
|
|
216
|
+
f"Migration {migration.filename} failed: {last_op.get('output', 'No output')}"
|
|
217
|
+
)
|
|
218
|
+
assert last_op["type"] == "migrate", (
|
|
219
|
+
f"Migration {migration.filename} last op is not migrate"
|
|
220
|
+
)
|