1a6d8d07d0
When a tweet has no engagement metrics, _first_of() returns None for
every key, producing {"likes": None, "reposts": None, ...}. This
all-None dict propagates to signals.py where it is treated as "data
exists but is zero" rather than "no data available." Return None
instead when every engagement field is missing.
235 lines
7.9 KiB
Python
235 lines
7.9 KiB
Python
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
|
|
from lib.bird_x import parse_bird_response
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VENDORED_BIRD = REPO_ROOT / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs"
|
|
|
|
|
|
class TestBirdXEngagementZero(unittest.TestCase):
|
|
def test_zero_likes_preserved(self):
|
|
tweets = [
|
|
{
|
|
"id": "1",
|
|
"text": "test",
|
|
"permanent_url": "https://x.com/u/status/1",
|
|
"likeCount": 0,
|
|
"retweetCount": 5,
|
|
}
|
|
]
|
|
items = parse_bird_response(tweets, "test query")
|
|
self.assertEqual(0, items[0]["engagement"]["likes"])
|
|
self.assertEqual(5, items[0]["engagement"]["reposts"])
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node is required for vendored Bird tests")
|
|
class TestVendoredBirdRuntime(unittest.TestCase):
|
|
def test_check_uses_env_credentials_without_browser_cookie_dependency(self):
|
|
env = os.environ.copy()
|
|
env["AUTH_TOKEN"] = "dummy-auth"
|
|
env["CT0"] = "dummy-ct0"
|
|
|
|
result = subprocess.run(
|
|
["node", str(VENDORED_BIRD), "--check"],
|
|
cwd=REPO_ROOT,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
payload = json.loads(result.stdout)
|
|
self.assertTrue(payload["authenticated"])
|
|
self.assertEqual("env AUTH_TOKEN", payload["source"])
|
|
|
|
def test_check_with_browser_lookup_disabled_returns_json_warnings(self):
|
|
env = os.environ.copy()
|
|
env.pop("AUTH_TOKEN", None)
|
|
env.pop("CT0", None)
|
|
env["BIRD_DISABLE_BROWSER_COOKIES"] = "1"
|
|
|
|
result = subprocess.run(
|
|
["node", str(VENDORED_BIRD), "--check"],
|
|
cwd=REPO_ROOT,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(1, result.returncode, result.stderr)
|
|
payload = json.loads(result.stdout)
|
|
self.assertFalse(payload["authenticated"])
|
|
self.assertTrue(payload["warnings"])
|
|
self.assertIn("Missing auth_token", " ".join(payload["warnings"]))
|
|
|
|
def test_browser_cookie_helpers_lazy_load_sweet_cookie(self):
|
|
sweet_cookie_dir = (
|
|
REPO_ROOT
|
|
/ "scripts"
|
|
/ "lib"
|
|
/ "vendor"
|
|
/ "bird-search"
|
|
/ "lib"
|
|
/ "node_modules"
|
|
/ "@steipete"
|
|
/ "sweet-cookie"
|
|
)
|
|
if sweet_cookie_dir.exists():
|
|
self.skipTest("vendored sweet-cookie test stub already exists")
|
|
|
|
sweet_cookie_dir.mkdir(parents=True)
|
|
(sweet_cookie_dir / "package.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "@steipete/sweet-cookie",
|
|
"type": "module",
|
|
"exports": "./index.js",
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(sweet_cookie_dir / "index.js").write_text(
|
|
textwrap.dedent(
|
|
"""
|
|
export async function getCookies(options) {
|
|
const browser = options.browsers?.[0] ?? "unknown";
|
|
return {
|
|
cookies: [
|
|
{ name: "auth_token", value: `${browser}-auth`, domain: "x.com" },
|
|
{ name: "ct0", value: `${browser}-ct0`, domain: "x.com" },
|
|
],
|
|
warnings: [],
|
|
};
|
|
}
|
|
"""
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
"node",
|
|
"--input-type=module",
|
|
"-e",
|
|
textwrap.dedent(
|
|
"""
|
|
import {
|
|
extractCookiesFromSafari,
|
|
extractCookiesFromChrome,
|
|
extractCookiesFromFirefox,
|
|
} from "./scripts/lib/vendor/bird-search/lib/cookies.js";
|
|
|
|
const payload = await Promise.all([
|
|
extractCookiesFromSafari(),
|
|
extractCookiesFromChrome("Profile 1"),
|
|
extractCookiesFromFirefox("default-release"),
|
|
]);
|
|
process.stdout.write(JSON.stringify(payload));
|
|
"""
|
|
),
|
|
],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
payload = json.loads(result.stdout)
|
|
self.assertEqual("Safari", payload[0]["cookies"]["source"])
|
|
self.assertEqual('Chrome profile "Profile 1"', payload[1]["cookies"]["source"])
|
|
self.assertEqual(
|
|
'Firefox profile "default-release"', payload[2]["cookies"]["source"]
|
|
)
|
|
self.assertEqual("safari-auth", payload[0]["cookies"]["authToken"])
|
|
self.assertEqual("chrome-auth", payload[1]["cookies"]["authToken"])
|
|
self.assertEqual("firefox-auth", payload[2]["cookies"]["authToken"])
|
|
finally:
|
|
shutil.rmtree(sweet_cookie_dir, ignore_errors=True)
|
|
for path in [sweet_cookie_dir.parent, sweet_cookie_dir.parent.parent]:
|
|
try:
|
|
path.rmdir()
|
|
except OSError:
|
|
pass
|
|
|
|
def test_none_likes_when_missing(self):
|
|
tweets = [
|
|
{
|
|
"id": "1",
|
|
"text": "test tweet with no engagement fields",
|
|
"permanent_url": "https://x.com/u/status/1",
|
|
# no likeCount, like_count, or favorite_count
|
|
}
|
|
]
|
|
items = parse_bird_response(tweets, "test query")
|
|
self.assertIsNone(items[0]["engagement"])
|
|
|
|
def test_fallback_to_second_key(self):
|
|
tweets = [
|
|
{
|
|
"id": "1",
|
|
"text": "test",
|
|
"permanent_url": "https://x.com/u/status/1",
|
|
"like_count": 7,
|
|
}
|
|
]
|
|
items = parse_bird_response(tweets, "test query")
|
|
self.assertEqual(7, items[0]["engagement"]["likes"])
|
|
|
|
def test_zero_does_not_fall_through(self):
|
|
"""likeCount=0 should not fall through to like_count=10."""
|
|
tweets = [
|
|
{
|
|
"id": "1",
|
|
"text": "test",
|
|
"permanent_url": "https://x.com/u/status/1",
|
|
"likeCount": 0,
|
|
"like_count": 10,
|
|
}
|
|
]
|
|
items = parse_bird_response(tweets, "test query")
|
|
self.assertEqual(0, items[0]["engagement"]["likes"])
|
|
|
|
def test_engagement_none_when_all_fields_missing(self):
|
|
"""All-None engagement dict should become None, not propagate."""
|
|
tweets = [
|
|
{
|
|
"id": "1",
|
|
"text": "test",
|
|
"permanent_url": "https://x.com/u/status/1",
|
|
}
|
|
]
|
|
items = parse_bird_response(tweets, "test query")
|
|
self.assertIsNone(items[0]["engagement"])
|
|
|
|
def test_engagement_preserved_when_any_field_present(self):
|
|
"""Engagement dict kept when at least one metric exists."""
|
|
tweets = [
|
|
{
|
|
"id": "1",
|
|
"text": "test",
|
|
"permanent_url": "https://x.com/u/status/1",
|
|
"likeCount": 5,
|
|
}
|
|
]
|
|
items = parse_bird_response(tweets, "test query")
|
|
self.assertIsNotNone(items[0]["engagement"])
|
|
self.assertEqual(5, items[0]["engagement"]["likes"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|