Export URL tokens in mini-export fixtures tripped secret-scan; replace with placeholders, stop regenerating them, and allowlist fixture paths for pre-redaction history.
204 lines
6.6 KiB
Python
204 lines
6.6 KiB
Python
"""Additional slack_export coverage — normalize, index, pagination, search."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from slack_export import SlackExport
|
|
|
|
|
|
@pytest.fixture
|
|
def export_with_synthetic(tmp_path, fixture_export_path):
|
|
"""Mini export plus synthetic message edge cases."""
|
|
ch = tmp_path / "synthetic"
|
|
ch.mkdir()
|
|
(tmp_path / "channels.json").write_text(
|
|
json.dumps([{"name": "synthetic", "id": "C1"}]),
|
|
encoding="utf-8",
|
|
)
|
|
(tmp_path / "users.json").write_text(
|
|
json.dumps(
|
|
[
|
|
{
|
|
"id": "U1",
|
|
"name": "ilia",
|
|
"profile": {"display_name": "Ilia", "real_name": "Ilia L"},
|
|
}
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
messages = [
|
|
{"type": "message", "user": "U1", "ts": "1000.1", "subtype": "channel_join"},
|
|
{
|
|
"type": "message",
|
|
"user": "U1",
|
|
"ts": "1000.2",
|
|
"subtype": "channel_topic",
|
|
"topic": "Rentals",
|
|
},
|
|
{
|
|
"type": "message",
|
|
"user": "U1",
|
|
"ts": "1000.3",
|
|
"subtype": "file_share",
|
|
"files": [{"id": "F1", "name": "invoice.pdf", "mimetype": "application/pdf"}],
|
|
},
|
|
{
|
|
"type": "message",
|
|
"user": "U1",
|
|
"ts": "1000.4",
|
|
"thread_ts": "1000.4",
|
|
"reply_count": 2,
|
|
"text": "Thread root",
|
|
},
|
|
{
|
|
"type": "message",
|
|
"user": "U1",
|
|
"ts": "1000.5",
|
|
"thread_ts": "1000.4",
|
|
"text": "Reply",
|
|
},
|
|
{"type": "message", "bot_id": "B1", "ts": "1000.6", "text": "bot says hi"},
|
|
{"type": "message", "user": "U1", "ts": "1000.7", "blocks": [{"type": "section"}]},
|
|
{"type": "message", "user": "U1", "ts": "1000.8", "attachments": [{"text": "card"}]},
|
|
]
|
|
(ch / "2016-10-10.json").write_text(json.dumps(messages), encoding="utf-8")
|
|
return SlackExport(tmp_path)
|
|
|
|
|
|
def test_classify_all_file_kinds(export):
|
|
assert export.classify_file({"name": "x.pptx"}) == "slides"
|
|
assert export.classify_file({"name": "x.mp4", "mimetype": "video/mp4"}) == "video"
|
|
assert export.classify_file({"name": "x.mp3"}) == "audio"
|
|
assert export.classify_file({"name": "x.zip"}) == "archive"
|
|
assert export.classify_file({"name": "readme.md"}) == "text"
|
|
assert export.classify_file({"name": "unknown.bin"}) == "other"
|
|
|
|
|
|
def test_normalize_message_subtypes(export_with_synthetic):
|
|
msgs = export_with_synthetic.iter_channel_messages("synthetic")
|
|
by_ts = {m["ts"]: m for m in msgs}
|
|
assert "joined" in by_ts["1000.1"]["text"]
|
|
assert "Rentals" in by_ts["1000.2"]["text"]
|
|
assert "invoice.pdf" in by_ts["1000.3"]["text"]
|
|
assert by_ts["1000.5"]["is_thread_reply"]
|
|
assert by_ts["1000.4"]["thread_reply_count"] >= 1
|
|
assert by_ts["1000.6"]["is_bot"]
|
|
assert by_ts["1000.7"]["text"] == "[rich message]"
|
|
assert by_ts["1000.8"]["text"] == "[attachment]"
|
|
|
|
|
|
def test_ensure_file_index(export_with_synthetic):
|
|
idx = export_with_synthetic.ensure_file_index()
|
|
assert "F1" in idx
|
|
assert idx["F1"]["name"] == "invoice.pdf"
|
|
|
|
|
|
def test_file_record_unknown(export):
|
|
assert export.file_record("NONEXISTENT_ID_XYZ") is None
|
|
|
|
|
|
def test_channel_messages_page_pagination(export):
|
|
page = export.channel_messages_page("zoey", limit=2, offset=0)
|
|
assert page["channel"] == "zoey"
|
|
assert len(page["messages"]) <= 2
|
|
assert page["total_in_date"] >= len(page["messages"])
|
|
if page["has_more"]:
|
|
page2 = export.channel_messages_page("zoey", limit=2, offset=2)
|
|
assert page2["offset"] == 2
|
|
|
|
|
|
def test_channel_messages_page_empty_channel(tmp_path):
|
|
empty = tmp_path / "empty_export"
|
|
empty.mkdir()
|
|
(empty / "channels.json").write_text("[]", encoding="utf-8")
|
|
(empty / "users.json").write_text("[]", encoding="utf-8")
|
|
(empty / "ghost").mkdir()
|
|
ex = SlackExport(empty)
|
|
out = ex.channel_messages_page("ghost")
|
|
assert out["messages"] == []
|
|
assert out["has_more"] is False
|
|
|
|
|
|
def test_search_messages_channel_scoped(export):
|
|
hits = export.search_messages("medical", "doctor", limit=5)
|
|
assert isinstance(hits, list)
|
|
assert export.search_messages("medical", "", limit=5) == []
|
|
|
|
|
|
def test_search_global_empty_query(export):
|
|
out = export.search_global(" ", limit=5)
|
|
assert out["channels"] == []
|
|
assert out["messages"] == []
|
|
|
|
|
|
def test_search_global_channel_name(export):
|
|
out = export.search_global("niagara", limit=10)
|
|
assert any(c["name"] == "153niagara" for c in out["channels"])
|
|
|
|
|
|
def test_search_files_with_query(export):
|
|
out = export.search_files_global("proof", file_type="all", limit=10)
|
|
assert "hits" in out
|
|
|
|
|
|
def test_channel_stats_missing(export):
|
|
stats = export.channel_stats("no-such-channel-xyz")
|
|
assert stats["exists_in_export"] is False
|
|
assert stats["message_count"] == 0
|
|
|
|
|
|
def test_read_messages_invalid_date(export):
|
|
assert export.read_messages_file("medical", "2099-99-99") == []
|
|
|
|
|
|
def test_attach_thread_reply_counts(export_with_synthetic):
|
|
msgs = export_with_synthetic.iter_channel_messages("synthetic")
|
|
assert any(m.get("thread_reply_count", 0) > 0 for m in msgs)
|
|
|
|
|
|
def test_fetch_file_bytes_local(tmp_path, export_with_synthetic):
|
|
rec = export_with_synthetic.file_record("F1")
|
|
assert rec
|
|
local = export_with_synthetic.paths.root / rec["local_relpath"]
|
|
local.parent.mkdir(parents=True, exist_ok=True)
|
|
local.write_bytes(b"%PDF-1.4 test")
|
|
data, meta = export_with_synthetic.fetch_file_bytes("F1")
|
|
assert data.startswith(b"%PDF")
|
|
assert meta["id"] == "F1"
|
|
|
|
|
|
def test_fetch_file_bytes_remote(export_with_synthetic, monkeypatch):
|
|
rec = export_with_synthetic.file_record("F1")
|
|
local = export_with_synthetic.paths.root / rec["local_relpath"]
|
|
if local.is_file():
|
|
local.unlink()
|
|
|
|
class _Resp:
|
|
def read(self):
|
|
return b"remote-bytes"
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
pass
|
|
|
|
monkeypatch.setattr("urllib.request.urlopen", lambda *a, **k: _Resp())
|
|
rec_entry = export_with_synthetic.ensure_file_index()["F1"]
|
|
rec_entry["url_private_download"] = "https://files.slack.com/x?token=xoxe-REDACTED"
|
|
|
|
data, meta = export_with_synthetic.fetch_file_bytes("F1")
|
|
assert data == b"remote-bytes"
|
|
assert local.is_file()
|
|
assert meta["local_exists"] is True
|
|
|
|
|
|
def test_fetch_file_bytes_unknown_id(export):
|
|
with pytest.raises(FileNotFoundError, match="Unknown file"):
|
|
export.fetch_file_bytes("NO_SUCH_FILE")
|