More database tests, test redirect

This commit is contained in:
John Doty 2024-07-17 09:43:25 -07:00
parent 52c12785c8
commit a08257ec76
3 changed files with 258 additions and 23 deletions

View file

@ -96,7 +96,7 @@ def subscribe(url, literal):
result = d result = d
# Check to see if this URL is already in the database. # Check to see if this URL is already in the database.
existing = db.load_feed(result.meta.url) existing = db.load_feed(result.meta.url) # TODO: Replace with 'load_meta'?
if existing is not None: if existing is not None:
click.echo(f"This feed already exists (as {result.meta.url})") click.echo(f"This feed already exists (as {result.meta.url})")
return 1 return 1
@ -129,7 +129,7 @@ def import_opml(opml_file):
click.echo(f"{url} does not seem to be a feed, skipping...") click.echo(f"{url} does not seem to be a feed, skipping...")
continue continue
existing = db.load_feed(meta.url) existing = db.load_feed(meta.url) # TODO: Replace with 'load_meta'?
if existing is not None: if existing is not None:
LOG.info(f"{url} already exists (as {meta.url})") LOG.info(f"{url} already exists (as {meta.url})")
continue continue
@ -151,7 +151,7 @@ def refresh(url):
db = database.Database.local() db = database.Database.local()
if url: if url:
f = db.load_feed(url) f = db.load_feed(url) # TODO: Replace with 'load_meta'?
if f is None: if f is None:
click.echo(f"Not subscribed to {url}") click.echo(f"Not subscribed to {url}")
return 1 return 1

View file

@ -91,7 +91,15 @@ class Database:
if not isinstance(path, str): if not isinstance(path, str):
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
db = sqlite3.Connection(str(path), autocommit=False) db = sqlite3.Connection(str(path), autocommit=False)
db.execute("PRAGMA foreign_keys = ON") # This is WILD that I need to do this because you cannot enable or
# disable foreign keys with a transaction active, and
# `autocommit=False` means that a transaction is *always* active.
db.executescript("COMMIT;PRAGMA foreign_keys = ON;BEGIN;")
cursor = db.execute("PRAGMA foreign_keys")
rows = cursor.fetchall()
assert str(rows[0][0]) == "1", f"Foreign keys not enabled! {rows[0][0]}"
self.db = db self.db = db
self.origin = origin self.origin = origin
@ -433,30 +441,21 @@ class Database:
"UPDATE feeds SET url = ? WHERE url = ?", [new_url, old_url] "UPDATE feeds SET url = ? WHERE url = ?", [new_url, old_url]
) )
else: else:
# Preserve the entries that were under the old url. # First update all the entries that you can with the old url.
self.db.execute( self.db.execute(
""" """
UPDATE entries UPDATE OR IGNORE entries
SET feed_url = ? SET feed_url = ?
WHERE feed_url = ? WHERE feed_url = ?
ON CONFLICT DO UPDATE """,
SET [new_url, old_url],
-- NOTE: This is also part of the feed merge algorithm, BUT
-- we implement it here. See the comment in store_feed
-- for the rationale.
inserted_at=MIN(inserted_at, excluded.inserted_at),
title=CASE
WHEN inserted_at < excluded.inserted_at THEN title
ELSE excluded.title
END,
link=CASE
WHEN inserted_at < excluded.inserted_at THEN link
ELSE excluded.link
END
"""
) )
# Mark the old feed dead. # TODO: It is expensive and not worth it to try to load and
# re-insert all the old stuff so I'm not going to
# bother.
# Mark the old feed unsubscribed.
self.db.execute( self.db.execute(
""" """
UPDATE feeds UPDATE feeds
@ -464,5 +463,5 @@ class Database:
last_fetched_ts = ? last_fetched_ts = ?
WHERE url = ? WHERE url = ?
""", """,
[feed.FEED_STATUS_DEAD, int(time.time()), old_url], [feed.FEED_STATUS_UNSUBSCRIBED, int(time.time()), old_url],
) )

View file

