1################################################################ 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20################################################################ 21 22"""The worker's convert op: ODF and foreign formats out, OOXML in, properties passed through, and the document always closed.""" 23import zipfile 24 25import pytest 26 27from aootest.client import WorkerError 28from aootest.odfversion import MODES, odf_versions 29 30pytestmark = pytest.mark.smoke 31 32DOCX = { 33 "[Content_Types].xml": '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' 34 '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' 35 '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' 36 '<Default Extension="xml" ContentType="application/xml"/>' 37 '<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' 38 '</Types>', 39 "_rels/.rels": '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' 40 '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' 41 '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>' 42 '</Relationships>', 43 "word/document.xml": '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' 44 '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>' 45 '<w:p><w:r><w:t>Imported from OOXML</w:t></w:r></w:p></w:body></w:document>', 46} 47 48 49@pytest.fixture 50def office(aoo_offices): 51 return aoo_offices.get(None) 52 53 54@pytest.fixture 55def source(office, tmp_path): 56 """A small Writer document with known text.""" 57 path = tmp_path / "source.odt" 58 office.call("new_and_store", {"app": "writer", "dst": str(path), "filter": "writer8", "text": "Hello Convert"}, timeout=120) 59 return path 60 61 62def open_documents(office): 63 return office.call("open_documents")["documents"] 64 65 66def test_odf_to_odf_keeps_the_text_and_leaves_nothing_open(office, source, tmp_path): 67 dst = tmp_path / "out.odt" 68 assert office.call("convert", {"src": str(source), "dst": str(dst), "filter": "writer8"}, timeout=120) == {"dst": str(dst)} 69 with zipfile.ZipFile(dst) as z: 70 assert "Hello Convert" in z.read("content.xml").decode("utf-8") 71 assert open_documents(office) == [] 72 73 74def test_text_export(office, source, tmp_path): 75 dst = tmp_path / "out.txt" 76 office.call("convert", {"src": str(source), "dst": str(dst), "filter": "Text"}, timeout=120) 77 assert "Hello Convert" in dst.read_text(encoding="utf-8", errors="replace") 78 79 80def test_pdf_export(office, source, tmp_path): 81 dst = tmp_path / "out.pdf" 82 office.call("convert", {"src": str(source), "dst": str(dst), "filter": "writer_pdf_Export"}, timeout=180) 83 data = dst.read_bytes() 84 assert data.startswith(b"%PDF-") and b"%%EOF" in data[-1024:] 85 86 87def test_binary_word_export(office, source, tmp_path): 88 dst = tmp_path / "out.doc" 89 office.call("convert", {"src": str(source), "dst": str(dst), "filter": "MS Word 97"}, timeout=180) 90 assert dst.read_bytes().startswith(bytes.fromhex("D0CF11E0A1B11AE1")) 91 92 93def test_spreadsheet_to_csv(office, tmp_path): 94 ods = tmp_path / "s.ods" 95 office.call("new_and_store", {"app": "calc", "dst": str(ods), "filter": "calc8", "text": "cell one"}, timeout=120) 96 csv = tmp_path / "s.csv" 97 office.call("convert", {"src": str(ods), "dst": str(csv), "filter": "Text - txt - csv (StarCalc)"}, timeout=120) 98 assert "cell one" in csv.read_text(encoding="utf-8", errors="replace") 99 100 101def test_ooxml_is_imported_by_type_detection(office, tmp_path): 102 docx = tmp_path / "in.docx" 103 with zipfile.ZipFile(docx, "w", zipfile.ZIP_DEFLATED) as z: 104 for name, text in DOCX.items(): 105 z.writestr(name, text) 106 dst = tmp_path / "out.odt" 107 office.call("convert", {"src": str(docx), "dst": str(dst), "filter": "writer8"}, timeout=180) 108 with zipfile.ZipFile(dst) as z: 109 assert "Imported from OOXML" in z.read("content.xml").decode("utf-8") 110 111 112@pytest.mark.parametrize("mode", [3, 4]) 113def test_the_save_mode_applies_to_a_conversion(aoo_offices, source, tmp_path, mode): 114 dst = tmp_path / "out.odt" 115 aoo_offices.get(mode).call("convert", {"src": str(source), "dst": str(dst), "filter": "writer8"}, timeout=120) 116 assert odf_versions(dst) == (MODES[mode], MODES[mode]) 117 118 119def test_store_properties_reach_the_filter(office, source, tmp_path): 120 enc = tmp_path / "enc.odt" 121 office.call("convert", {"src": str(source), "dst": str(enc), "filter": "writer8", "store_props": {"Password": "secret"}}, timeout=120) 122 with zipfile.ZipFile(enc) as z: 123 assert b"encryption-data" in z.read("META-INF/manifest.xml") 124 assert "Hello Convert" not in z.read("content.xml").decode("latin-1") 125 # without the password the load fails cleanly; with it the text comes back 126 with pytest.raises(WorkerError) as e: 127 office.call("convert", {"src": str(enc), "dst": str(tmp_path / "nopw.odt"), "filter": "writer8"}, timeout=120) 128 assert e.value.kind == "uno" 129 back = tmp_path / "back.odt" 130 office.call("convert", {"src": str(enc), "dst": str(back), "filter": "writer8", "load_props": {"Password": "secret"}}, timeout=120) 131 with zipfile.ZipFile(back) as z: 132 assert "Hello Convert" in z.read("content.xml").decode("utf-8") 133 assert open_documents(office) == [] 134 135 136def test_a_missing_source_is_a_uno_error_and_the_office_stays_usable(office, tmp_path): 137 with pytest.raises(WorkerError) as e: 138 office.call("convert", {"src": str(tmp_path / "nothing.odt"), "dst": str(tmp_path / "o.odt"), "filter": "writer8"}, timeout=60) 139 assert e.value.kind == "uno" 140 assert office.call("ping") == {"pong": True} and open_documents(office) == [] 141 142 143def test_an_unknown_filter_closes_the_document_it_loaded(office, source, tmp_path): 144 with pytest.raises(WorkerError) as e: 145 office.call("convert", {"src": str(source), "dst": str(tmp_path / "o.x"), "filter": "no such filter"}, timeout=60) 146 assert e.value.kind == "uno" 147 assert open_documents(office) == [] 148 149 150@pytest.mark.parametrize("content", [b"", b"this is not a zip file at all\n"], ids=["empty", "garbage"]) 151def test_a_damaged_input_ends_cleanly(office, tmp_path, content): 152 """Either a uno error or a converted document is fine; a hang, a crash, a leaked document or a dead office is not.""" 153 src = tmp_path / "damaged.odt" 154 src.write_bytes(content) 155 dst = tmp_path / "out.odt" 156 try: 157 office.call("convert", {"src": str(src), "dst": str(dst), "filter": "writer8"}, timeout=60) 158 except WorkerError as e: 159 assert e.kind == "uno" 160 else: 161 assert zipfile.is_zipfile(dst) 162 assert office.call("ping") == {"pong": True} and open_documents(office) == [] 163