| 1 | """Tests for ``ConfigLoader.save()`` — settings persistence. |
| 2 | |
| 3 | These cover the scenario fixed by the desktop settings-persistence bug: |
| 4 | the REST settings endpoint mutates the in-memory config, and on a real |
| 5 | ``config.yml`` path that mutation must round-trip to disk so it survives |
| 6 | a sidecar (and app) restart. |
| 7 | """ |
| 8 | |
| 9 | from __future__ import annotations |
| 10 | |
| 11 | import yaml |
| 12 | |
| 13 | from config import ConfigLoader |
| 14 | |
| 15 | |
| 16 | def test_save_without_config_path_is_noop(tmp_path): |
| 17 | """ConfigLoader(None) has nothing to write to; save() must return False |
| 18 | without raising so the server handler can stay oblivious.""" |
| 19 | loader = ConfigLoader(None) |
| 20 | loader.update(path=str(tmp_path), thread=9) |
| 21 | |
| 22 | assert loader.save() is False |
| 23 | |
| 24 | |
| 25 | def test_save_writes_ui_keys_to_yaml(tmp_path): |
| 26 | """After save() the target YAML must contain the UI-editable keys the |
| 27 | user tweaked — this is the core guarantee the bug was violating.""" |
| 28 | config_path = tmp_path / "config.yml" |
| 29 | |
| 30 | loader = ConfigLoader(str(config_path)) |
| 31 | loader.update(path=str(tmp_path / "downloads"), thread=12, rate_limit=3.5) |
| 32 | |
| 33 | assert loader.save() is True |
| 34 | assert config_path.exists() |
| 35 | |
| 36 | written = yaml.safe_load(config_path.read_text(encoding="utf-8")) |
| 37 | assert written["path"] == str(tmp_path / "downloads") |
| 38 | assert written["thread"] == 12 |
| 39 | assert written["rate_limit"] == 3.5 |
| 40 | |
| 41 | |
| 42 | def test_save_roundtrips_across_new_loader(tmp_path): |
| 43 | """Persistence is only useful if a fresh ConfigLoader picks the values |
| 44 | back up from disk — emulates the desktop app restarting the sidecar.""" |
| 45 | config_path = tmp_path / "config.yml" |
| 46 | |
| 47 | first = ConfigLoader(str(config_path)) |
| 48 | first.update(path=str(tmp_path / "downloads"), thread=7) |
| 49 | first.save() |
| 50 | |
| 51 | second = ConfigLoader(str(config_path)) |
| 52 | assert second.get("thread") == 7 |
| 53 | assert second.get("path") == str(tmp_path / "downloads") |
| 54 | |
| 55 | |
| 56 | def test_save_preserves_unrelated_user_keys(tmp_path): |
| 57 | """Users may have edited their config.yml by hand (e.g. ``link``, |
| 58 | ``cookies``). save() must never drop those just because the UI doesn't |
| 59 | know about them.""" |
| 60 | config_path = tmp_path / "config.yml" |
| 61 | config_path.write_text( |
| 62 | "link:\n - https://www.douyin.com/video/123\ncookies:\n sessionid_ss: abc\nthread: 2\n", |
| 63 | encoding="utf-8", |
| 64 | ) |
| 65 | |
| 66 | loader = ConfigLoader(str(config_path)) |
| 67 | loader.update(thread=8) |
| 68 | loader.save() |
| 69 | |
| 70 | written = yaml.safe_load(config_path.read_text(encoding="utf-8")) |
| 71 | # Unrelated user-authored keys survive… |
| 72 | assert written["link"] == ["https://www.douyin.com/video/123"] |
| 73 | assert written["cookies"] == {"sessionid_ss": "abc"} |
| 74 | # …and the UI-driven update is reflected. |
| 75 | assert written["thread"] == 8 |
| 76 | |
| 77 | |
| 78 | def test_save_creates_parent_directory(tmp_path): |
| 79 | """Desktop writes config to ``<userData>/config.yml``; on a fresh install |
| 80 | that directory may not exist yet. save() must materialise it.""" |
| 81 | config_path = tmp_path / "nested" / "subdir" / "config.yml" |
| 82 | |
| 83 | loader = ConfigLoader(str(config_path)) |
| 84 | loader.update(thread=4) |
| 85 | assert loader.save() is True |
| 86 | assert config_path.exists() |
| 87 | |
| 88 | |
| 89 | def test_save_nested_sub_models(tmp_path): |
| 90 | """Nested sub-models (comments/live/transcript/notifications) must be |
| 91 | written as nested dicts, not flattened or dropped.""" |
| 92 | config_path = tmp_path / "config.yml" |
| 93 | |
| 94 | loader = ConfigLoader(str(config_path)) |
| 95 | loader.update( |
| 96 | comments={"enabled": True, "max_comments": 100}, |
| 97 | notifications={"enabled": True, "providers": [{"type": "bark", "url": "https://x"}]}, |
| 98 | ) |
| 99 | loader.save() |
| 100 | |
| 101 | written = yaml.safe_load(config_path.read_text(encoding="utf-8")) |
| 102 | assert written["comments"]["enabled"] is True |
| 103 | assert written["comments"]["max_comments"] == 100 |
| 104 | assert written["notifications"]["enabled"] is True |
| 105 | assert written["notifications"]["providers"] == [{"type": "bark", "url": "https://x"}] |
| 106 |