Skip to content

Commit da7ec5e

Browse files
committed
packagingv3: add tests for new app configuration / regenconf mechanism
1 parent 481ac2a commit da7ec5e

File tree

3 files changed

+329
-3
lines changed

3 files changed

+329
-3
lines changed

.gitlab/ci/test.gitlab-ci.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,15 @@ test-questions:
7171
script:
7272
- python3 -m pytest src/tests/test_questions.py
7373

74-
test-app-config:
74+
test-app-configpanel:
7575
extends: .test-stage
7676
script:
77-
- python3 -m pytest src/tests/test_app_config.py
77+
- python3 -m pytest src/tests/test_app_configpanel.py
78+
79+
test-app-regenconf:
80+
extends: .test-stage
81+
script:
82+
- python3 -m pytest src/tests/test_app_regenconf.py
7883

7984
test-app-resources:
8085
extends: .test-stage
@@ -147,7 +152,9 @@ coverage:
147152
artifacts: true
148153
- job: test-questions
149154
artifacts: true
150-
- job: test-app-config
155+
- job: test-app-configpanel
156+
artifacts: true
157+
- job: test-app-regenconf
151158
artifacts: true
152159
- job: test-app-resources
153160
artifacts: true
File renamed without changes.

src/tests/test_app_regenconf.py

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2025 YunoHost Contributors
4+
#
5+
# This file is part of YunoHost (see https://yunohost.org)
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Affero General Public License as
9+
# published by the Free Software Foundation, either version 3 of the
10+
# License, or (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Affero General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Affero General Public License
18+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
#
20+
21+
import glob
22+
import os
23+
import pytest
24+
25+
from moulinette.utils.filesystem import read_file, write_to_file
26+
from yunohost.app import _make_environment_for_app_script, app_setting, _get_app_settings
27+
from yunohost.utils.error import YunohostError
28+
from yunohost.utils.configurations import (
29+
BaseConfiguration,
30+
Configurations,
31+
ConfigurationsClassesByType,
32+
AppConfigurationsManager,
33+
DIR_TO_BACKUP_CONF_MANUALLY_MODIFIED,
34+
)
35+
from .conftest import message
36+
37+
38+
class DummyAppConfigurations(Configurations):
39+
type = "dummy"
40+
41+
class MainConfiguration(BaseConfiguration):
42+
template: str = "dummy.conf"
43+
path: str = "/tmp/dummyconfs/__APP__.conf"
44+
45+
class ExtraConfiguration(BaseConfiguration):
46+
template: str = "dummy-__CONF_ID__.conf"
47+
path: str = "/tmp/dummyconfs/__APP__-__CONF_ID__.conf"
48+
49+
def reload():
50+
pass
51+
52+
53+
ConfigurationsClassesByType["dummy"] = DummyAppConfigurations
54+
55+
56+
def setup_function(function):
57+
clean()
58+
59+
os.system("mkdir /etc/yunohost/apps/testapp")
60+
os.system("mkdir /etc/yunohost/apps/testapp/conf")
61+
os.system("mkdir /tmp/dummyconfs/")
62+
os.system("echo 'id: testapp' > /etc/yunohost/apps/testapp/settings.yml")
63+
os.system("echo 'foo: bar' >> /etc/yunohost/apps/testapp/settings.yml")
64+
dummy_manifest = '\n'.join([
65+
'packaging_format = 3',
66+
'id = "testapp"',
67+
'version = "0.1"',
68+
'description.en = "A dummy app to test app resources"'
69+
])
70+
write_to_file("/etc/yunohost/apps/testapp/manifest.toml", dummy_manifest)
71+
dummy_conf = '\n'.join([
72+
'# This is a dummy conf file',
73+
'APP = __APP__',
74+
'FOO = __FOO__'
75+
])
76+
write_to_file("/etc/yunohost/apps/testapp/conf/dummy.conf", dummy_conf)
77+
78+
79+
def teardown_function(function):
80+
clean()
81+
82+
83+
def clean():
84+
os.system("rm -rf /etc/yunohost/apps/testapp")
85+
os.system("rm -rf /tmp/dummyconfs/")
86+
os.system(f"rm -rf {DIR_TO_BACKUP_CONF_MANUALLY_MODIFIED}/testapp/")
87+
os.system("userdel testapp 2>/dev/null")
88+
89+
90+
def test_conf_dummy_new():
91+
workdir = "/etc/yunohost/apps/testapp/"
92+
env = _make_environment_for_app_script("testapp", workdir=workdir)
93+
wanted = {"configurations": {"dummy": {}}, "env": env}
94+
conf = "/tmp/dummyconfs/testapp.conf"
95+
assert not os.path.exists(conf)
96+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
97+
rollback_and_raise_exception_if_failure=False
98+
)
99+
assert "FOO = bar" in read_file(conf).strip()
100+
101+
settings = _get_app_settings("testapp")
102+
assert settings.get("_configurations", {}).get("dummy.main")
103+
104+
105+
def test_conf_dummy_remove():
106+
workdir = "/etc/yunohost/apps/testapp/"
107+
env = _make_environment_for_app_script("testapp", workdir=workdir)
108+
current = {"configurations": {"dummy": {}}, "env": env}
109+
conf = "/tmp/dummyconfs/testapp.conf"
110+
write_to_file(conf, "FOO = bar")
111+
assert os.path.exists(conf)
112+
AppConfigurationsManager("testapp", current=current, wanted={}, workdir=workdir).apply(
113+
rollback_and_raise_exception_if_failure=False
114+
)
115+
assert not os.path.exists(conf)
116+
117+
118+
def test_conf_dummy_dryrun():
119+
workdir = "/etc/yunohost/apps/testapp/"
120+
env = _make_environment_for_app_script("testapp", workdir=workdir)
121+
wanted = {"configurations": {"dummy": {}}, "env": env}
122+
conf = "/tmp/dummyconfs/testapp.conf"
123+
assert not os.path.exists(conf)
124+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
125+
rollback_and_raise_exception_if_failure=False, dry_run=True
126+
)
127+
assert not os.path.exists(conf)
128+
129+
130+
def test_conf_dummy_different_template():
131+
132+
write_to_file("/etc/yunohost/apps/testapp/conf/dummy2.conf", "This is another template")
133+
workdir = "/etc/yunohost/apps/testapp/"
134+
env = _make_environment_for_app_script("testapp", workdir=workdir)
135+
wanted = {"configurations": {"dummy": {"main": {"template": "dummy2.conf"}}}, "env": env}
136+
conf = "/tmp/dummyconfs/testapp.conf"
137+
assert not os.path.exists(conf)
138+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
139+
rollback_and_raise_exception_if_failure=False
140+
)
141+
assert "FOO = bar" not in read_file(conf).strip()
142+
assert "This is another template" in read_file(conf).strip()
143+
144+
145+
def test_conf_dummy_with_extra():
146+
147+
write_to_file("/etc/yunohost/apps/testapp/conf/dummy-extra.conf", "This is another template")
148+
workdir = "/etc/yunohost/apps/testapp/"
149+
env = _make_environment_for_app_script("testapp", workdir=workdir)
150+
wanted = {"configurations": {"dummy": {"extra": {}}}, "env": env}
151+
conf = "/tmp/dummyconfs/testapp.conf"
152+
conf2 = "/tmp/dummyconfs/testapp-extra.conf"
153+
assert not os.path.exists(conf)
154+
assert not os.path.exists(conf2)
155+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
156+
rollback_and_raise_exception_if_failure=False
157+
)
158+
assert "FOO = bar" in read_file(conf).strip()
159+
assert "This is another template" in read_file(conf2).strip()
160+
161+
162+
def test_conf_dummy_missingvar():
163+
workdir = "/etc/yunohost/apps/testapp/"
164+
app_setting("testapp", "foo", delete=True)
165+
env = _make_environment_for_app_script("testapp", workdir=workdir)
166+
wanted = {"configurations": {"dummy": {}}, "env": env}
167+
168+
with pytest.raises(YunohostError):
169+
with message("app_uninitialized_variables"):
170+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
171+
rollback_and_raise_exception_if_failure=True
172+
)
173+
174+
175+
def test_conf_dummy_update_after_var_change():
176+
177+
workdir = "/etc/yunohost/apps/testapp/"
178+
env = _make_environment_for_app_script("testapp", workdir=workdir)
179+
wanted = {"configurations": {"dummy": {}}, "env": env}
180+
conf = "/tmp/dummyconfs/testapp.conf"
181+
assert not os.path.exists(conf)
182+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
183+
rollback_and_raise_exception_if_failure=False
184+
)
185+
assert "FOO = bar" in read_file(conf).strip()
186+
187+
app_setting("testapp", "foo", "nyah")
188+
189+
env = _make_environment_for_app_script("testapp", workdir=workdir)
190+
current = wanted
191+
wanted = {"configurations": {"dummy": {}}, "env": env}
192+
AppConfigurationsManager("testapp", current=current, wanted=wanted, workdir=workdir).apply(
193+
rollback_and_raise_exception_if_failure=False
194+
)
195+
196+
assert "FOO = nyah" in read_file(conf).strip()
197+
198+
199+
def test_conf_dummy_update_after_path_change():
200+
201+
workdir = "/etc/yunohost/apps/testapp/"
202+
env = _make_environment_for_app_script("testapp", workdir=workdir)
203+
wanted = {"configurations": {"dummy": {}}, "env": env}
204+
conf = "/tmp/dummyconfs/testapp.conf"
205+
assert not os.path.exists(conf)
206+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
207+
rollback_and_raise_exception_if_failure=False
208+
)
209+
assert "FOO = bar" in read_file(conf).strip()
210+
211+
current = wanted
212+
conf2 = "/tmp/dummyconfs/wat.conf"
213+
wanted = {"configurations": {"dummy": {"main": {"path": conf2}}}, "env": env}
214+
AppConfigurationsManager("testapp", current=current, wanted=wanted, workdir=workdir).apply(
215+
rollback_and_raise_exception_if_failure=False
216+
)
217+
assert not os.path.exists(conf)
218+
assert "FOO = bar" in read_file(conf2).strip()
219+
220+
221+
def test_conf_dummy_update_after_template_change():
222+
223+
workdir = "/etc/yunohost/apps/testapp/"
224+
env = _make_environment_for_app_script("testapp", workdir=workdir)
225+
wanted = {"configurations": {"dummy": {}}, "env": env}
226+
conf = "/tmp/dummyconfs/testapp.conf"
227+
assert not os.path.exists(conf)
228+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
229+
rollback_and_raise_exception_if_failure=False
230+
)
231+
assert "FOO = bar" in read_file(conf).strip()
232+
233+
write_to_file("/etc/yunohost/apps/testapp/conf/dummy.conf", "# This is the updated conf template")
234+
235+
AppConfigurationsManager("testapp", current=wanted, wanted=wanted, workdir=workdir).apply(
236+
rollback_and_raise_exception_if_failure=False
237+
)
238+
239+
assert read_file(conf).strip() == "# This is the updated conf template"
240+
241+
242+
def test_conf_dummy_manualchange_mergeable():
243+
244+
workdir = "/etc/yunohost/apps/testapp/"
245+
env = _make_environment_for_app_script("testapp", workdir=workdir)
246+
wanted = {"configurations": {"dummy": {}}, "env": env}
247+
conf = "/tmp/dummyconfs/testapp.conf"
248+
assert not os.path.exists(conf)
249+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
250+
rollback_and_raise_exception_if_failure=False
251+
)
252+
assert "FOO = bar" in read_file(conf).strip()
253+
254+
write_to_file(conf, "# Manual comment on top of file\n" + read_file(conf))
255+
256+
app_setting("testapp", "foo", "nyah")
257+
258+
env = _make_environment_for_app_script("testapp", workdir=workdir)
259+
current = wanted
260+
wanted = {"configurations": {"dummy": {}}, "env": env}
261+
AppConfigurationsManager("testapp", current=current, wanted=wanted, workdir=workdir).apply(
262+
rollback_and_raise_exception_if_failure=False
263+
)
264+
265+
assert "# Manual comment on top of file" in read_file(conf).strip()
266+
assert "FOO = nyah" in read_file(conf).strip()
267+
268+
backup_conf_glob = f"{DIR_TO_BACKUP_CONF_MANUALLY_MODIFIED}/testapp/{conf}.backup.*"
269+
assert len(glob.glob(backup_conf_glob)) == 1
270+
backup_content = read_file(list(glob.glob(backup_conf_glob))[0])
271+
assert "# Manual comment on top of file" in backup_content
272+
assert "FOO = bar" in backup_content
273+
274+
275+
def test_conf_dummy_manualchange_nonmergeable():
276+
277+
workdir = "/etc/yunohost/apps/testapp/"
278+
env = _make_environment_for_app_script("testapp", workdir=workdir)
279+
wanted = {"configurations": {"dummy": {}}, "env": env}
280+
conf = "/tmp/dummyconfs/testapp.conf"
281+
assert not os.path.exists(conf)
282+
AppConfigurationsManager("testapp", current={}, wanted=wanted, workdir=workdir).apply(
283+
rollback_and_raise_exception_if_failure=False
284+
)
285+
assert "FOO = bar" in read_file(conf).strip()
286+
287+
write_to_file(conf, read_file(conf).replace("FOO = bar", "FOO = manual_value"))
288+
289+
app_setting("testapp", "foo", "nyah")
290+
291+
env = _make_environment_for_app_script("testapp", workdir=workdir)
292+
current = wanted
293+
wanted = {"configurations": {"dummy": {}}, "env": env}
294+
AppConfigurationsManager("testapp", current=current, wanted=wanted, workdir=workdir).apply(
295+
rollback_and_raise_exception_if_failure=False
296+
)
297+
298+
assert "FOO = nyah" in read_file(conf).strip()
299+
300+
backup_conf_glob = f"{DIR_TO_BACKUP_CONF_MANUALLY_MODIFIED}/testapp/{conf}.backup.*"
301+
assert len(glob.glob(backup_conf_glob)) == 1
302+
backup_content = read_file(list(glob.glob(backup_conf_glob))[0])
303+
assert "FOO = manual_value" in backup_content
304+
305+
306+
@pytest.mark.skip
307+
def test_conf_dummy_unexposed_property():
308+
raise NotImplementedError
309+
310+
311+
@pytest.mark.skip
312+
def test_conf_dummy_ifclause():
313+
raise NotImplementedError
314+
315+
316+
@pytest.mark.skip
317+
def test_conf_dummy_reload_fails():
318+
# TODO: should rollback?
319+
raise NotImplementedError

0 commit comments

Comments
 (0)