"""Display / edit / download for .xlsx files (viewer + PUT xlsx/save).""" from __future__ import annotations from pathlib import Path import pytest openpyxl = pytest.importorskip("openpyxl") VAULT = "TestVault" @pytest.fixture def xlsx_file(test_vault_dir: str) -> str: from openpyxl import Workbook path = Path(test_vault_dir) / "budget.xlsx" wb = Workbook() ws = wb.active ws.title = "Budget" ws["A1"] = "Poste" ws["B1"] = 100 ws["A2"] = "Total" ws["B2"] = "=B1*2" notes = wb.create_sheet("Notes") notes["A1"] = "hello" wb.save(path) return str(path) # ── Display ─────────────────────────────────────────────────────────────── class TestXlsxDisplay: def test_renders_all_sheets(self, client, xlsx_file): resp = client.get(f"/api/file/{VAULT}", params={"path": "budget.xlsx"}) assert resp.status_code == 200 data = resp.json() assert data["is_xlsx"] is True assert data["unsupported"] is False assert [s["name"] for s in data["xlsx_sheets"]] == ["Budget", "Notes"] first = data["xlsx_sheets"][0]["html"] assert "Poste" in first assert 'data-cell="B1"' in first assert "=B1*2" in first # formula kept as text (data_only=False) assert 'data-cell="A1"' in data["xlsx_sheets"][1]["html"] def test_corrupt_xlsx_returns_500(self, client, test_vault_dir): bad = Path(test_vault_dir) / "corrupt.xlsx" bad.write_bytes(b"this is not a zip archive") resp = client.get(f"/api/file/{VAULT}", params={"path": "corrupt.xlsx"}) assert resp.status_code == 500 # ── Index parity (tree visibility) ──────────────────────────────────────── class TestXlsxIndexing: def test_xlsx_in_supported_extensions(self): from backend.indexer import SUPPORTED_EXTENSIONS assert ".xlsx" in SUPPORTED_EXTENSIONS def test_xlsx_indexed_metadata_only(self, test_vault_dir, xlsx_file): from backend.indexer import _index_single_file_sync info = _index_single_file_sync(VAULT, test_vault_dir, xlsx_file) assert info is not None assert info["extension"] == ".xlsx" assert info["content"] == "" # binary: never read into TF-IDF assert info["title"] # filename-derived title # ── Edit ────────────────────────────────────────────────────────────────── class TestXlsxSave: def _save(self, client, body, path="budget.xlsx"): return client.put( f"/api/file/{VAULT}/xlsx/save", params={"path": path}, json=body, ) def test_save_updates_cell_with_number_coercion(self, client, xlsx_file): resp = self._save(client, {"sheet": "Budget", "cells": {"B1": "250"}}) assert resp.status_code == 200 assert resp.json()["status"] == "ok" from openpyxl import load_workbook wb = load_workbook(xlsx_file) assert wb["Budget"]["B1"].value == 250 # int, not "250" def test_save_leaves_other_sheets_and_formulas(self, client, xlsx_file): resp = self._save(client, {"sheet": "Budget", "cells": {"A1": "Titre", "B1": 42}}) assert resp.status_code == 200 from openpyxl import load_workbook wb = load_workbook(xlsx_file) assert wb["Budget"]["B2"].value == "=B1*2" assert wb["Notes"]["A1"].value == "hello" def test_save_empty_string_clears_cell(self, client, xlsx_file): resp = self._save(client, {"sheet": "Budget", "cells": {"A1": ""}}) assert resp.status_code == 200 from openpyxl import load_workbook assert load_workbook(xlsx_file)["Budget"]["A1"].value is None def test_save_creates_backup(self, client, xlsx_file): from backend.services.backups import get_backup_dir resp = self._save(client, {"sheet": "Budget", "cells": {"A1": "backup-me"}}) assert resp.status_code == 200 backup_dir = Path(get_backup_dir(VAULT, "budget.xlsx")) assert backup_dir.is_dir() assert list(backup_dir.glob("*.bak")) def test_unknown_sheet_400(self, client, xlsx_file): resp = self._save(client, {"sheet": "Nope", "cells": {"A1": "x"}}) assert resp.status_code == 400 def test_invalid_cell_ref_400(self, client, xlsx_file): resp = self._save(client, {"sheet": "Budget", "cells": {"A-1": "x"}}) assert resp.status_code == 400 def test_wrong_extension_400(self, client, test_vault_dir): (Path(test_vault_dir) / "note.md").write_text("# hi\n", encoding="utf-8") resp = self._save(client, {"sheet": "Sheet", "cells": {"A1": "x"}}, path="note.md") assert resp.status_code == 400 def test_missing_file_404(self, client): resp = self._save(client, {"sheet": "Budget", "cells": {"A1": "x"}}, path="absent.xlsx") assert resp.status_code == 404 def test_too_many_cells_400(self, client, xlsx_file): cells = {f"A{i}": i for i in range(1, 502)} resp = self._save(client, {"sheet": "Budget", "cells": cells}) assert resp.status_code == 400 def test_nested_value_400(self, client, xlsx_file): resp = self._save(client, {"sheet": "Budget", "cells": {"A1": {"nested": 1}}}) assert resp.status_code == 400 def test_missing_sheet_field_400(self, client, xlsx_file): resp = self._save(client, {"cells": {"A1": "x"}}) assert resp.status_code == 400