mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-08-22 22:27:22 +00:00
RSS - RSS pubDate is wrong on any non-UTC container: local time is relabelled as UTC - #4309 (#4314)
This commit is contained in:
@@ -150,7 +150,8 @@ def populate_feed_entry(fe, watch, content, guid, timestamp, link=None, title_su
|
||||
fe.guid(guid, permalink=False)
|
||||
|
||||
# Set pubDate using the timestamp of this specific change
|
||||
dt = datetime.datetime.fromtimestamp(int(timestamp))
|
||||
dt = dt.replace(tzinfo=pytz.UTC)
|
||||
# Note: tz= must be passed to fromtimestamp(), otherwise we get a naive datetime in the
|
||||
# container's local timezone and relabelling it as UTC shifts every pubDate by the local offset
|
||||
dt = datetime.datetime.fromtimestamp(int(timestamp), tz=pytz.UTC)
|
||||
fe.pubDate(dt)
|
||||
|
||||
|
||||
@@ -91,6 +91,10 @@ export HIDE_REFERER=True
|
||||
REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv -s --maxfail=1 tests/test_notification.py tests/test_access_control.py
|
||||
|
||||
|
||||
# Re #4309 - RSS pubDate/timestamps must be correct on containers that don't run UTC
|
||||
# (Europe/Athens is UTC+2/+3, so any naive local->UTC relabelling shows up as a shifted date)
|
||||
TZ=Europe/Athens pytest -vv -s --maxfail=1 tests/test_rss.py
|
||||
|
||||
# Re-run a few tests that will trigger brotli based storage
|
||||
# And again with brotli+screenshot attachment
|
||||
SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD=5 REMOVE_REQUESTS_OLD_SCREENSHOTS=false pytest -vv -s --maxfail=1 --dist=load tests/test_backend.py tests/test_rss.py tests/test_unique_lines.py tests/test_notification.py tests/test_access_control.py
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import email.utils
|
||||
import os
|
||||
import time
|
||||
import pytest
|
||||
from flask import url_for
|
||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, extract_rss_token_from_UI, \
|
||||
extract_UUID_from_client, delete_all_watches
|
||||
@@ -354,3 +356,54 @@ def test_rss_single_watch_feed(client, live_server, measure_memory_usage, datast
|
||||
assert (">3<" in descriptions[2] or "Version 3" in descriptions[2]) and "content" in descriptions[2], \
|
||||
f"Third item should show Version 3, but got: {descriptions[2][:500]}"
|
||||
|
||||
# #4309 - pubDate must be the real UTC time of the change, whatever the server's local timezone is.
|
||||
# The GUID carries the snapshot timestamp, so the two must agree to the second.
|
||||
for item in items:
|
||||
guid_timestamp = int(item.findtext('guid').rsplit('/', 1)[1])
|
||||
pub_date = email.utils.parsedate_to_datetime(item.findtext('pubDate'))
|
||||
assert pub_date.timestamp() == guid_timestamp, \
|
||||
f"pubDate {item.findtext('pubDate')} does not match snapshot timestamp {guid_timestamp}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(time, 'tzset'), reason="Changing TZ at runtime needs a POSIX platform")
|
||||
@pytest.mark.parametrize("tz_name", ['UTC', 'Europe/Athens', 'America/New_York', 'Australia/Sydney'])
|
||||
def test_rss_pubdate_is_utc_regardless_of_local_timezone(tz_name):
|
||||
"""#4309 - datetime.fromtimestamp() without tz= returns local wall-clock time, and relabelling
|
||||
that as UTC shifts every pubDate by the local offset (items appear in the future on TZ=Europe/Athens).
|
||||
|
||||
No live server needed - this drives populate_feed_entry() directly under several timezones."""
|
||||
import xml.etree.ElementTree as ET
|
||||
from feedgen.feed import FeedGenerator
|
||||
from ..blueprint.rss._util import populate_feed_entry
|
||||
|
||||
timestamp = 1700000000 # Tue, 14 Nov 2023 22:13:20 UTC
|
||||
|
||||
original_tz = os.environ.get('TZ')
|
||||
try:
|
||||
os.environ['TZ'] = tz_name
|
||||
time.tzset()
|
||||
|
||||
fg = FeedGenerator()
|
||||
fg.title('test')
|
||||
fg.link(href='https://example.com', rel='self')
|
||||
fg.description('test')
|
||||
fe = fg.add_entry()
|
||||
|
||||
populate_feed_entry(fe=fe,
|
||||
watch={'uuid': 'fake-uuid', 'url': 'https://example.com'},
|
||||
content='some content',
|
||||
guid=f'fake-uuid/{timestamp}',
|
||||
timestamp=timestamp)
|
||||
|
||||
pub_date = ET.fromstring(fg.rss_str()).findtext('.//item/pubDate')
|
||||
finally:
|
||||
if original_tz is None:
|
||||
os.environ.pop('TZ', None)
|
||||
else:
|
||||
os.environ['TZ'] = original_tz
|
||||
time.tzset()
|
||||
|
||||
# parsedate_to_datetime() honours the offset in the header, so this compares real instants
|
||||
assert email.utils.parsedate_to_datetime(pub_date).timestamp() == timestamp, \
|
||||
f"With TZ={tz_name} the feed said {pub_date}, expected the instant {timestamp}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user