mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 23:18:30 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1cd1b3bbb |
+1
-1
@@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Perf harness now measures 2D-highway frame time (R3c gate).** `scripts/perf-baseline.mjs` gains a `--song` mode that reports per-frame draw-cost p50/p95/p99 (draw-tagged via `highway.addDrawHook`), the metric that gates the `highway.js` split. Maintainer/CI-only; baseline recorded in `docs/perf-baseline.md`.
|
- **Perf harness now measures 2D-highway frame time (R3c gate).** `scripts/perf-baseline.mjs` gains a `--song` mode that reports per-frame draw-cost p50/p95/p99 (draw-tagged via `highway.addDrawHook`), the metric that gates the `highway.js` split. Maintainer/CI-only; baseline recorded in `docs/perf-baseline.md`.
|
||||||
- **`routers/` — extracting `server.py`'s route layer, cheapest-first (R3).** Each PR moves a cohesive route group into a `fastapi.APIRouter` under `lib/routers/`, mounted with `app.include_router(...)` at its original site (FastAPI matches in registration order; the full route table stays byte-identical). Bodies are verbatim — only the decorator receiver (`@app` → `@router`) and singleton reads (`meta_db` → `appstate.meta_db`, resolved at call time) change. So far: `audio_effects` (5), `artist_aliases` (5), `loops` (3), `playlists` (12 + covers), `ws_highway` (the 902-line highway chart WebSocket), `chart` (split/unsplit/work/fileinfo — unblocked by the DLC-path substrate), `library_extras`, `wanted`, `shop`, `progression`, `profile`, `stats` (the `/api/stats/{path}` catch-all stays registered last so it can't shadow `/recent` `/best` `/top`), `version` (`/api/version`; VERSION-file lookup adjusted for the router subdir depth), `art` (the `/api/song/{f}/art*` serve/cover-search/candidates/upload/url + `/api/art/{f}/override` routes; the shared `_song_pack_art_exists`/`_art_override_paths`/`_art_safe_name` helpers stay in `server.py` for the song/delete routes and are reached through the `appstate` seam, the CAA/release transport as `enrichment.X`), and `diagnostics` (`/api/diagnostics/export|preview|hardware`; the plugins-root lookup adjusted for the router subdir depth, `_running_version` reached through the `appstate` seam, pure payload-cap helpers re-exported for the `server._diag_*` tests), and `tunings` (`/api/tunings`; the pure `config.json` reader moved to `lib/appconfig.py`, the tuning-provider registry read through the `appstate` seam so plugin-contributed tunings still merge). The DLC library-path resolution (`_get_dlc_dir`, pure `_resolve_dlc_path`) moved to `lib/dlc_paths.py`, reading paths through the seam; `config_dir`/`dlc_dir`/`dlc_dir_env` now ride the `appstate` seam (env-derived, so the pop-and-reimport fixtures reconfigure it for free), and the shared request-field sanitizer `_clean_str` moved to `lib/reqfields.py`. The next cut is picked by a dependency-closure scan that ranks groups by how many `monkeypatch.setattr(server, …)` targets they'd drag along.
|
- **`routers/` — extracting `server.py`'s route layer, cheapest-first (R3).** Each PR moves a cohesive route group into a `fastapi.APIRouter` under `lib/routers/`, mounted with `app.include_router(...)` at its original site (FastAPI matches in registration order; the full route table stays byte-identical). Bodies are verbatim — only the decorator receiver (`@app` → `@router`) and singleton reads (`meta_db` → `appstate.meta_db`, resolved at call time) change. So far: `audio_effects` (5), `artist_aliases` (5), `loops` (3), `playlists` (12 + covers), `ws_highway` (the 902-line highway chart WebSocket), `chart` (split/unsplit/work/fileinfo — unblocked by the DLC-path substrate), `library_extras`, `wanted`, `shop`, `progression`, `profile`, `stats` (the `/api/stats/{path}` catch-all stays registered last so it can't shadow `/recent` `/best` `/top`), `version` (`/api/version`; VERSION-file lookup adjusted for the router subdir depth), `art` (the `/api/song/{f}/art*` serve/cover-search/candidates/upload/url + `/api/art/{f}/override` routes; the shared `_song_pack_art_exists`/`_art_override_paths`/`_art_safe_name` helpers stay in `server.py` for the song/delete routes and are reached through the `appstate` seam, the CAA/release transport as `enrichment.X`), and `settings` (`GET`/`POST /api/settings`, `/reset`, and the two-phase atomic export/import bundle `/api/settings/export|import`; the shared `_default_settings` builder stays in `server.py` and is reached through the `appstate` seam), and `diagnostics` (`/api/diagnostics/export|preview|hardware`; the plugins-root lookup adjusted for the router subdir depth, `_running_version` reached through the `appstate` seam, pure payload-cap helpers re-exported for the `server._diag_*` tests), and `tunings` (`/api/tunings`; the pure `config.json` reader moved to `lib/appconfig.py`, the tuning-provider registry read through the `appstate` seam so plugin-contributed tunings still merge). The DLC library-path resolution (`_get_dlc_dir`, pure `_resolve_dlc_path`) moved to `lib/dlc_paths.py`, reading paths through the seam; `config_dir`/`dlc_dir`/`dlc_dir_env` now ride the `appstate` seam (env-derived, so the pop-and-reimport fixtures reconfigure it for free), and the shared request-field sanitizer `_clean_str` moved to `lib/reqfields.py`. The next cut is picked by a dependency-closure scan that ranks groups by how many `monkeypatch.setattr(server, …)` targets they'd drag along.
|
||||||
- **`routers/` — the first extracted route module (R3).** The five audio-effects mapping
|
- **`routers/` — the first extracted route module (R3).** The five audio-effects mapping
|
||||||
endpoints move out of `server.py` into `lib/routers/audio_effects.py` as a
|
endpoints move out of `server.py` into `lib/routers/audio_effects.py` as a
|
||||||
`fastapi.APIRouter`, mounted with `app.include_router(...)` **at the point in the file
|
`fastapi.APIRouter`, mounted with `app.include_router(...)` **at the point in the file
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable.
|
|||||||
## Planned, NOT exempt (owned by split plans — listed so nothing falls between states)
|
## Planned, NOT exempt (owned by split plans — listed so nothing falls between states)
|
||||||
|
|
||||||
core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py`
|
core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py`
|
||||||
(5,540 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
|
(4,478 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
|
||||||
extractions and sixteen `routers/` modules (the album-art routes are now `lib/routers/art.py`); the ~930-line metadata-enrichment subsystem — MB/CAA/AcoustID transport, matcher, background worker — now lives in `lib/enrichment.py`) ·
|
extractions and seventeen `routers/` modules (album-art in `lib/routers/art.py`, the settings + export/import bundle in `lib/routers/settings.py`); the ~930-line metadata-enrichment subsystem — MB/CAA/AcoustID transport, matcher, background worker — now lives in `lib/enrichment.py`) ·
|
||||||
`lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines
|
`lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines
|
||||||
and is a monolith in its own right, to be split per-table once the router train
|
and is a monolith in its own right, to be split per-table once the router train
|
||||||
lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js`
|
lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js`
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ art_cache_dir = None
|
|||||||
song_pack_art_exists = None
|
song_pack_art_exists = None
|
||||||
art_override_paths = None
|
art_override_paths = None
|
||||||
art_safe_name = None
|
art_safe_name = None
|
||||||
|
# The canonical settings-defaults builder — stays in server.py (shared with the
|
||||||
|
# scan/artist-links code) but the settings router calls it through the seam.
|
||||||
|
default_settings = None
|
||||||
|
|
||||||
_SLOTS = frozenset({
|
_SLOTS = frozenset({
|
||||||
"meta_db", "audio_effect_mappings", "tuning_providers",
|
"meta_db", "audio_effect_mappings", "tuning_providers",
|
||||||
@@ -107,6 +110,7 @@ _SLOTS = frozenset({
|
|||||||
"get_progression_content", "builtin_diagnostic_filename",
|
"get_progression_content", "builtin_diagnostic_filename",
|
||||||
"running_version",
|
"running_version",
|
||||||
"art_cache_dir", "song_pack_art_exists", "art_override_paths", "art_safe_name",
|
"art_cache_dir", "song_pack_art_exists", "art_override_paths", "art_safe_name",
|
||||||
|
"default_settings",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ back-compat for `.sloppak` libraries or stop accepting the new `.feedpak`:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
from routers import settings as settings_router
|
||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -242,7 +243,7 @@ def test_settings_dlc_count_includes_both_suffixes(tmp_path, settings_server):
|
|||||||
(dlc / "c.FEEDPAK").write_bytes(b"") # case-insensitive (suffix.lower())
|
(dlc / "c.FEEDPAK").write_bytes(b"") # case-insensitive (suffix.lower())
|
||||||
(dlc / "notes.txt").write_bytes(b"") # ignored
|
(dlc / "notes.txt").write_bytes(b"") # ignored
|
||||||
|
|
||||||
result = settings_server.save_settings({"dlc_dir": str(dlc)})
|
result = settings_router.save_settings({"dlc_dir": str(dlc)})
|
||||||
|
|
||||||
assert "error" not in result, result
|
assert "error" not in result, result
|
||||||
# save_settings joins its notices into a single ``message`` string.
|
# save_settings joins its notices into a single ``message`` string.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ shadow the config.json dlc_dir fallback.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
|
from routers import settings as settings_router
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -32,13 +33,13 @@ class _DirectSettingsClient:
|
|||||||
def get(self, path):
|
def get(self, path):
|
||||||
if path != "/api/settings":
|
if path != "/api/settings":
|
||||||
raise ValueError(f"unsupported path: {path}")
|
raise ValueError(f"unsupported path: {path}")
|
||||||
return _DirectResponse(self._server.get_settings())
|
return _DirectResponse(settings_router.get_settings())
|
||||||
|
|
||||||
def post(self, path, json):
|
def post(self, path, json):
|
||||||
if path == "/api/settings":
|
if path == "/api/settings":
|
||||||
return _DirectResponse(self._server.save_settings(json))
|
return _DirectResponse(settings_router.save_settings(json))
|
||||||
if path == "/api/settings/reset":
|
if path == "/api/settings/reset":
|
||||||
return _DirectResponse(self._server.reset_settings(json))
|
return _DirectResponse(settings_router.reset_settings(json))
|
||||||
raise ValueError(f"unsupported path: {path}")
|
raise ValueError(f"unsupported path: {path}")
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -643,7 +644,7 @@ def test_achievements_enabled_persists_and_validates(api_client, tmp_path):
|
|||||||
|
|
||||||
def test_achievements_enabled_is_resettable(server_module):
|
def test_achievements_enabled_is_resettable(server_module):
|
||||||
"""The flag is in the resettable allow-list so a Reset clears it to default."""
|
"""The flag is in the resettable allow-list so a Reset clears it to default."""
|
||||||
assert "achievements_enabled" in server_module._RESETTABLE_SETTINGS_KEYS
|
assert "achievements_enabled" in settings_router._RESETTABLE_SETTINGS_KEYS
|
||||||
|
|
||||||
|
|
||||||
def test_skip_startup_tasks_drives_startup_to_complete(api_client):
|
def test_skip_startup_tasks_drives_startup_to_complete(api_client):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ is exercised separately in `test_plugins.py`.
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import importlib
|
import importlib
|
||||||
|
from routers import settings as settings_router
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -478,7 +479,7 @@ def test_normalize_export_paths_consistency(server_mod, tmp_path):
|
|||||||
# Wraps `_validate_relpath` to assert it doesn't raise the
|
# Wraps `_validate_relpath` to assert it doesn't raise the
|
||||||
# hard-failure ValueErrors. _UndeclaredFile would mean the
|
# hard-failure ValueErrors. _UndeclaredFile would mean the
|
||||||
# allowlist is wrong, not that the relpath shape is bad.
|
# allowlist is wrong, not that the relpath shape is bad.
|
||||||
server_mod._validate_relpath(rel, cleaned, tmp_path)
|
settings_router._validate_relpath(rel, cleaned, tmp_path)
|
||||||
|
|
||||||
|
|
||||||
# ── Atomic write: unique tmp + cleanup on failure ───────────────────────────
|
# ── Atomic write: unique tmp + cleanup on failure ───────────────────────────
|
||||||
@@ -502,7 +503,7 @@ def test_atomic_write_cleans_up_tmp_on_failure(server_mod, tmp_path, monkeypatch
|
|||||||
|
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
server_mod._atomic_write_file(target, b"payload")
|
settings_router._atomic_write_file(target, b"payload")
|
||||||
|
|
||||||
# Both attempts cleaned up. No .tmp.import residue means the
|
# Both attempts cleaned up. No .tmp.import residue means the
|
||||||
# mkstemp + finally-unlink pattern held even across failures.
|
# mkstemp + finally-unlink pattern held even across failures.
|
||||||
@@ -512,7 +513,7 @@ def test_atomic_write_cleans_up_tmp_on_failure(server_mod, tmp_path, monkeypatch
|
|||||||
|
|
||||||
# Restoring real replace, the function should still work end-to-end.
|
# Restoring real replace, the function should still work end-to-end.
|
||||||
monkeypatch.setattr(server_mod.os, "replace", real_replace)
|
monkeypatch.setattr(server_mod.os, "replace", real_replace)
|
||||||
server_mod._atomic_write_file(target, b"payload")
|
settings_router._atomic_write_file(target, b"payload")
|
||||||
assert target.read_bytes() == b"payload"
|
assert target.read_bytes() == b"payload"
|
||||||
assert list(tmp_path.glob("*.tmp.import")) == []
|
assert list(tmp_path.glob("*.tmp.import")) == []
|
||||||
|
|
||||||
@@ -764,7 +765,7 @@ def test_atomic_write_closes_fd_when_fdopen_fails(server_mod, tmp_path, monkeypa
|
|||||||
monkeypatch.setattr(server_mod.os, "fdopen", boom_fdopen)
|
monkeypatch.setattr(server_mod.os, "fdopen", boom_fdopen)
|
||||||
|
|
||||||
with pytest.raises(OSError, match="simulated EMFILE"):
|
with pytest.raises(OSError, match="simulated EMFILE"):
|
||||||
server_mod._atomic_write_file(target, b"payload")
|
settings_router._atomic_write_file(target, b"payload")
|
||||||
|
|
||||||
# fd was closed (so it didn't leak), and the temp file mkstemp
|
# fd was closed (so it didn't leak), and the temp file mkstemp
|
||||||
# created was removed (so it doesn't litter / lock on Windows).
|
# created was removed (so it doesn't litter / lock on Windows).
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ this file pins the additive `core_server_files` section:
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import importlib
|
import importlib
|
||||||
|
from routers import settings as settings_router
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -118,7 +119,7 @@ def test_import_stages_db_restore_without_touching_live_db(client, server_mod, t
|
|||||||
|
|
||||||
payload = _valid_db_bytes(tmp_path, name="incoming.db", marker="restored")
|
payload = _valid_db_bytes(tmp_path, name="incoming.db", marker="restored")
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"web_library.db": {"encoding": "base64",
|
"web_library.db": {"encoding": "base64",
|
||||||
@@ -143,7 +144,7 @@ def test_import_rejects_corrupt_db_with_valid_magic_header(client, server_mod, t
|
|||||||
# fail to open the bad restore.
|
# fail to open the bad restore.
|
||||||
corrupt = b"SQLite format 3\x00" + b"\xff" * 200
|
corrupt = b"SQLite format 3\x00" + b"\xff" * 200
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"web_library.db": {"encoding": "base64",
|
"web_library.db": {"encoding": "base64",
|
||||||
@@ -158,7 +159,7 @@ def test_import_rejects_non_sqlite_db_payload(client, server_mod, tmp_path):
|
|||||||
# A truncated / wrong file staged as the restore would brick startup —
|
# A truncated / wrong file staged as the restore would brick startup —
|
||||||
# reject anything lacking the SQLite magic header, before touching disk.
|
# reject anything lacking the SQLite magic header, before touching disk.
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"web_library.db": {"encoding": "base64",
|
"web_library.db": {"encoding": "base64",
|
||||||
@@ -171,7 +172,7 @@ def test_import_rejects_non_sqlite_db_payload(client, server_mod, tmp_path):
|
|||||||
|
|
||||||
def test_import_writes_custom_art_immediately(client, server_mod, tmp_path):
|
def test_import_writes_custom_art_immediately(client, server_mod, tmp_path):
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"playlist_covers/7.png": {"encoding": "base64",
|
"playlist_covers/7.png": {"encoding": "base64",
|
||||||
@@ -186,7 +187,7 @@ def test_import_writes_custom_art_immediately(client, server_mod, tmp_path):
|
|||||||
def test_import_core_path_traversal_rejected(client, server_mod, tmp_path):
|
def test_import_core_path_traversal_rejected(client, server_mod, tmp_path):
|
||||||
secret = tmp_path.parent / "escape.txt"
|
secret = tmp_path.parent / "escape.txt"
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"../escape.txt": {"encoding": "base64",
|
"../escape.txt": {"encoding": "base64",
|
||||||
@@ -201,7 +202,7 @@ def test_import_core_undeclared_path_skipped_not_fatal(client, server_mod, tmp_p
|
|||||||
# A relpath outside the core allowlist is a warn-and-skip, not a refusal —
|
# A relpath outside the core allowlist is a warn-and-skip, not a refusal —
|
||||||
# the rest of the bundle still applies.
|
# the rest of the bundle still applies.
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"audio_cache/x.ogg": {"encoding": "base64",
|
"audio_cache/x.ogg": {"encoding": "base64",
|
||||||
@@ -289,7 +290,7 @@ def test_full_db_backup_restore_round_trip(client, server_mod, tmp_path):
|
|||||||
def test_export_fails_hard_when_db_snapshot_unavailable(client, server_mod, monkeypatch):
|
def test_export_fails_hard_when_db_snapshot_unavailable(client, server_mod, monkeypatch):
|
||||||
# A backup that silently omits the library DB is a data-loss trap — the
|
# A backup that silently omits the library DB is a data-loss trap — the
|
||||||
# export must error rather than hand back an incomplete-looking bundle.
|
# export must error rather than hand back an incomplete-looking bundle.
|
||||||
monkeypatch.setattr(server_mod, "_snapshot_library_db", lambda: None)
|
monkeypatch.setattr(settings_router, "_snapshot_library_db", lambda: None)
|
||||||
r = client.get("/api/settings/export")
|
r = client.get("/api/settings/export")
|
||||||
assert r.status_code == 500
|
assert r.status_code == 500
|
||||||
assert "library database" in r.json()["error"].lower()
|
assert "library database" in r.json()["error"].lower()
|
||||||
@@ -299,16 +300,16 @@ def test_failed_import_disarms_staged_db_restore(client, server_mod, tmp_path, m
|
|||||||
# If a later write in phase 2 fails, the request 500s — but a staged DB
|
# If a later write in phase 2 fails, the request 500s — but a staged DB
|
||||||
# restore must NOT survive to swap in on the next restart.
|
# restore must NOT survive to swap in on the next restart.
|
||||||
payload = _valid_db_bytes(tmp_path, name="incoming.db")
|
payload = _valid_db_bytes(tmp_path, name="incoming.db")
|
||||||
real_write = server_mod._atomic_write_file
|
real_write = settings_router._atomic_write_file
|
||||||
|
|
||||||
def boom(target, data):
|
def boom(target, data):
|
||||||
if target.name == "config.json": # last write of the commit
|
if target.name == "config.json": # last write of the commit
|
||||||
raise OSError("disk full")
|
raise OSError("disk full")
|
||||||
return real_write(target, data)
|
return real_write(target, data)
|
||||||
|
|
||||||
monkeypatch.setattr(server_mod, "_atomic_write_file", boom)
|
monkeypatch.setattr(settings_router, "_atomic_write_file", boom)
|
||||||
r = client.post("/api/settings/import", json={
|
r = client.post("/api/settings/import", json={
|
||||||
"schema": server_mod.SETTINGS_BUNDLE_SCHEMA,
|
"schema": settings_router.SETTINGS_BUNDLE_SCHEMA,
|
||||||
"server_config": {},
|
"server_config": {},
|
||||||
"core_server_files": {
|
"core_server_files": {
|
||||||
"web_library.db": {"encoding": "base64",
|
"web_library.db": {"encoding": "base64",
|
||||||
|
|||||||
Reference in New Issue
Block a user