@ -1,9 +1,12 @@
import dataclasses
import pathlib import pathlib
import random import random
import string import string
import tempfile import tempfile
import time
from cry import database from cry import database
from cry import feed
def random_slug() -> str: def random_slug() -> str:
@ -64,3 +67,236 @@ def test_database_prop_get_set():
val = random_slug() val = random_slug()
db.set_property("foo", val) db.set_property("foo", val)
assert db.get_property("foo") == val assert db.get_property("foo") == val
REF_TIME = int(time.time())
FEED = feed.Feed(
meta=feed.FeedMeta(
url="http://example.com/test/feed",
last_fetched_ts=REF_TIME,
retry_after_ts=REF_TIME,
status=feed.FEED_STATUS_ALIVE,
etag=random_slug(),
modified=random_slug(),
),
title="Test Feed",
link="http://example.com/test",
entries=[
feed.Entry(
id=random_slug(),
inserted_at=(REF_TIME * 1000) + index,
title=f"Entry {index}",
link=f"http://example.com/test/a{index}",
)
for index in range(100, 0, -1)
],
)
def test_database_load_store_meta():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
metas = db.load_all_meta()
assert metas == []
def test_database_store_feed():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
loaded = db.load_feed(FEED.meta.url)
assert loaded == FEED
def test_database_store_feed_dups():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
count = db.store_feed(FEED)
assert count == len(FEED.entries)
new_entries = db.store_feed(FEED)
assert new_entries == 0
def test_database_store_feed_fetch_meta():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
meta = db.load_all_meta()
assert meta == [FEED.meta]
def test_database_store_feed_fetch_all():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
expected = dataclasses.replace(FEED, entries=FEED.entries[:13])
all_feeds = db.load_all(feed_limit=13)
assert all_feeds == [expected]
def test_database_store_feed_fetch_all_dups():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
db.store_feed(FEED)
all_feeds = db.load_all(feed_limit=10000)
assert all_feeds == [FEED]
def test_database_store_feed_fetch_pattern_miss():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
expected = dataclasses.replace(FEED, entries=FEED.entries[:13])
all_feeds = db.load_all(feed_limit=13, pattern="no_existo")
assert all_feeds == []
def test_database_store_feed_fetch_pattern_url():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
expected = dataclasses.replace(FEED, entries=FEED.entries[:13])
all_feeds = db.load_all(feed_limit=13, pattern=FEED.link)
assert all_feeds == [expected]
def test_database_store_feed_fetch_pattern_name():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
expected = dataclasses.replace(FEED, entries=FEED.entries[:13])
all_feeds = db.load_all(feed_limit=13, pattern=FEED.title)
assert all_feeds == [expected]
def test_database_store_with_update():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
updated_feed = dataclasses.replace(
FEED,
meta=dataclasses.replace(
FEED.meta,
last_fetched_ts=FEED.meta.last_fetched_ts + 10,
retry_after_ts=FEED.meta.retry_after_ts + 20,
# status=feed.FEED_STATUS_UNSUBSCRIBED,
etag=None,
modified=random_slug(),
),
title=FEED.title + " (updated)",
link=FEED.link + "/updated",
)
db.store_feed(updated_feed)
all_feeds = db.load_all(feed_limit=100)
assert all_feeds == [updated_feed]
def test_database_store_with_older_entries():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
old_entry = FEED.entries[0]
older_entry = dataclasses.replace(
old_entry,
inserted_at=old_entry.inserted_at - 10,
title=old_entry.title + " (older)",
link=old_entry.link + "/older",
)
updated_feed = dataclasses.replace(FEED, entries=[older_entry])
db.store_feed(updated_feed)
all_feeds = db.load_all(feed_limit=100)
found_entries = list(
filter(
lambda e: e.id == older_entry.id,
all_feeds[0].entries,
)
)
assert found_entries == [older_entry]
def test_database_store_update_meta():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
new_meta = dataclasses.replace(
FEED.meta,
last_fetched_ts=FEED.meta.last_fetched_ts + 10,
retry_after_ts=FEED.meta.last_fetched_ts + 20,
status=feed.FEED_STATUS_DEAD,
etag=random_slug(),
modified=random_slug(),
)
db.update_meta(new_meta)
assert db.load_all_meta()[0] == new_meta
def test_database_set_feed_status():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
assert db.load_all_meta()[0].status != feed.FEED_STATUS_UNSUBSCRIBED
db.set_feed_status(FEED.meta.url, feed.FEED_STATUS_UNSUBSCRIBED)
# TODO: Ensure that the updated time is touched too.
assert db.load_all_meta()[0].status == feed.FEED_STATUS_UNSUBSCRIBED
def test_database_redirect_clean():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
new_url = f"http://example.com/redirect/{random_slug()}"
db.redirect_feed(FEED.meta.url, new_url)
expected_meta = dataclasses.replace(FEED.meta, url=new_url)
assert db.load_all_meta() == [expected_meta]
expected_feed = dataclasses.replace(FEED, meta=expected_meta)
assert db.load_all(feed_limit=9999) == [expected_feed]
def test_database_redirect_with_merge():
db = database.Database(":memory:", random_slug())
db.ensure_database_schema()
db.store_feed(FEED)
new_url = f"http://example.com/redirect/{random_slug()}"
expected_meta = dataclasses.replace(FEED.meta, url=new_url)
expected_feed = dataclasses.replace(FEED, meta=expected_meta)
db.store_feed(expected_feed)
# NOTE: This is flaky because the time might shift on me.
db.redirect_feed(FEED.meta.url, new_url)
old_dead_meta = dataclasses.replace(FEED.meta, status=feed.FEED_STATUS_UNSUBSCRIBED)
assert db.load_all_meta() == [old_dead_meta, expected_meta]
assert db.load_all(feed_limit=9999) == [expected_feed]