Files
yunke/yunke-server/tests/test_parsers.py
T

118 lines
4.4 KiB
Python
Raw Normal View History

2026-08-01 11:39:55 +08:00
"""Tests for the message-type normalization parser (Task 1.6).
The parser must be total — it never raises on weird input — because every
incoming WeChat record runs through it during a scrape, and one malformed
type code must not abort the whole pull. Unknown codes pass through as
``"unknown"`` so the message is still ingested (and recoverable via
``raw_json``); only the ``-2`` date-divider code maps to ``"divider"``,
which the scraper uses as its drop signal.
"""
from __future__ import annotations
import pytest
from app.scraper.parsers import (
coerce_int_or_none,
normalize_wechat_type,
normalize_whatsapp_type,
)
2026-08-01 11:39:55 +08:00
@pytest.mark.parametrize(
"raw,expected",
[
# Phase-0 confirmed codes (see docs/superpowers/specs/phase0-findings.md §2).
2026-08-01 11:39:55 +08:00
(1, "text"),
(-2, "divider"), # date separator — NOT a real message (scraper drops it)
(2, "image"), # image / sticker (webp) — file/fileTh are direct URLs
(3, "image_legacy"), # old image format — file is an opaque ID (needs another endpoint)
(4, "video"), # mp4 — file is a direct URL
(10, "system"), # system / link / card / invite — non-media, skip download
# Still-unconfirmed codes — pass through as "unknown", never dropped.
# (6, 9, 13, 15, 21, 22 — voice/file/transfer/redpacket/quote/etc; raw_json retains the code.)
(6, "unknown"),
(9, "unknown"),
(34, "unknown"), # voice (legacy WeChat code, not yet seen in yunke)
2026-08-01 11:39:55 +08:00
(49, "unknown"), # file / link / transfer
(10000, "unknown"),
# Stringified ints (the upstream API may send either form).
("1", "text"),
("-2", "divider"),
("2", "image"),
("3", "image_legacy"),
("4", "video"),
("10", "system"),
2026-08-01 11:39:55 +08:00
("34", "unknown"),
],
)
def test_normalize_wechat_type_maps_known_and_unknown(raw, expected):
assert normalize_wechat_type(raw) == expected
def test_normalize_wechat_type_never_raises():
"""Whatever we throw at it, it returns a string — never raises."""
for raw in (None, "", "junk", "1.5", [], {}, object(), float("nan")):
out = normalize_wechat_type(raw)
assert isinstance(out, str)
assert out == "unknown"
# ===========================================================================
# WhatsApp message-type normalization (M3)
# ===========================================================================
@pytest.mark.parametrize(
"raw,expected",
[
# Phase-0 confirmed WhatsApp codes (see data dictionary §3):
(0, "text"),
(1, "image"), # image / media
(-2, "divider"), # date separator — NOT a real message (scraper drops it)
# Unconfirmed / other media codes — pass through as "unknown", never dropped.
(2, "unknown"),
(3, "unknown"),
(13, "unknown"),
(100, "unknown"),
# Stringified ints (the upstream API may send either form).
("0", "text"),
("1", "image"),
("-2", "divider"),
("2", "unknown"),
],
)
def test_normalize_whatsapp_type_maps_known_and_unknown(raw, expected):
assert normalize_whatsapp_type(raw) == expected
def test_normalize_whatsapp_type_never_raises():
"""Whatever we throw at it, it returns a string — never raises."""
for raw in (None, "", "junk", "1.5", [], {}, object(), float("nan")):
out = normalize_whatsapp_type(raw)
assert isinstance(out, str)
assert out == "unknown"
# ===========================================================================
# coerce_int_or_none — shared scraper helper (M3)
# ===========================================================================
def test_coerce_int_or_none_handles_documented_cases():
"""``type_raw``-style fields: str -> int; None preserved; 0 preserved."""
assert coerce_int_or_none(None) is None
assert coerce_int_or_none(0) == 0
assert coerce_int_or_none(2) == 2
assert coerce_int_or_none(-2) == -2
assert coerce_int_or_none("0") == 0
assert coerce_int_or_none("2") == 2
assert coerce_int_or_none("-2") == -2
def test_coerce_int_or_none_returns_none_for_non_numeric_string():
"""A non-numeric value (e.g. a label) -> None."""
assert coerce_int_or_none("text") is None
assert coerce_int_or_none("") is None
assert coerce_int_or_none("unknown") is None