Spaces:
Running
Running
File size: 17,484 Bytes
6c3a31d 58f00c9 6c3a31d 58f00c9 49b8170 58f00c9 6c3a31d 58f00c9 c3c6394 ce89343 f4c871b ce89343 c3c6394 6c3a31d c3c6394 f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | import json
import pytest
from bot.db import AUTO_FORMATS_MIGRATION, DEFAULT_CAPTION, DEFAULT_FORMAT, DEFAULT_SETTINGS, Database, Format, User, normalize_database_url
from bot.parser import DEFAULT_TEMPLATE
@pytest.fixture
async def db(tmp_path):
database = Database(f"sqlite+aiosqlite:///{tmp_path}/t.db")
await database.init()
yield database
await database.close()
def test_normalize_postgres_urls():
url, args = normalize_database_url("postgres://u:p@host/db?sslmode=require&channel_binding=require")
assert url == "postgresql+asyncpg://u:p@host/db"
assert args == {"ssl": "require"}
url, args = normalize_database_url("postgresql://u:p@host/db")
assert url == "postgresql+asyncpg://u:p@host/db" and args == {}
url, args = normalize_database_url("sqlite+aiosqlite:///x.db")
assert url == "sqlite+aiosqlite:///x.db" and args == {}
async def test_defaults_and_update_roundtrip(db):
s = await db.get_settings(1)
assert s == DEFAULT_SETTINGS
s["mode"] = "tampered" # returned copy must not leak into the cache
assert (await db.get_settings(1))["mode"] == "filename"
await db.update_settings(1, mode="caption", keep_audio=["2", "1"], meta={"title": "Hello"})
await db.update_settings(1, meta={"author": "me"})
# a fresh Database instance proves the values were persisted, not just cached
other = Database(db.url)
s2 = await other.get_settings(1)
assert s2["mode"] == "caption"
assert s2["keep_audio"] == ["2", "1"]
assert s2["meta"]["title"] == "Hello" and s2["meta"]["author"] == "me" and s2["meta"]["artist"] == ""
await other.close()
async def test_unknown_setting_rejected(db):
with pytest.raises(KeyError):
await db.update_settings(1, nope=1)
async def test_stats(db):
await db.record_job(5, 1000)
await db.record_job(5, 24)
await db.get_settings(6)
assert await db.count_users() == 2
assert await db.totals() == (2, 1024)
async def test_admins_and_kv(db):
assert await db.add_admin(10, 1) is True
assert await db.add_admin(10, 1) is False
assert await db.add_admin(11, 1) is True
assert sorted(await db.list_admins()) == [10, 11]
assert await db.del_admin(10) is True
assert await db.del_admin(10) is False
assert await db.list_admins() == [11]
assert await db.kv_get("session") is None
await db.kv_set("session", "abc")
await db.kv_set("session", "def")
assert await db.kv_get("session") == "def"
await db.kv_delete("session")
assert await db.kv_get("session") is None
async def test_a_foreign_table_called_formats_in_a_shared_database_is_left_alone(tmp_path):
"""The Postgres can be shared with another app whose "formats" table has other columns (that broke /rename once)."""
import sqlite3
path = tmp_path / "shared.db"
other_app = sqlite3.connect(path)
other_app.execute("CREATE TABLE formats (id INTEGER PRIMARY KEY, name TEXT, body TEXT)")
other_app.execute("INSERT INTO formats (name, body) VALUES ('their format', 'their body')")
other_app.commit()
other_app.close()
database = Database(f"sqlite+aiosqlite:///{path}")
await database.init()
try:
created = await database.create_format(1, "One Piece", "{title} E{episode}")
assert [f["name"] for f in await database.list_formats(1)] == ["One Piece"]
assert (await database.get_format(1, created["id"]))["template"] == "{title} E{episode}"
assert await database.share_token(1, created["id"]) # the sharing table is ours too
assert await database.delete_format(1, created["id"]) is True
finally:
await database.close()
other_app = sqlite3.connect(path)
assert other_app.execute("SELECT name, body FROM formats").fetchall() == [("their format", "their body")]
assert [r[1] for r in other_app.execute("PRAGMA table_info(formats)")] == ["id", "name", "body"]
other_app.close()
async def test_first_use_by_many_concurrent_handlers_does_not_collide(db):
"""A forwarded batch is handled by several tasks at once; all of them look the new user up together."""
import asyncio
results = await asyncio.gather(*(db.get_settings(77) for _ in range(8)))
assert all(r == DEFAULT_SETTINGS for r in results)
# the same goes for the other writers that create the row on first use
await asyncio.gather(*(db.record_job(78, 10) for _ in range(5)), *(db.update_settings(79, mode="caption") for _ in range(5)))
assert await db.count_users() == 3
# ---------------------------------------------------------------------------
# bold captions
# ---------------------------------------------------------------------------
async def test_captions_are_bold_by_default(db):
assert DEFAULT_CAPTION == DEFAULT_FORMAT["caption"] == "<b>{filename}</b>"
assert (await db.create_format(1, "New", kind="auto"))["caption"] == "<b>{filename}</b>"
assert "caption" not in DEFAULT_SETTINGS and "template" not in DEFAULT_SETTINGS # they live in the formats now
async def test_the_plain_default_caption_of_earlier_versions_becomes_bold_once(db):
import json
from bot.db import AUTO_FORMATS_MIGRATION, BOLD_CAPTION_MIGRATION, User
# what an older version left behind: no markers yet, and the settings stored in full
await db.kv_delete(BOLD_CAPTION_MIGRATION)
await db.kv_delete(AUTO_FORMATS_MIGRATION)
stored = {
1: {"caption": "{filename}", "mode": "caption", "template": "T {title}"}, # still the old default
2: {"caption": "{filename} · {size}"}, # their own caption
3: {"mode": "caption"}, # no caption stored: gets the default
4: "not json",
5: [1, 2],
}
async with db._sessions() as session:
for uid, value in stored.items():
session.add(User(id=uid, settings=value if isinstance(value, str) else json.dumps(value)))
await session.commit()
await db.init() # runs the migrations
fresh = Database(db.url) # a new instance has no cache
try:
# the old default became the bold one, and the old template and caption became an auto format
s1 = await fresh.get_settings(1)
assert s1["mode"] == "caption"
(one,) = await fresh.list_formats(1, "auto")
assert one["name"] == "Default" and one["caption"] == "<b>{filename}</b>" and one["template"] == "T {title}"
assert s1["auto_format"] == one["id"]
(two,) = await fresh.list_formats(2, "auto")
assert two["caption"] == "{filename} · {size}" and two["template"] == DEFAULT_FORMAT["template"]
assert await fresh.list_formats(3) == [] and (await fresh.get_settings(3))["mode"] == "caption" # nothing of theirs to keep
assert await fresh.get_settings(4) == DEFAULT_SETTINGS and await fresh.get_settings(5) == DEFAULT_SETTINGS
await fresh.update_format(1, one["id"], caption="{filename}") # going back to a plain caption afterwards...
finally:
await fresh.close()
await db.init()
again = Database(db.url)
try:
assert (await again.get_format(1, one["id"]))["caption"] == "{filename}" # ...sticks, also across restarts
assert len(await again.list_formats(1)) == 1 # and the migrations ran only once
finally:
await again.close()
# ---------------------------------------------------------------------------
# sharing formats
# ---------------------------------------------------------------------------
async def test_share_token_is_stable_private_to_the_owner_and_dies_with_the_format(db):
fmt = await db.create_format(1, "One Piece", "E{episode}")
token = await db.share_token(1, fmt["id"])
assert token and len(token) >= 16 and token == await db.share_token(1, fmt["id"])
other = await db.create_format(1, "Dragon Ball")
assert await db.share_token(1, other["id"]) != token
assert await db.share_token(2, fmt["id"]) is None and await db.share_token(1, 999) is None
owner, shared = await db.get_shared_format(token)
assert owner == 1 and shared["name"] == "One Piece" and shared["template"] == "E{episode}"
await db.update_format(1, fmt["id"], template="New {title}")
assert (await db.get_shared_format(token))[1]["template"] == "New {title}" # the format as it is now
assert await db.get_shared_format("nope") is None
assert await db.delete_format(1, fmt["id"]) is True
assert await db.get_shared_format(token) is None
await db.create_format(1, "Another") # it may get the deleted format's id: the old link must not lead to it
assert await db.get_shared_format(token) is None
assert await db.get_shared_format(await db.share_token(1, other["id"])) is not None # the other link is untouched
async def test_copy_format_copies_everything_and_finds_a_free_name(db):
src = await db.create_format(1, "One Piece", "E{episode}")
await db.update_format(1, src["id"], meta={"title": "OP"}, keep_audio=["japanese"], keep_subtitle=["2"])
await db.set_format_thumb(1, src["id"], b"\xff\xd8thumb")
mine = await db.copy_format(2, 1, src["id"])
assert mine["name"] == "One Piece" and mine["template"] == "E{episode}" and mine["meta"]["title"] == "OP"
assert mine["keep_audio"] == ["japanese"] and mine["keep_subtitle"] == ["2"] and mine["has_thumb"] is True
assert mine["id"] != src["id"] and await db.get_format_thumb(2, mine["id"]) == b"\xff\xd8thumb"
assert [(await db.copy_format(2, 1, src["id"]))["name"] for _ in range(2)] == ["One Piece (2)", "One Piece (3)"]
await db.update_format(2, mine["id"], template="Changed") # the copy is independent of the original
assert (await db.get_format(1, src["id"]))["template"] == "E{episode}"
assert await db.copy_format(2, 3, src["id"]) is None and await db.copy_format(2, 1, 999) is None # not theirs / gone
big = await db.create_format(1, "N" * 30)
names = [(await db.copy_format(3, 1, big["id"], name_max=30))["name"] for _ in range(2)]
assert names == ["N" * 30, "N" * 26 + " (2)"] # the number fits inside the length limit
async def test_copy_format_stops_at_the_limit(db):
from bot.db import MAX_FORMATS
from bot.errors import FormatError
src = await db.create_format(1, "One Piece")
for i in range(MAX_FORMATS):
await db.create_format(2, f"F{i}")
with pytest.raises(FormatError) as raised:
await db.copy_format(2, 1, src["id"])
assert raised.value.code == "limit" and await db.count_formats(2) == MAX_FORMATS
# ---------------------------------------------------------------------------
# from the global template, caption and thumbnail to auto formats
# ---------------------------------------------------------------------------
META = {"title": "My Title", "author": "", "artist": "", "audio": "", "subtitle": "", "video": ""}
async def start_after_an_old_version(db, users, formats=()):
"""Store what a version without auto formats left behind (no marker yet), start the bot on it and return a fresh Database.
``users``: ``{id: (stored settings, old thumbnail)}``; ``formats``: ``[(user id, name, stored data, thumbnail)]`` - formats of
that version had no kind.
"""
await db.kv_delete(AUTO_FORMATS_MIGRATION)
async with db._sessions() as session:
for uid, (settings, thumb) in users.items():
session.add(User(id=uid, settings=json.dumps(settings), thumb=thumb))
for uid, name, data, thumb in formats:
session.add(Format(user_id=uid, name=name, data=json.dumps(data), thumb=thumb))
await session.commit()
await db.init()
return Database(db.url) # a new instance has no cache
async def test_the_old_template_caption_and_thumbnail_become_the_auto_format_default(db):
fresh = await start_after_an_old_version(
db, {1: ({"template": "T {title}", "caption": "<i>{filename}</i>", "mode": "caption"}, b"\xff\xd8old")}
)
try:
(auto,) = await fresh.list_formats(1)
assert (auto["name"], auto["kind"]) == ("Default", "auto")
assert auto["template"] == "T {title}" and auto["caption"] == "<i>{filename}</i>" and auto["has_thumb"]
assert await fresh.get_format_thumb(1, auto["id"]) == b"\xff\xd8old"
settings = await fresh.get_settings(1)
assert settings["auto_format"] == auto["id"] and settings["mode"] == "caption" # in use, the rest of the settings is untouched
assert (await fresh.get_auto_format(1))["id"] == auto["id"]
async with fresh._sessions() as session: # and nothing is kept in two places
row = await session.get(User, 1)
assert row.thumb is None and {"template", "caption"}.isdisjoint(json.loads(row.settings))
finally:
await fresh.close()
async def test_the_old_metadata_stays_as_the_default_metadata_and_is_not_copied_into_the_auto_format(db):
users = {
1: ({"template": "A {title}", "metadata_on": True, "meta": META, "keep_audio": ["english"]}, None),
2: ({"template": "B {title}", "metadata_on": False, "meta": META, "keep_audio": ["english"]}, None),
3: ({"metadata_on": True, "meta": META}, None), # nothing else of its own
}
fresh = await start_after_an_old_version(db, users)
try:
for uid, template in ((1, "A {title}"), (2, "B {title}")):
(auto,) = await fresh.list_formats(uid)
# no snapshot: it has none of its own, so the default metadata applies to it while it is On, edits included
assert auto["template"] == template and not any(auto["meta"].values()) and auto["keep_audio"] == [] and auto["keep_subtitle"] == []
assert await fresh.list_formats(3) == [] # nothing to keep: the built-in defaults are used
for uid in (1, 2, 3): # the metadata is where it always was
settings = await fresh.get_settings(uid)
assert settings["meta"]["title"] == "My Title" and settings["metadata_on"] is (uid in (1, 3))
assert (await fresh.get_settings(2))["keep_audio"] == ["english"] and (await fresh.get_settings(1))["keep_audio"] == ["english"]
finally:
await fresh.close()
async def test_formats_made_before_the_kinds_are_manual_and_the_ones_without_a_thumbnail_keep_the_old_one(db):
fresh = await start_after_an_old_version(
db,
{1: ({"template": "T"}, b"old-1"), 2: ({}, b"old-2"), 3: ({}, None)},
formats=[
(1, "Own thumb", {"template": "X"}, b"own"), (1, "Borrowing", {"template": "Y"}, None),
(2, "Other", {}, None), (3, "Plain", {}, None),
],
)
try:
own, borrowing = await fresh.list_formats(1, "manual")
assert (own["name"], borrowing["name"]) == ("Own thumb", "Borrowing") and own["template"] == "X"
assert await fresh.get_format_thumb(1, own["id"]) == b"own" # it had one of its own
assert await fresh.get_format_thumb(1, borrowing["id"]) == b"old-1" # it used the old global one: it still does
(auto,) = await fresh.list_formats(1, "auto")
assert await fresh.get_format_thumb(1, auto["id"]) == b"old-1"
(other,) = await fresh.list_formats(2, "manual")
assert await fresh.get_format_thumb(2, other["id"]) == b"old-2"
(their_auto,) = await fresh.list_formats(2, "auto") # a thumbnail alone is something to keep
assert their_auto["template"] == DEFAULT_TEMPLATE and their_auto["has_thumb"]
(plain,) = await fresh.list_formats(3)
assert plain["kind"] == "manual" and not plain["has_thumb"] and await fresh.get_auto_format(3) is None
finally:
await fresh.close()
async def test_someone_who_never_changed_anything_gets_no_auto_format(db):
fresh = await start_after_an_old_version(db, {1: ({}, None), 2: ({"template": DEFAULT_TEMPLATE, "caption": DEFAULT_CAPTION}, None)})
try:
for uid in (1, 2):
assert await fresh.list_formats(uid) == [] and (await fresh.get_settings(uid))["auto_format"] == 0
assert await fresh.get_auto_format(uid) is None
finally:
await fresh.close()
async def test_the_migration_makes_each_users_auto_format_once(db):
fresh = await start_after_an_old_version(db, {1: ({"template": "T"}, b"t")})
await fresh.close()
await db.kv_delete(AUTO_FORMATS_MIGRATION) # as if the marker was lost: the user's own mark is what counts
await db.init()
again = Database(db.url)
try:
assert len(await again.list_formats(1)) == 1
assert await again.kv_get(AUTO_FORMATS_MIGRATION) == "done"
finally:
await again.close()
async def test_settings_that_cannot_be_read_are_left_as_they_are(db):
await db.kv_delete(AUTO_FORMATS_MIGRATION)
async with db._sessions() as session:
session.add(User(id=1, settings="not json", thumb=b"x"))
session.add(User(id=2, settings="[1, 2]"))
await session.commit()
await db.init()
async with db._sessions() as session:
assert (await session.get(User, 1)).settings == "not json" and (await session.get(User, 2)).settings == "[1, 2]"
assert await db.list_formats(1) == [] and await db.list_formats(2) == []
async def test_a_new_installation_just_notes_that_nothing_needs_changing(tmp_path):
fresh = Database(f"sqlite+aiosqlite:///{tmp_path}/new.db")
await fresh.init()
try:
assert await fresh.kv_get(AUTO_FORMATS_MIGRATION) == "done" and await fresh.count_users() == 0
finally:
await fresh.close()
|