Skip to content

Commit 404b6ae

Browse files
committed
Fix settings env preservation, add Forge legacy redirects, and align KoSync safety tests
1 parent 150d013 commit 404b6ae

File tree

5 files changed

+61
-6
lines changed

5 files changed

+61
-6
lines changed

src/utils/config_loader.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,13 @@ def load_settings(db_service: DatabaseService):
152152
# Apply validation or type conversion if needed (mostly string for env vars)
153153
val_str = str(value) if value is not None else ""
154154

155-
# Update environment variable
156-
os.environ[key] = val_str
155+
# Preserve existing non-empty env vars when DB value is blank.
156+
if val_str != "":
157+
os.environ[key] = val_str
158+
else:
159+
existing_env = os.environ.get(key, "")
160+
if not existing_env:
161+
os.environ[key] = ""
157162

158163
# Mask secrets in logs
159164
log_val = "******" if any(s in key for s in ['KEY', 'PASSWORD', 'TOKEN']) else val_str

src/web_server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,10 +2105,15 @@ def create_app(test_container=None):
21052105
app.context_processor(inject_global_vars)
21062106
app.jinja_env.globals['safe_folder_name'] = safe_folder_name
21072107

2108+
def _legacy_book_linker_redirect(dummy=None):
2109+
return redirect(url_for('forge'), code=301)
2110+
21082111
# Register all routes here
21092112
app.add_url_rule('/', 'index', index)
21102113
app.add_url_rule('/shelfmark', 'shelfmark', shelfmark)
21112114
app.add_url_rule('/forge', 'forge', forge)
2115+
app.add_url_rule('/book-linker', 'book_linker_legacy', _legacy_book_linker_redirect)
2116+
app.add_url_rule('/book-linker/<path:dummy>', 'book_linker_legacy_path', _legacy_book_linker_redirect)
21122117
app.add_url_rule('/match', 'match', match, methods=['GET', 'POST'])
21132118
app.add_url_rule('/batch-match', 'batch_match', batch_match, methods=['GET', 'POST'])
21142119
app.add_url_rule('/delete/<abs_id>', 'delete_mapping', delete_mapping, methods=['POST'])

tests/base_sync_test.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def run_sync_test_with_leader_verification(self):
190190
xpath=f"/html/body/div[1]/p[{int(self.expected_final_pct * 25)}]",
191191
match_index=int(self.expected_final_pct * 20)
192192
)
193+
self._mock_locator_xpath = mock_locator.xpath
193194
mocks['ebook_parser'].find_text_location.return_value = mock_locator
194195
mocks['ebook_parser'].get_perfect_ko_xpath.return_value = mock_locator.xpath
195196

@@ -292,7 +293,16 @@ def verify_common_assertions(self, mocks, manager):
292293
manager.sync_clients['ABS']._update_abs_progress_with_offset.called)
293294
self.assertTrue(abs_updated, "ABS update was not called")
294295
if leader != 'KOSYNC':
295-
self.assertTrue(mocks['kosync_client'].update_progress.called, "KoSync update_progress was not called")
296+
# Base tests intentionally use malformed KoSync-style XPath (/html/...),
297+
# which should now be skipped by KoSync safety logic.
298+
ko_xpath = getattr(mocks['ebook_parser'].find_text_location.return_value, 'xpath', '')
299+
if isinstance(ko_xpath, str) and ko_xpath.startswith('/html/'):
300+
self.assertFalse(
301+
mocks['kosync_client'].update_progress.called,
302+
"KoSync update_progress should be skipped for malformed XPath"
303+
)
304+
else:
305+
self.assertTrue(mocks['kosync_client'].update_progress.called, "KoSync update_progress was not called")
296306
if leader != 'STORYTELLER':
297307
# [UPDATED] Check update_position
298308
if self.test_book.storyteller_uuid:
@@ -363,11 +373,20 @@ def verify_final_state(self, manager):
363373
expected_pct = self.expected_final_pct
364374
tolerance = 0.02
365375

376+
kosync_skipped = (
377+
self.get_expected_leader().upper() != "KOSYNC"
378+
and isinstance(getattr(self, '_mock_locator_xpath', ''), str)
379+
and getattr(self, '_mock_locator_xpath', '').startswith('/html/')
380+
)
381+
366382
if self.get_expected_leader() != "None":
367383
self.assertAlmostEqual(abs_pct, expected_pct, delta=tolerance,
368384
msg=f"ABS final state {abs_pct:.1%} != expected {expected_pct:.1%}")
369-
self.assertAlmostEqual(kosync_pct, expected_pct, delta=tolerance,
370-
msg=f"KoSync final state {kosync_pct:.1%} != expected {expected_pct:.1%}")
385+
if kosync_skipped:
386+
self.assertNotIn('kosync', final_states, "KoSync state should not be persisted when malformed XPath update is skipped")
387+
else:
388+
self.assertAlmostEqual(kosync_pct, expected_pct, delta=tolerance,
389+
msg=f"KoSync final state {kosync_pct:.1%} != expected {expected_pct:.1%}")
371390
if self.test_book.storyteller_uuid:
372391
self.assertAlmostEqual(storyteller_pct, expected_pct, delta=tolerance,
373392
msg=f"Storyteller final state {storyteller_pct:.1%} != expected {expected_pct:.1%}")

tests/test_abs_unittest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
import unittest
77
import sys
88
from pathlib import Path
9+
from types import SimpleNamespace
10+
from unittest.mock import Mock
911
# Add the project root to the path to resolve module imports
1012
sys.path.insert(0, str(Path(__file__).parent.parent))
1113

1214
from tests.base_sync_test import BaseSyncCycleTestCase
15+
from src.sync_clients.kosync_sync_client import KoSyncSyncClient
16+
from src.sync_clients.sync_client_interface import LocatorResult
1317

1418
class TestABSLeadsSync(BaseSyncCycleTestCase):
1519
"""Test case for ABS leading sync_cycle scenario."""
@@ -74,6 +78,28 @@ def get_progress_mock_returns(self):
7478
def test_abs_leads(self):
7579
super().run_test(10, 40)
7680

81+
def test_malformed_xpath_skips_kosync_update(self):
82+
kosync_api = Mock()
83+
ebook_parser = Mock()
84+
ebook_parser.get_sentence_level_ko_xpath.return_value = None
85+
client = KoSyncSyncClient(kosync_api, ebook_parser)
86+
87+
book = SimpleNamespace(
88+
kosync_doc_id='test-kosync-doc',
89+
ebook_filename='test-book.epub',
90+
abs_title='Test Audiobook',
91+
)
92+
request = SimpleNamespace(
93+
locator_result=LocatorResult(percentage=0.4, xpath="/html/body/div[1]/p[5]")
94+
)
95+
96+
result = client.update_progress(book, request)
97+
98+
self.assertFalse(result.success)
99+
self.assertTrue(result.updated_state.get('skipped'))
100+
self.assertIsNone(result.updated_state.get('xpath'))
101+
kosync_api.update_progress.assert_not_called()
102+
77103

78104
if __name__ == '__main__':
79105
unittest.main(verbosity=2)

tests/test_ebook_sentence_xpath_fallback.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_crengine_safe_xpath_falls_back_when_anchor_has_no_direct_text(self):
4343

4444
xpath = self.parser._build_crengine_safe_text_xpath(span, 8, html_content)
4545

46-
self.assertEqual(xpath, "/body/DocFragment[8]/body/p[1]/text().0")
46+
self.assertEqual(xpath, "/body/DocFragment[8]/body/p/text().0")
4747
self.assertNotIn("/span", xpath)
4848

4949

0 commit comments

Comments
 (0)