Files
felhom.eu/scripts/facebook/test_fb_probe.py
T
admin 5bea5f2ebf
gates / gates (push) Successful in 6m15s
marketing + seo: the three-week tester campaign, schedule-post --link, the not-indexed audit
COPY.md section 7: five Hungarian posts for Tue/Thu 19:00 over three weeks, each
with a source comment per claim and one link. The Monday post (2026-10-12, 6.2)
is untouched and no slot shares its day.

fb_probe.py gains --link (default unchanged). Every scheduled post used to
attach the home page's preview card whatever its text said, so a post about
/teszteles would have sent the people who clicked its card to the home page.
build_parser() split out of main() so a test can parse argv. Red-proved: both
new tests fail with "Namespace object has no attribute link" without the flag.

Search Console audit (seo-2026-10-10/not-indexed.md): all seven not-indexed
URLs with a verdict each - four expected by design, two waiting on Google's
crawl queue, one fault on our side. That one is /api/contact, the contact
form's POST endpoint crawled as a page; robots.txt now disallows /api/. The
performance baseline (0 clicks, 47 impressions, position 73, nine queries) is
recorded there to compare against in a month.
2026-10-10 16:13:23 +02:00

209 lines
8.4 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Tests for fb_probe.py's two secret-handling seams: the key loader (R-453) and redact()."""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fb_probe # noqa: E402
from read_credential import CredentialError # noqa: E402
FAKE = "EAAfakeprobe0123456789abcdef"
def creds(text):
fd, p = tempfile.mkstemp()
with os.fdopen(fd, "w") as fh:
fh.write(text)
return p
class LoadKey(unittest.TestCase):
def load(self, text):
p = creds(text)
try:
return fb_probe.load_key(p)
finally:
os.unlink(p)
def test_single_quotes(self):
self.assertEqual(self.load("OTHER='x'\nFACEBOOK_API='%s'\n" % FAKE), FAKE)
def test_double_quotes_and_export(self):
self.assertEqual(self.load('export FACEBOOK_API="%s"\n' % FAKE), FAKE)
def test_trailing_whitespace_stripped(self):
self.assertEqual(self.load("FACEBOOK_API='%s' \n" % FAKE), FAKE)
def test_mismatched_quote_refused(self):
with self.assertRaises(CredentialError):
self.load("FACEBOOK_API='%s\"\n" % FAKE)
def test_inner_whitespace_refused(self):
with self.assertRaises(CredentialError):
self.load("FACEBOOK_API='EAA abc'\n")
def test_not_a_meta_token_refused(self):
with self.assertRaises(CredentialError):
self.load("FACEBOOK_API='xyz123'\n")
def test_missing_key_refused(self):
with self.assertRaises(CredentialError):
self.load("FACEBOOK_APIX='%s'\n" % FAKE)
class Redact(unittest.TestCase):
def test_access_token_key_dropped_everywhere(self):
out = fb_probe.redact({"data": [{"id": "1", "access_token": "zzz"}], "access_token": "y"}, secrets=[])
self.assertEqual(out, {"data": [{"id": "1"}]})
def test_held_secret_and_token_shape_replaced(self):
out = fb_probe.redact({"a": "pre-sekret-post", "b": "x " + FAKE, "c": "fine"}, secrets=["sekret"])
self.assertEqual(out, {"a": "[REDACTED]", "b": "[REDACTED]", "c": "fine"})
def test_hungarian_text_survives(self):
self.assertEqual(fb_probe.redact(fb_probe.TEST_TEXT, secrets=[]), fb_probe.TEST_TEXT)
class GoneError(unittest.TestCase):
def test_measured_code_10_does_not_exist(self):
self.assertTrue(fb_probe.gone_error({"code": 10, "message": "(#10) Object does not exist, cannot be loaded due to missing permission"}))
def test_code_100(self):
self.assertTrue(fb_probe.gone_error({"code": 100, "message": "Unsupported get request"}))
def test_other_code_10_is_not_removal(self):
self.assertFalse(fb_probe.gone_error({"code": 10, "message": "(#10) Application does not have permission"}))
def test_success_body_is_not_removal(self):
self.assertFalse(fb_probe.gone_error(None))
# ---------------------------------------------------------------- schedule-post (R-917)
class ScheduleForm(unittest.TestCase):
"""The guard that matters: this command can only SCHEDULE. The operator's review in Planner is
the safety net, so an immediate post must be unreachable — not merely undefaulted."""
def test_published_is_always_false(self):
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600)
self.assertEqual(f["published"], "false")
def test_no_argument_can_publish_immediately(self):
# every keyword the function accepts, tried: none of them flips `published`
import inspect
names = [p for p in inspect.signature(fb_probe.schedule_form).parameters if p not in ("message", "when")]
for name in names:
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600, **{name: "true"})
self.assertEqual(f["published"], "false", "%s changed published" % name)
def test_link_is_included_by_default(self):
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600)
self.assertEqual(f["link"], "https://felhom.eu/")
def test_link_can_be_dropped(self):
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600, link=None)
self.assertNotIn("link", f)
def test_a_per_post_link_reaches_the_form(self):
"""2026-10-10: the campaign posts each point at their OWN page, and the preview CARD is what
a reader clicks. Before --link existed, every scheduled post attached https://felhom.eu/ no
matter what its text said, so a post about /teszteles sent its readers to the home page.
This pins the whole path: the CLI flag must reach the form."""
a = fb_probe.build_parser().parse_args(
["schedule-post", "--section", "7.1", "--at", "2026-10-13 19:00",
"--link", "https://felhom.eu/teszteles"])
self.assertEqual(a.link, "https://felhom.eu/teszteles")
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600, link=a.link)
self.assertEqual(f["link"], "https://felhom.eu/teszteles")
self.assertEqual(f["published"], "false") # still unreachable, even with a link set
def test_link_defaults_to_the_home_page_when_the_flag_is_absent(self):
a = fb_probe.build_parser().parse_args(["schedule-post", "--at", "2026-10-13 19:00"])
self.assertEqual(a.link, "https://felhom.eu/")
class ScheduleWindow(unittest.TestCase):
NOW = 1_760_000_000
def test_too_soon_is_refused(self):
with self.assertRaises(ValueError):
fb_probe.check_when(self.NOW + 9 * 60, now=self.NOW)
def test_in_the_past_is_refused(self):
with self.assertRaises(ValueError):
fb_probe.check_when(self.NOW - 60, now=self.NOW)
def test_too_far_is_refused(self):
with self.assertRaises(ValueError):
fb_probe.check_when(self.NOW + fb_probe.SCHED_MAX_S + 60, now=self.NOW)
def test_ten_minutes_exactly_is_allowed(self):
self.assertEqual(fb_probe.check_when(self.NOW + 600, now=self.NOW), self.NOW + 600)
def test_a_normal_time_is_allowed(self):
self.assertEqual(fb_probe.check_when(self.NOW + 86400, now=self.NOW), self.NOW + 86400)
def test_schedule_form_refuses_a_bad_time_too(self):
# the guard is not only on check_when: the form builder must not assemble a bad post
with self.assertRaises(ValueError):
fb_probe.schedule_form("x", 0)
class CopySection(unittest.TestCase):
"""The post body is READ FROM COPY.md, never passed through a shell (brief 9.6)."""
def test_reads_the_fenced_block(self):
txt = fb_probe.read_copy_section(fb_probe.COPY_MD, "6.1 Rövid változat")
self.assertIn("felhom.eu/kapcsolat", txt)
self.assertFalse(txt.startswith("```"))
self.assertFalse(txt.endswith(chr(10)))
def test_sections_differ(self):
a = fb_probe.read_copy_section(fb_probe.COPY_MD, "6.1 Rövid változat")
b = fb_probe.read_copy_section(fb_probe.COPY_MD, "6.2 Közepes változat")
self.assertNotEqual(a, b)
self.assertGreater(len(b), len(a))
def test_missing_section_raises(self):
with self.assertRaises(ValueError):
fb_probe.read_copy_section(fb_probe.COPY_MD, "9.9 nincs ilyen")
class ScheduleRedaction(unittest.TestCase):
def test_a_token_in_a_scheduled_form_is_redacted(self):
f = fb_probe.schedule_form("see " + FAKE, fb_probe.time.time() + 3600)
self.assertNotIn(FAKE, fb_probe.redact(f, secrets=[FAKE])["message"])
def _has_budapest_tz():
try:
from zoneinfo import ZoneInfo
ZoneInfo("Europe/Budapest")
return True
except Exception:
return False
@unittest.skipUnless(_has_budapest_tz(),
"no Europe/Budapest in this interpreter's tz database (Windows ships none; "
"the command runs on the Linux host, which has the system zoneinfo)")
class BudapestTime(unittest.TestCase):
def test_summer_and_winter_offsets_differ(self):
# CEST in July (+02:00), CET in January (+01:00) — the tz database, not a guessed offset
jul = fb_probe.budapest_to_epoch("2026-07-01 19:00")
jan = fb_probe.budapest_to_epoch("2026-01-01 19:00")
import datetime
self.assertEqual(datetime.datetime.utcfromtimestamp(jul).hour, 17)
self.assertEqual(datetime.datetime.utcfromtimestamp(jan).hour, 18)
def test_bad_format_raises(self):
with self.assertRaises(ValueError):
fb_probe.budapest_to_epoch("tomorrow evening")
if __name__ == "__main__":
unittest.main()