facebook: the first post drafted (COPY.md section 6) + fb_probe schedule-post
gates / gates (push) Successful in 5m58s
gates / gates (push) Successful in 5m58s
DRAFTS. COPY.md section 6: two versions of the Page's first post, shortened
from section 2. Hungarian, tegezo, no price. 6.1 = 347 characters, 6.2 = 638
(counted as Unicode characters). Every claim carries a source comment naming
the line of website/index.html it rests on; the first line of each carries the
point alone, because Facebook cuts after about three lines.
Deliberate: ZERO emoji and ZERO hashtags, though the brief allows two of each.
The Felhom design system uses no emoji (the website gate holds it at 0) and
two hashtags would serve no real search.
CHECKED, because the post repeats it: "56 alkalmazas" is CORRECT. The apps
page carries 57 <div class="app-card"> but states 56, which looks off by one
until you read the category line -- "6 alkalmazas + 1 beepitett". The 57th
card is FileBrowser, built into every box and deliberately not counted.
index.html says "56 telepitheto alkalmazas" too. I nearly "fixed" a live page
into being wrong. NOT-A-FINDING.
SCHEDULE-POST. A third sub-command on fb_probe.py, reusing its token loader
(R-453), redaction, Bearer call and evidence writer:
- the body is READ FROM COPY.md by section name. The Hungarian never passes
through a shell or an argv string (brief 9.6); the caller names a section.
- published is ALWAYS "false" and NO argument can change it (brief 9.7).
The operator's review in Planner is the safety net, so an immediate post
must be unreachable, not merely not-the-default.
- check_when refuses a time under Meta's 10-minute floor or over its
6-month ceiling, BEFORE the call, so a bad time is a readable local
refusal rather than a Graph error.
- budapest_to_epoch uses the real tz database. If zoneinfo has no
Europe/Budapest it REFUSES rather than falling back to a hardcoded
+01:00/+02:00 -- guessing the offset is how a post goes out an hour wrong
across a DST boundary.
- list_scheduled tries /scheduled_posts then feed?is_published=false and
RECORDS WHICH ANSWERED; when both are refused it returns None so the
caller says "unproven" instead of claiming a removal it never saw.
TESTS: 30, of which 2 skip on Windows (no tzdata in this interpreter; the
command runs on the Linux host, which has the system zoneinfo).
RED-PROOF of the guard that matters, as the brief requires. Made
schedule_form accept published=..., ran ScheduleForm, and watched
test_no_argument_can_publish_immediately FAIL:
AssertionError: 'true' != 'false' : published changed published
Guard restored, all 30 green again.
No post has been made. The dry check and the real post come next; the
operator picks the version and the time first.
This commit is contained in:
@@ -153,3 +153,57 @@ Válasz:
|
||||
```
|
||||
A költség két részből áll: egyszeri költségek (a hardver ára, konfigurációtól függően 80.000–400.000+ Ft, és a telepítési díj) és havi üzemeltetési díj (monitoring, támogatás, frissítések, mentés-felügyelet). A hardvert te vásárolod meg – a tiéd, örökre. Pontos árajánlatot az egyeztetés után adunk, mert az igények nagyon eltérőek lehetnek. Részletek: https://felhom.eu/gyik.html
|
||||
```
|
||||
|
||||
|
||||
## 6. Az első bejegyzés (R-917 (c) — a Page első, kiemelt posztja)
|
||||
|
||||
A §2 hosszabb leírásból rövidítve. **Minden állítás ma is ott van a `website/index.html`-en**, soronként
|
||||
megjelölve. Magyarul, tegező hangnem, ár nélkül. **Emodzsi: egy sem** — a Felhom arculata nem használ
|
||||
emodzsit (a weboldal gate-je 0-ra tartja), és egy első bemutatkozó poszton olcsóbbnak hatna, mint
|
||||
amilyen a termék. **Hashtag: egy sem** — kettő is elférne, de semmilyen valódi keresést nem szolgálna
|
||||
ki, és a poszt nem lesz tőlük megtalálhatóbb.
|
||||
|
||||
A Facebook kb. három sor után elvágja a szöveget és „Továbbiak"-at ír ki, ezért **az első két sor
|
||||
önmagában is elmondja a lényeget**, és nem bemutatkozással kezd.
|
||||
|
||||
### 6.1 Rövid változat
|
||||
|
||||
<!-- forrás: index.html:17-18 „Fényképek, dokumentumok, média — A telefonod fotói automatikusan feltöltődnek a saját gépedre" -->
|
||||
<!-- forrás: index.html:61 „Az adataid a te otthonodban maradnak, nem a Google vagy a Microsoft szerverein" -->
|
||||
<!-- forrás: index.html:12 „Professzionális otthoni szerver telepítés és üzemeltetés. Te irányítasz, mi segítünk." -->
|
||||
<!-- forrás: index.html:21-22 „Háromszintű automatikus mentés — Minden éjjel készül egy mentés…" -->
|
||||
<!-- forrás: index.html:23-24 „Helyreállítási kód, amit csak te ismersz … Mi nem ismerjük" -->
|
||||
<!-- forrás: index.html:152-154 „Zárt teszt — Néhány magyar háztartással most kezdjük el élesben használni a rendszert" -->
|
||||
<!-- forrás: index.html:165-167 „Ha ez érdekel, írj — nem kötelez semmire, először csak beszélgetünk" -->
|
||||
|
||||
```
|
||||
A fotóid és a dokumentumaid a saját gépeden maradnak – nem egy nagy techcég szerverén.
|
||||
|
||||
A Felhom egy otthoni szerver: mi telepítjük és üzemeltetjük, te csak használod. Minden éjjel mentés készül, és a távoli mentés kulcsát egyedül te ismered.
|
||||
|
||||
Most zárt tesztet indítunk néhány magyar háztartással. Nem kötelez semmire: https://felhom.eu/kapcsolat
|
||||
```
|
||||
|
||||
### 6.2 Közepes változat
|
||||
|
||||
<!-- forrás: a 6.1 összes sora, plusz: -->
|
||||
<!-- forrás: index.html:18 „A beszkennelt papírjaidat a rendszer felismeri és kereshetővé teszi" -->
|
||||
<!-- forrás: index.html:19-20 „A doboz mappái megjelennek a Windows Intézőben és a Mac Finderben" -->
|
||||
<!-- forrás: index.html:25-26 „56 telepíthető alkalmazás — A vezérlőpultból telepíthetők, pár kattintással — parancssor nélkül" -->
|
||||
<!-- forrás: index.html:33-34 „Internetkimaradáskor is működik … az otthoni hálózaton továbbra is eléred a fotóidat és a fájljaidat" -->
|
||||
<!-- forrás: index.html:27-28 „Távoli felügyelet, automatikus frissítés — Figyeljük a doboz állapotát" -->
|
||||
|
||||
```
|
||||
A fotóid, a papírjaid és a médiatárad a saját gépeden maradnak – nem egy nagy techcég szerverén.
|
||||
|
||||
A Felhom egy otthoni szerver, amit mi telepítünk és üzemeltetünk. Te csak használod:
|
||||
– a telefonod fotói maguktól feltöltődnek rá,
|
||||
– a papírjaid kereshetővé válnak,
|
||||
– a mappái ott vannak a Windows Intézőben és a Mac Finderben,
|
||||
– 56 alkalmazás telepíthető pár kattintással,
|
||||
– ha elmegy a net, otthon is eléred a fájljaidat.
|
||||
|
||||
Minden éjjel mentés készül három helyre. A távoli mentés kulcsát egyedül te ismered.
|
||||
|
||||
Most néhány magyar háztartással zárt tesztet indítunk. Nem kötelez semmire, először csak beszélgetünk: https://felhom.eu/kapcsolat
|
||||
```
|
||||
|
||||
@@ -25,6 +25,7 @@ Exit codes: 0 ok · 2 key refused · 3 Scenario A failed · 4 Scenario B failed
|
||||
· 8 a test object read back PUBLISHED (deleted at once — read the report first)
|
||||
"""
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -47,6 +48,13 @@ KEY = "FACEBOOK_API"
|
||||
DEFAULT_EVIDENCE = os.path.join(ROOT, "documentation", "audits", "facebook-page-api-2026-10-08")
|
||||
LOGO = os.path.join(ROOT, "website", "assets", "logo.png")
|
||||
WEEK_S = 7 * 24 * 3600
|
||||
# Meta refuses a scheduled_publish_time outside this window. 10 minutes is Meta's own floor;
|
||||
# the 6-month ceiling is its documented maximum. Both are enforced BEFORE the call, so a bad
|
||||
# time is a local refusal with a readable reason rather than a Graph error.
|
||||
SCHED_MIN_S = 10 * 60
|
||||
SCHED_MAX_S = 180 * 24 * 3600
|
||||
COPY_MD = os.path.join(ROOT, "marketing", "facebook", "COPY.md")
|
||||
POST_LINK = "https://felhom.eu/"
|
||||
# "Felhom teszt – árvíztűrő tükörfúrógép. Ez a bejegyzés törlődik." — built from escapes, never typed
|
||||
# through a shell (brief §9.8).
|
||||
TEST_TEXT = ("Felhom teszt – árvíztűrő tükörfúrógép. "
|
||||
@@ -274,6 +282,25 @@ def scenario_c(g, page):
|
||||
else json.dumps(c.err, ensure_ascii=False)))
|
||||
|
||||
|
||||
def budapest_to_epoch(text):
|
||||
"""'YYYY-MM-DD HH:MM' in Europe/Budapest -> Unix seconds. Uses the real tz database, so the
|
||||
summer/winter offset is never guessed."""
|
||||
from datetime import datetime
|
||||
try:
|
||||
from zoneinfo import ZoneInfo
|
||||
except ImportError: # pragma: no cover - python < 3.9
|
||||
raise ValueError("zoneinfo is unavailable; cannot convert a Budapest wall-clock time safely")
|
||||
try:
|
||||
tz = ZoneInfo("Europe/Budapest")
|
||||
except Exception as e:
|
||||
# No silent fallback to a hardcoded +01:00/+02:00: guessing the offset is how a post goes
|
||||
# out an hour early or late across a DST boundary. Refuse and say what is missing.
|
||||
raise ValueError("no Europe/Budapest in the tz database (%s); install tzdata or run this on "
|
||||
"the Linux host, where the system zoneinfo is present" % e)
|
||||
dt = datetime.strptime(text.strip(), "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
||||
return int(dt.timestamp())
|
||||
|
||||
|
||||
def hexs(s):
|
||||
return (s or "").encode("utf-8").hex()
|
||||
|
||||
@@ -394,6 +421,126 @@ def scenario_e(g, page, rc):
|
||||
return rc, res
|
||||
|
||||
|
||||
# ------------------------------------------------- schedule-post (R-917, the first post)
|
||||
|
||||
def read_copy_section(path, want):
|
||||
"""Return the fenced block under '### <want>' in COPY.md, as text.
|
||||
|
||||
The Hungarian never passes through a shell or an argv string (brief §9.6): the post body is
|
||||
READ FROM THE FILE and sent as UTF-8 bytes. The caller names a section, not a message.
|
||||
"""
|
||||
with io.open(path, encoding="utf-8") as fh:
|
||||
t = fh.read()
|
||||
marker = "### " + want
|
||||
i = t.find(marker)
|
||||
if i < 0:
|
||||
raise ValueError("no section %r in %s" % (want, path))
|
||||
j = t.find("```", i)
|
||||
if j < 0:
|
||||
raise ValueError("section %r has no fenced block" % want)
|
||||
j = t.index("\n", j) + 1
|
||||
k = t.find("```", j)
|
||||
if k < 0:
|
||||
raise ValueError("section %r has an unclosed fenced block" % want)
|
||||
return t[j:k].rstrip("\n")
|
||||
|
||||
|
||||
def check_when(when, now=None):
|
||||
"""Refuse a scheduled time outside Meta's window. Returns the time; raises ValueError."""
|
||||
now = int(time.time()) if now is None else now
|
||||
delta = int(when) - now
|
||||
if delta < SCHED_MIN_S:
|
||||
raise ValueError("scheduled_publish_time is %d s away; Meta's floor is %d s (10 minutes)"
|
||||
% (delta, SCHED_MIN_S))
|
||||
if delta > SCHED_MAX_S:
|
||||
raise ValueError("scheduled_publish_time is %d s away; Meta's ceiling is %d s (6 months)"
|
||||
% (delta, SCHED_MAX_S))
|
||||
return int(when)
|
||||
|
||||
|
||||
def schedule_form(message, when, link=POST_LINK):
|
||||
"""The form for ONE scheduled post. published is ALWAYS 'false' and no argument can change it
|
||||
(brief §9.7): the operator's review in Planner is the safety net, so this command cannot
|
||||
publish immediately even by mistake."""
|
||||
form = {"message": message, "published": "false", "scheduled_publish_time": str(check_when(when))}
|
||||
if link:
|
||||
form["link"] = link
|
||||
return form
|
||||
|
||||
|
||||
def list_scheduled(g, step, pid, ptok):
|
||||
"""The Page's scheduled posts. Two routes are tried and WHICH ONE ANSWERED IS RECORDED —
|
||||
/scheduled_posts is the documented edge; the is_published=false feed filter is the fallback.
|
||||
Returns (ids, route) or (None, None) when both are refused, so the caller can say 'unproven'
|
||||
instead of claiming a removal it did not see."""
|
||||
c = g.call(step, "GET", pid + "/scheduled_posts", ptok, query={"fields": "id,is_published", "limit": "50"})
|
||||
if c.ok and isinstance(c.body.get("data"), list):
|
||||
return [d.get("id") for d in c.body["data"]], "scheduled_posts"
|
||||
c2 = g.call(step + "-feedfallback", "GET", pid + "/feed", ptok,
|
||||
query={"fields": "id,is_published", "limit": "50", "is_published": "false"})
|
||||
if c2.ok and isinstance(c2.body.get("data"), list):
|
||||
return [d.get("id") for d in c2.body["data"]], "feed?is_published=false"
|
||||
log(" scheduled-post LIST refused on both routes: %s | %s"
|
||||
% (json.dumps(c.err, ensure_ascii=False), json.dumps(c2.err, ensure_ascii=False)))
|
||||
return None, None
|
||||
|
||||
|
||||
def scenario_sched_dry(g, page):
|
||||
"""Scenario A: prove the link parameter is safe on a scheduled post, and prove REMOVAL from the
|
||||
list rather than from an error after DELETE."""
|
||||
pid, ptok = page["id"], page["token"]
|
||||
when = int(time.time()) + WEEK_S
|
||||
c = g.call("S1-dry-create", "POST", pid + "/feed", ptok, form=schedule_form(TEST_TEXT, when))
|
||||
if not c.ok:
|
||||
log("S link+schedule REFUSED: %s" % json.dumps(c.err, ensure_ascii=False))
|
||||
return 6, {"created": False, "error": c.err}
|
||||
post_id = c.body.get("id")
|
||||
log("S created %s (scheduled, with link)" % post_id)
|
||||
rb = g.call("S2-dry-readback", "GET", post_id, ptok,
|
||||
query={"fields": "message,is_published,scheduled_publish_time,permalink_url"})
|
||||
res = check_readback("S", rb, hexs(TEST_TEXT), "message", when)
|
||||
res["post_id"] = post_id
|
||||
published = res["is_published"] is True
|
||||
before, route = list_scheduled(g, "S3-list-before", pid, ptok)
|
||||
res["list_route"] = route
|
||||
res["present_before_delete"] = (post_id in before) if before is not None else None
|
||||
log(" list route=%s present=%s" % (route, res["present_before_delete"]))
|
||||
res["delete_ok"] = delete(g, "S4-dry-delete", post_id, ptok)
|
||||
after, _ = list_scheduled(g, "S5-list-after", pid, ptok)
|
||||
res["absent_after_delete"] = (post_id not in after) if after is not None else None
|
||||
log(" after delete: absent=%s" % res["absent_after_delete"])
|
||||
note(g, "S9-dry-verdict", res)
|
||||
if published:
|
||||
log("S !!! the scheduled post read back PUBLISHED — deleted at once")
|
||||
return 8, res
|
||||
return 0, res
|
||||
|
||||
|
||||
def scenario_sched_post(g, page, message, when):
|
||||
pid, ptok = page["id"], page["token"]
|
||||
c = g.call("P1-create", "POST", pid + "/feed", ptok, form=schedule_form(message, when))
|
||||
if not c.ok:
|
||||
log("P REFUSED: %s" % json.dumps(c.err, ensure_ascii=False))
|
||||
return 6, {"created": False, "error": c.err}
|
||||
post_id = c.body.get("id")
|
||||
log("P scheduled %s" % post_id)
|
||||
rb = g.call("P2-readback", "GET", post_id, ptok,
|
||||
query={"fields": "message,is_published,scheduled_publish_time,permalink_url,created_time"})
|
||||
res = check_readback("P", rb, hexs(message), "message", when)
|
||||
res["post_id"] = post_id
|
||||
res["permalink_url"] = rb.body.get("permalink_url") if rb.ok else None
|
||||
ids, route = list_scheduled(g, "P3-list", pid, ptok)
|
||||
res["list_route"] = route
|
||||
res["in_scheduled_list"] = (post_id in ids) if ids is not None else None
|
||||
log(" in the scheduled list (%s): %s" % (route, res["in_scheduled_list"]))
|
||||
if res["is_published"] is True:
|
||||
log("P !!! read back PUBLISHED — this command must never publish; NOT deleting, tell the operator")
|
||||
note(g, "P9-verdict", res)
|
||||
return 8, res
|
||||
note(g, "P9-verdict", res)
|
||||
return 0, res
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- main
|
||||
|
||||
def main(argv=None):
|
||||
@@ -403,7 +550,13 @@ def main(argv=None):
|
||||
ap.add_argument("--version", default="v26.0", help="Graph API version (default v26.0)")
|
||||
ap.add_argument("--evidence", default=DEFAULT_EVIDENCE)
|
||||
ap.add_argument("--credentials", default=os.path.expanduser("~/.config/credentials"))
|
||||
ap.add_argument("cmd", choices=("read", "write-test"))
|
||||
ap.add_argument("cmd", choices=("read", "write-test", "schedule-post"))
|
||||
ap.add_argument("--section", default="6.1",
|
||||
help="COPY.md section to post, e.g. 6.1 or 6.2 (schedule-post)")
|
||||
ap.add_argument("--at", default=None,
|
||||
help="when to publish, 'YYYY-MM-DD HH:MM' in Europe/Budapest (schedule-post)")
|
||||
ap.add_argument("--dry", action="store_true",
|
||||
help="schedule-post: the throwaway link+schedule check, then delete it")
|
||||
a = ap.parse_args(argv)
|
||||
VERBOSE = a.v
|
||||
try:
|
||||
@@ -414,15 +567,16 @@ def main(argv=None):
|
||||
SECRETS.append(token)
|
||||
log("key %s: %d chars, starts %s" % (KEY, len(token), token[:3]))
|
||||
# write-test re-derives A and B into its own sub-directory, so the read run's files stay as they were
|
||||
g = Graph(a.version, a.evidence if a.cmd == "read" else os.path.join(a.evidence, "write-test"))
|
||||
sub = {"read": "", "write-test": "write-test"}.get(a.cmd, "first-post")
|
||||
g = Graph(a.version, a.evidence if not sub else os.path.join(a.evidence, sub))
|
||||
try:
|
||||
return run(a.cmd, g, token)
|
||||
return run(a.cmd, g, token, a)
|
||||
finally:
|
||||
note(g, "F1-headers", g.headers_seen) # §7 F: recorded once each, on every exit path
|
||||
log("F headers: %s" % json.dumps(g.headers_seen))
|
||||
|
||||
|
||||
def run(cmd, g, token):
|
||||
def run(cmd, g, token, a=None):
|
||||
ok_a, _ = scenario_a(g, token)
|
||||
if not ok_a:
|
||||
log("A FAILED")
|
||||
@@ -438,6 +592,26 @@ def run(cmd, g, token):
|
||||
if "CREATE_CONTENT" not in page["tasks"]:
|
||||
log("WRITE GATE: the page's tasks lack CREATE_CONTENT (%s) — no write" % page["tasks"])
|
||||
return 5
|
||||
if cmd == "schedule-post":
|
||||
if a.dry:
|
||||
rc, _ = scenario_sched_dry(g, page)
|
||||
log("schedule-post --dry: done (rc=%d)" % rc)
|
||||
return rc
|
||||
if not a.at:
|
||||
log("schedule-post needs --at 'YYYY-MM-DD HH:MM' (Europe/Budapest)")
|
||||
return 2
|
||||
try:
|
||||
when = budapest_to_epoch(a.at)
|
||||
check_when(when)
|
||||
message = read_copy_section(COPY_MD, a.section)
|
||||
except (ValueError, OSError) as e:
|
||||
log("REFUSED: %s" % e)
|
||||
return 2
|
||||
log("P section %s: %d characters, publishing at %s (epoch %d)"
|
||||
% (a.section, len(message), a.at, when))
|
||||
rc, _ = scenario_sched_post(g, page, message, when)
|
||||
log("schedule-post: done (rc=%d)" % rc)
|
||||
return rc
|
||||
rc, _ = scenario_d(g, page, 0)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
@@ -81,5 +81,111 @@ class GoneError(unittest.TestCase):
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user