forked from arechen/EapilSkillMarket
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed.
127 lines
5.7 KiB
Python
127 lines
5.7 KiB
Python
import importlib.util
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from openpyxl import Workbook
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
COMMON = ROOT / "config/skill-overrides/common"
|
|
|
|
|
|
def load_module(name):
|
|
spec = importlib.util.spec_from_file_location(name, COMMON / f"{name}.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def load_recalc():
|
|
office = types.ModuleType("office.soffice")
|
|
office.run_soffice = lambda *args, **kwargs: None
|
|
spec = importlib.util.spec_from_file_location("market_recalc", ROOT / "config/skill-overrides/xlsx/recalc.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
with patch.dict(sys.modules, {"office.soffice": office}):
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class OfficePortabilityTests(unittest.TestCase):
|
|
def test_windows_install_path_and_arguments_with_spaces(self):
|
|
office = load_module("soffice")
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
program_files = Path(temporary) / "Program Files"
|
|
executable = program_files / "LibreOffice/program/soffice.com"
|
|
executable.parent.mkdir(parents=True)
|
|
executable.touch()
|
|
with patch.object(office.platform, "system", return_value="Windows"), \
|
|
patch.object(office.shutil, "which", return_value=None), \
|
|
patch.dict(office.os.environ, {"ProgramFiles": str(program_files)}, clear=True), \
|
|
patch.object(office.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)) as run:
|
|
office.run_soffice(["--headless", "--convert-to", "pdf", "中文 文件.docx"])
|
|
args, kwargs = run.call_args
|
|
self.assertEqual(args[0][0], str(executable))
|
|
self.assertEqual(args[0][-1], "中文 文件.docx")
|
|
self.assertTrue(args[0][1].startswith("-env:UserInstallation=file:"))
|
|
self.assertNotIn("LD_PRELOAD", kwargs["env"])
|
|
|
|
def test_existing_profile_is_preserved(self):
|
|
office = load_module("soffice")
|
|
with patch.object(office, "resolve_soffice_binary", return_value="soffice"), \
|
|
patch.object(office.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)) as run:
|
|
office.run_soffice(["-env:UserInstallation=file:///profile", "--headless"])
|
|
self.assertEqual(run.call_args.args[0], ["soffice", "-env:UserInstallation=file:///profile", "--headless"])
|
|
|
|
def test_package_roundtrip_and_removed_parts(self):
|
|
package = load_module("office_package")
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
source = root / "中文 输入.docx"
|
|
with zipfile.ZipFile(source, "w") as archive:
|
|
archive.writestr("[Content_Types].xml", "<Types/>")
|
|
archive.writestr("word/document.xml", "<正文/>")
|
|
archive.writestr("word/removed.xml", "<remove/>")
|
|
extracted = root / "解包"
|
|
package.unpack(source, extracted)
|
|
(extracted / "word/removed.xml").unlink()
|
|
output = root / "中文 输出.docx"
|
|
package.pack(extracted, output)
|
|
with zipfile.ZipFile(output) as archive:
|
|
self.assertNotIn("word/removed.xml", archive.namelist())
|
|
self.assertEqual(archive.read("word/document.xml").decode(), "<正文/>")
|
|
|
|
def test_package_rejects_path_escape(self):
|
|
package = load_module("office_package")
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
source = root / "bad.docx"
|
|
with zipfile.ZipFile(source, "w") as archive:
|
|
archive.writestr("../escape.txt", "bad")
|
|
with self.assertRaisesRegex(ValueError, "Unsafe ZIP entry"):
|
|
package.unpack(source, root / "unpacked")
|
|
self.assertFalse((root / "escape.txt").exists())
|
|
|
|
def test_formula_without_cache_is_not_success(self):
|
|
recalc = load_recalc()
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
source = Path(temporary) / "中文.xlsx"
|
|
workbook = Workbook()
|
|
workbook.active["A1"] = "=1+1"
|
|
workbook.save(source)
|
|
formulas, errors = recalc.inspect_workbook(source)
|
|
self.assertEqual(formulas, {"Sheet!A1"})
|
|
self.assertEqual(errors, {"MISSING_FORMULA_CACHE": ["Sheet!A1"]})
|
|
|
|
def test_recalc_timeout_preserves_original(self):
|
|
recalc = load_recalc()
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
source = Path(temporary) / "中文.xlsx"
|
|
Workbook().save(source)
|
|
before = source.read_bytes()
|
|
with patch.object(recalc, "run_soffice", side_effect=subprocess.TimeoutExpired("soffice", 1)):
|
|
report = recalc.recalc(source, 1)
|
|
self.assertIn("error", report)
|
|
self.assertEqual(source.read_bytes(), before)
|
|
|
|
def test_recalc_external_links_refused_without_conversion(self):
|
|
recalc = load_recalc()
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
source = Path(temporary) / "linked.xlsx"
|
|
with zipfile.ZipFile(source, "w") as archive:
|
|
archive.writestr("xl/externalLinks/externalLink1.xml", "<externalLink/>")
|
|
before = source.read_bytes()
|
|
with patch.object(recalc, "run_soffice") as convert:
|
|
report = recalc.recalc(source)
|
|
self.assertIn("error", report)
|
|
convert.assert_not_called()
|
|
self.assertEqual(source.read_bytes(), before)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|